aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/xtext
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-11-10 14:35:24 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-11-10 14:35:24 +0100
commit4971c864a603e0c01f7ad84a23697905d096283b (patch)
treefc2ed2ced39aa50aa98602ad4bd2a9d877577dcd /subprojects/frontend/src/xtext
parentrefactor: rename CallKind to Polarity (diff)
downloadrefinery-4971c864a603e0c01f7ad84a23697905d096283b.tar.gz
refinery-4971c864a603e0c01f7ad84a23697905d096283b.tar.zst
refinery-4971c864a603e0c01f7ad84a23697905d096283b.zip
feat(web): backend URL configuration
To point the frontend to a backend server, update the config.json file in the website root. The config.json is generated automatically in debug mode and when running from a standalone jar.
Diffstat (limited to 'subprojects/frontend/src/xtext')
-rw-r--r--subprojects/frontend/src/xtext/XtextClient.ts3
-rw-r--r--subprojects/frontend/src/xtext/XtextWebSocketClient.ts65
-rw-r--r--subprojects/frontend/src/xtext/fetchBackendConfig.ts16
-rw-r--r--subprojects/frontend/src/xtext/webSocketMachine.ts36
4 files changed, 53 insertions, 67 deletions
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts
index e7d26ae6..14fb2430 100644
--- a/subprojects/frontend/src/xtext/XtextClient.ts
+++ b/subprojects/frontend/src/xtext/XtextClient.ts
@@ -35,8 +35,7 @@ export default class XtextClient {
35 this.webSocketClient = new XtextWebSocketClient( 35 this.webSocketClient = new XtextWebSocketClient(
36 () => this.onReconnect(), 36 () => this.onReconnect(),
37 () => this.onDisconnect(), 37 () => this.onDisconnect(),
38 (resource, stateId, service, push) => 38 this.onPush.bind(this),
39 this.onPush(resource, stateId, service, push),
40 ); 39 );
41 this.updateService = new UpdateService(store, this.webSocketClient); 40 this.updateService = new UpdateService(store, this.webSocketClient);
42 this.contentAssistService = new ContentAssistService(this.updateService); 41 this.contentAssistService = new ContentAssistService(this.updateService);
diff --git a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts
index a39620cb..6b734546 100644
--- a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts
+++ b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts
@@ -7,7 +7,8 @@ import CancelledError from '../utils/CancelledError';
7import PendingTask from '../utils/PendingTask'; 7import PendingTask from '../utils/PendingTask';
8import getLogger from '../utils/getLogger'; 8import getLogger from '../utils/getLogger';
9 9
10import webSocketMachine, { isWebSocketURLLocal } from './webSocketMachine'; 10import fetchBackendConfig from './fetchBackendConfig';
11import webSocketMachine from './webSocketMachine';
11import { 12import {
12 type XtextWebPushService, 13 type XtextWebPushService,
13 XtextResponse, 14 XtextResponse,
@@ -42,26 +43,18 @@ export default class XtextWebSocketClient {
42 private readonly pendingRequests = new Map<string, PendingTask<unknown>>(); 43 private readonly pendingRequests = new Map<string, PendingTask<unknown>>();
43 44
44 private readonly interpreter = interpret( 45 private readonly interpreter = interpret(
45 webSocketMachine 46 webSocketMachine.withConfig({
46 .withContext({ 47 actions: {
47 ...webSocketMachine.context, 48 openWebSocket: () => this.openWebSocket(),
48 webSocketURL: `${window.location.origin.replace( 49 closeWebSocket: () => this.closeWebSocket(),
49 /^http/, 50 notifyReconnect: () => this.onReconnect(),
50 'ws', 51 notifyDisconnect: () => this.onDisconnect(),
51 )}/xtext-service`, 52 cancelPendingRequests: () => this.cancelPendingRequests(),
52 }) 53 },
53 .withConfig({ 54 services: {
54 actions: { 55 pingService: () => this.sendPing(),
55 openWebSocket: ({ webSocketURL }) => this.openWebSocket(webSocketURL), 56 },
56 closeWebSocket: () => this.closeWebSocket(), 57 }),
57 notifyReconnect: () => this.onReconnect(),
58 notifyDisconnect: () => this.onDisconnect(),
59 cancelPendingRequests: () => this.cancelPendingRequests(),
60 },
61 services: {
62 pingService: () => this.sendPing(),
63 },
64 }),
65 { 58 {
66 logger: log.log.bind(log), 59 logger: log.log.bind(log),
67 }, 60 },
@@ -151,6 +144,7 @@ export default class XtextWebSocketClient {
151 | 'webSocket' 144 | 'webSocket'
152 | 'interpreter' 145 | 'interpreter'
153 | 'openListener' 146 | 'openListener'
147 | 'openWebSocket'
154 | 'errorListener' 148 | 'errorListener'
155 | 'closeListener' 149 | 'closeListener'
156 | 'messageListener' 150 | 'messageListener'
@@ -160,6 +154,7 @@ export default class XtextWebSocketClient {
160 webSocket: observable.ref, 154 webSocket: observable.ref,
161 interpreter: false, 155 interpreter: false,
162 openListener: false, 156 openListener: false,
157 openWebSocket: false,
163 errorListener: false, 158 errorListener: false,
164 closeListener: false, 159 closeListener: false,
165 messageListener: false, 160 messageListener: false,
@@ -221,9 +216,7 @@ export default class XtextWebSocketClient {
221 get networkMissing(): boolean { 216 get networkMissing(): boolean {
222 return ( 217 return (
223 this.state.matches('connection.temporarilyOffline') || 218 this.state.matches('connection.temporarilyOffline') ||
224 (this.disconnectedByUser && 219 (this.disconnectedByUser && this.state.matches('network.offline'))
225 this.state.matches('network.offline') &&
226 !isWebSocketURLLocal(this.state.context.webSocketURL))
227 ); 220 );
228 } 221 }
229 222
@@ -275,17 +268,27 @@ export default class XtextWebSocketClient {
275 this.interpreter.send(document.hidden ? 'TAB_HIDDEN' : 'TAB_VISIBLE'); 268 this.interpreter.send(document.hidden ? 'TAB_HIDDEN' : 'TAB_VISIBLE');
276 } 269 }
277 270
278 private openWebSocket(webSocketURL: string | undefined): void { 271 private openWebSocket(): void {
279 if (this.webSocket !== undefined) { 272 if (this.webSocket !== undefined) {
280 throw new Error('WebSocket already open'); 273 throw new Error('WebSocket already open');
281 } 274 }
282 275
283 if (webSocketURL === undefined) {
284 throw new Error('URL not configured');
285 }
286
287 log.debug('Creating WebSocket'); 276 log.debug('Creating WebSocket');
288 277
278 (async () => {
279 const { webSocketURL } = await fetchBackendConfig();
280 this.openWebSocketWithURL(webSocketURL);
281 })().catch((error) => {
282 log.error('Error while initializing connection', error);
283 const message = error instanceof Error ? error.message : String(error);
284 this.interpreter.send({
285 type: 'ERROR',
286 message,
287 });
288 });
289 }
290
291 private openWebSocketWithURL(webSocketURL: string): void {
289 this.webSocket = new WebSocket(webSocketURL, XTEXT_SUBPROTOCOL_V1); 292 this.webSocket = new WebSocket(webSocketURL, XTEXT_SUBPROTOCOL_V1);
290 this.webSocket.addEventListener('open', this.openListener); 293 this.webSocket.addEventListener('open', this.openListener);
291 this.webSocket.addEventListener('close', this.closeListener); 294 this.webSocket.addEventListener('close', this.closeListener);
@@ -295,7 +298,9 @@ export default class XtextWebSocketClient {
295 298
296 private closeWebSocket() { 299 private closeWebSocket() {
297 if (this.webSocket === undefined) { 300 if (this.webSocket === undefined) {
298 throw new Error('WebSocket already closed'); 301 // We might get here when there is a network error before the socket is initialized
302 // and we don't have to do anything to close it.
303 return;
299 } 304 }
300 305
301 log.debug('Closing WebSocket'); 306 log.debug('Closing WebSocket');
diff --git a/subprojects/frontend/src/xtext/fetchBackendConfig.ts b/subprojects/frontend/src/xtext/fetchBackendConfig.ts
new file mode 100644
index 00000000..f8087a70
--- /dev/null
+++ b/subprojects/frontend/src/xtext/fetchBackendConfig.ts
@@ -0,0 +1,16 @@
1/* eslint-disable @typescript-eslint/no-redeclare -- Declare types with their companion objects */
2
3import { z } from 'zod';
4
5export const BackendConfig = z.object({
6 webSocketURL: z.string().url(),
7});
8
9export type BackendConfig = z.infer<typeof BackendConfig>;
10
11export default async function fetchBackendConfig(): Promise<BackendConfig> {
12 const configURL = `${import.meta.env.BASE_URL}config.json`;
13 const response = await fetch(configURL);
14 const rawConfig = (await response.json()) as unknown;
15 return BackendConfig.parse(rawConfig);
16}
diff --git a/subprojects/frontend/src/xtext/webSocketMachine.ts b/subprojects/frontend/src/xtext/webSocketMachine.ts
index b5b40d11..5f6bc604 100644
--- a/subprojects/frontend/src/xtext/webSocketMachine.ts
+++ b/subprojects/frontend/src/xtext/webSocketMachine.ts
@@ -6,12 +6,10 @@ const { raise } = actions;
6const ERROR_WAIT_TIMES = ['200', '1s', '5s', '30s'].map(ms); 6const ERROR_WAIT_TIMES = ['200', '1s', '5s', '30s'].map(ms);
7 7
8export interface WebSocketContext { 8export interface WebSocketContext {
9 webSocketURL: string | undefined;
10 errors: string[]; 9 errors: string[];
11} 10}
12 11
13export type WebSocketEvent = 12export type WebSocketEvent =
14 | { type: 'CONFIGURE'; webSocketURL: string }
15 | { type: 'CONNECT' } 13 | { type: 'CONNECT' }
16 | { type: 'DISCONNECT' } 14 | { type: 'DISCONNECT' }
17 | { type: 'OPENED' } 15 | { type: 'OPENED' }
@@ -25,24 +23,6 @@ export type WebSocketEvent =
25 | { type: 'OFFLINE' } 23 | { type: 'OFFLINE' }
26 | { type: 'ERROR'; message: string }; 24 | { type: 'ERROR'; message: string };
27 25
28export function isWebSocketURLLocal(webSocketURL: string | undefined): boolean {
29 if (webSocketURL === undefined) {
30 return false;
31 }
32 let hostname: string;
33 try {
34 ({ hostname } = new URL(webSocketURL));
35 } catch {
36 return false;
37 }
38 // https://stackoverflow.com/a/57949518
39 return (
40 hostname === 'localhost' ||
41 hostname === '[::1]' ||
42 hostname.match(/^127(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/) !== null
43 );
44}
45
46export default createMachine( 26export default createMachine(
47 { 27 {
48 id: 'webSocket', 28 id: 'webSocket',
@@ -53,7 +33,6 @@ export default createMachine(
53 }, 33 },
54 tsTypes: {} as import('./webSocketMachine.typegen').Typegen0, 34 tsTypes: {} as import('./webSocketMachine.typegen').Typegen0,
55 context: { 35 context: {
56 webSocketURL: undefined,
57 errors: [], 36 errors: [],
58 }, 37 },
59 type: 'parallel', 38 type: 'parallel',
@@ -65,16 +44,12 @@ export default createMachine(
65 disconnected: { 44 disconnected: {
66 id: 'disconnected', 45 id: 'disconnected',
67 entry: ['clearErrors', 'notifyDisconnect'], 46 entry: ['clearErrors', 'notifyDisconnect'],
68 on: {
69 CONFIGURE: { actions: 'configure' },
70 },
71 }, 47 },
72 timedOut: { 48 timedOut: {
73 id: 'timedOut', 49 id: 'timedOut',
74 always: [ 50 always: [
75 { 51 {
76 target: 'temporarilyOffline', 52 target: 'temporarilyOffline',
77 cond: 'needsNetwork',
78 in: '#offline', 53 in: '#offline',
79 }, 54 },
80 { target: 'socketCreated', in: '#tabVisible' }, 55 { target: 'socketCreated', in: '#tabVisible' },
@@ -89,7 +64,6 @@ export default createMachine(
89 always: [ 64 always: [
90 { 65 {
91 target: 'temporarilyOffline', 66 target: 'temporarilyOffline',
92 cond: 'needsNetwork',
93 in: '#offline', 67 in: '#offline',
94 }, 68 },
95 ], 69 ],
@@ -183,7 +157,7 @@ export default createMachine(
183 }, 157 },
184 }, 158 },
185 on: { 159 on: {
186 CONNECT: { target: '.timedOut', cond: 'hasWebSocketURL' }, 160 CONNECT: '.timedOut',
187 DISCONNECT: '.disconnected', 161 DISCONNECT: '.disconnected',
188 }, 162 },
189 }, 163 },
@@ -224,10 +198,6 @@ export default createMachine(
224 }, 198 },
225 }, 199 },
226 { 200 {
227 guards: {
228 hasWebSocketURL: ({ webSocketURL }) => webSocketURL !== undefined,
229 needsNetwork: ({ webSocketURL }) => !isWebSocketURLLocal(webSocketURL),
230 },
231 delays: { 201 delays: {
232 IDLE_TIMEOUT: ms('5m'), 202 IDLE_TIMEOUT: ms('5m'),
233 OPEN_TIMEOUT: ms('10s'), 203 OPEN_TIMEOUT: ms('10s'),
@@ -239,10 +209,6 @@ export default createMachine(
239 }, 209 },
240 }, 210 },
241 actions: { 211 actions: {
242 configure: assign((context, { webSocketURL }) => ({
243 ...context,
244 webSocketURL,
245 })),
246 pushError: assign((context, { message }) => ({ 212 pushError: assign((context, { message }) => ({
247 ...context, 213 ...context,
248 errors: [...context.errors, message], 214 errors: [...context.errors, message],