From 4971c864a603e0c01f7ad84a23697905d096283b Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 10 Nov 2022 14:35:24 +0100 Subject: 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. --- subprojects/frontend/src/xtext/XtextClient.ts | 3 +- .../frontend/src/xtext/XtextWebSocketClient.ts | 65 ++++++++++++---------- .../frontend/src/xtext/fetchBackendConfig.ts | 16 ++++++ subprojects/frontend/src/xtext/webSocketMachine.ts | 36 +----------- 4 files changed, 53 insertions(+), 67 deletions(-) create mode 100644 subprojects/frontend/src/xtext/fetchBackendConfig.ts (limited to 'subprojects/frontend/src/xtext') 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 { this.webSocketClient = new XtextWebSocketClient( () => this.onReconnect(), () => this.onDisconnect(), - (resource, stateId, service, push) => - this.onPush(resource, stateId, service, push), + this.onPush.bind(this), ); this.updateService = new UpdateService(store, this.webSocketClient); 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'; import PendingTask from '../utils/PendingTask'; import getLogger from '../utils/getLogger'; -import webSocketMachine, { isWebSocketURLLocal } from './webSocketMachine'; +import fetchBackendConfig from './fetchBackendConfig'; +import webSocketMachine from './webSocketMachine'; import { type XtextWebPushService, XtextResponse, @@ -42,26 +43,18 @@ export default class XtextWebSocketClient { private readonly pendingRequests = new Map>(); private readonly interpreter = interpret( - webSocketMachine - .withContext({ - ...webSocketMachine.context, - webSocketURL: `${window.location.origin.replace( - /^http/, - 'ws', - )}/xtext-service`, - }) - .withConfig({ - actions: { - openWebSocket: ({ webSocketURL }) => this.openWebSocket(webSocketURL), - closeWebSocket: () => this.closeWebSocket(), - notifyReconnect: () => this.onReconnect(), - notifyDisconnect: () => this.onDisconnect(), - cancelPendingRequests: () => this.cancelPendingRequests(), - }, - services: { - pingService: () => this.sendPing(), - }, - }), + webSocketMachine.withConfig({ + actions: { + openWebSocket: () => this.openWebSocket(), + closeWebSocket: () => this.closeWebSocket(), + notifyReconnect: () => this.onReconnect(), + notifyDisconnect: () => this.onDisconnect(), + cancelPendingRequests: () => this.cancelPendingRequests(), + }, + services: { + pingService: () => this.sendPing(), + }, + }), { logger: log.log.bind(log), }, @@ -151,6 +144,7 @@ export default class XtextWebSocketClient { | 'webSocket' | 'interpreter' | 'openListener' + | 'openWebSocket' | 'errorListener' | 'closeListener' | 'messageListener' @@ -160,6 +154,7 @@ export default class XtextWebSocketClient { webSocket: observable.ref, interpreter: false, openListener: false, + openWebSocket: false, errorListener: false, closeListener: false, messageListener: false, @@ -221,9 +216,7 @@ export default class XtextWebSocketClient { get networkMissing(): boolean { return ( this.state.matches('connection.temporarilyOffline') || - (this.disconnectedByUser && - this.state.matches('network.offline') && - !isWebSocketURLLocal(this.state.context.webSocketURL)) + (this.disconnectedByUser && this.state.matches('network.offline')) ); } @@ -275,17 +268,27 @@ export default class XtextWebSocketClient { this.interpreter.send(document.hidden ? 'TAB_HIDDEN' : 'TAB_VISIBLE'); } - private openWebSocket(webSocketURL: string | undefined): void { + private openWebSocket(): void { if (this.webSocket !== undefined) { throw new Error('WebSocket already open'); } - if (webSocketURL === undefined) { - throw new Error('URL not configured'); - } - log.debug('Creating WebSocket'); + (async () => { + const { webSocketURL } = await fetchBackendConfig(); + this.openWebSocketWithURL(webSocketURL); + })().catch((error) => { + log.error('Error while initializing connection', error); + const message = error instanceof Error ? error.message : String(error); + this.interpreter.send({ + type: 'ERROR', + message, + }); + }); + } + + private openWebSocketWithURL(webSocketURL: string): void { this.webSocket = new WebSocket(webSocketURL, XTEXT_SUBPROTOCOL_V1); this.webSocket.addEventListener('open', this.openListener); this.webSocket.addEventListener('close', this.closeListener); @@ -295,7 +298,9 @@ export default class XtextWebSocketClient { private closeWebSocket() { if (this.webSocket === undefined) { - throw new Error('WebSocket already closed'); + // We might get here when there is a network error before the socket is initialized + // and we don't have to do anything to close it. + return; } 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 @@ +/* eslint-disable @typescript-eslint/no-redeclare -- Declare types with their companion objects */ + +import { z } from 'zod'; + +export const BackendConfig = z.object({ + webSocketURL: z.string().url(), +}); + +export type BackendConfig = z.infer; + +export default async function fetchBackendConfig(): Promise { + const configURL = `${import.meta.env.BASE_URL}config.json`; + const response = await fetch(configURL); + const rawConfig = (await response.json()) as unknown; + return BackendConfig.parse(rawConfig); +} 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; const ERROR_WAIT_TIMES = ['200', '1s', '5s', '30s'].map(ms); export interface WebSocketContext { - webSocketURL: string | undefined; errors: string[]; } export type WebSocketEvent = - | { type: 'CONFIGURE'; webSocketURL: string } | { type: 'CONNECT' } | { type: 'DISCONNECT' } | { type: 'OPENED' } @@ -25,24 +23,6 @@ export type WebSocketEvent = | { type: 'OFFLINE' } | { type: 'ERROR'; message: string }; -export function isWebSocketURLLocal(webSocketURL: string | undefined): boolean { - if (webSocketURL === undefined) { - return false; - } - let hostname: string; - try { - ({ hostname } = new URL(webSocketURL)); - } catch { - return false; - } - // https://stackoverflow.com/a/57949518 - return ( - hostname === 'localhost' || - hostname === '[::1]' || - hostname.match(/^127(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/) !== null - ); -} - export default createMachine( { id: 'webSocket', @@ -53,7 +33,6 @@ export default createMachine( }, tsTypes: {} as import('./webSocketMachine.typegen').Typegen0, context: { - webSocketURL: undefined, errors: [], }, type: 'parallel', @@ -65,16 +44,12 @@ export default createMachine( disconnected: { id: 'disconnected', entry: ['clearErrors', 'notifyDisconnect'], - on: { - CONFIGURE: { actions: 'configure' }, - }, }, timedOut: { id: 'timedOut', always: [ { target: 'temporarilyOffline', - cond: 'needsNetwork', in: '#offline', }, { target: 'socketCreated', in: '#tabVisible' }, @@ -89,7 +64,6 @@ export default createMachine( always: [ { target: 'temporarilyOffline', - cond: 'needsNetwork', in: '#offline', }, ], @@ -183,7 +157,7 @@ export default createMachine( }, }, on: { - CONNECT: { target: '.timedOut', cond: 'hasWebSocketURL' }, + CONNECT: '.timedOut', DISCONNECT: '.disconnected', }, }, @@ -224,10 +198,6 @@ export default createMachine( }, }, { - guards: { - hasWebSocketURL: ({ webSocketURL }) => webSocketURL !== undefined, - needsNetwork: ({ webSocketURL }) => !isWebSocketURLLocal(webSocketURL), - }, delays: { IDLE_TIMEOUT: ms('5m'), OPEN_TIMEOUT: ms('10s'), @@ -239,10 +209,6 @@ export default createMachine( }, }, actions: { - configure: assign((context, { webSocketURL }) => ({ - ...context, - webSocketURL, - })), pushError: assign((context, { message }) => ({ ...context, errors: [...context.errors, message], -- cgit v1.2.3-54-g00ecf