aboutsummaryrefslogtreecommitdiffstats
path: root/language-web
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-31 15:02:16 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-31 19:26:15 +0100
commit36a2f8a6e0c19f728ddd8e88ccd45fa2a7aea283 (patch)
tree6f27bf231184646287bb502e0533f7f16e2db026 /language-web
parentfix(web): undo/redo button accessibility issue (diff)
downloadrefinery-36a2f8a6e0c19f728ddd8e88ccd45fa2a7aea283.tar.gz
refinery-36a2f8a6e0c19f728ddd8e88ccd45fa2a7aea283.tar.zst
refinery-36a2f8a6e0c19f728ddd8e88ccd45fa2a7aea283.zip
chore(web): refactor xtext client
Diffstat (limited to 'language-web')
-rw-r--r--language-web/src/main/js/editor/EditorStore.ts11
-rw-r--r--language-web/src/main/js/xtext/ContentAssistService.ts4
-rw-r--r--language-web/src/main/js/xtext/HighlightingService.ts4
-rw-r--r--language-web/src/main/js/xtext/OccurrencesService.ts10
-rw-r--r--language-web/src/main/js/xtext/UpdateService.ts116
-rw-r--r--language-web/src/main/js/xtext/ValidationService.ts4
-rw-r--r--language-web/src/main/js/xtext/XtextClient.ts22
-rw-r--r--language-web/src/main/js/xtext/XtextWebSocketClient.ts34
8 files changed, 130 insertions, 75 deletions
diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts
index 8788e00f..ba31efcb 100644
--- a/language-web/src/main/js/editor/EditorStore.ts
+++ b/language-web/src/main/js/editor/EditorStore.ts
@@ -56,11 +56,11 @@ import { XtextClient } from '../xtext/XtextClient';
56const log = getLogger('editor.EditorStore'); 56const log = getLogger('editor.EditorStore');
57 57
58export class EditorStore { 58export class EditorStore {
59 themeStore; 59 private readonly themeStore;
60 60
61 state: EditorState; 61 state: EditorState;
62 62
63 client: XtextClient; 63 private readonly client: XtextClient;
64 64
65 showLineNumbers = false; 65 showLineNumbers = false;
66 66
@@ -74,11 +74,11 @@ export class EditorStore {
74 74
75 infoCount = 0; 75 infoCount = 0;
76 76
77 readonly defaultDispatcher = (tr: Transaction): void => { 77 private readonly defaultDispatcher = (tr: Transaction): void => {
78 this.onTransaction(tr); 78 this.onTransaction(tr);
79 }; 79 };
80 80
81 dispatcher = this.defaultDispatcher; 81 private dispatcher = this.defaultDispatcher;
82 82
83 constructor(initialValue: string, themeStore: ThemeStore) { 83 constructor(initialValue: string, themeStore: ThemeStore) {
84 this.themeStore = themeStore; 84 this.themeStore = themeStore;
@@ -148,10 +148,7 @@ export class EditorStore {
148 }, 148 },
149 ); 149 );
150 makeAutoObservable(this, { 150 makeAutoObservable(this, {
151 themeStore: false,
152 state: observable.ref, 151 state: observable.ref,
153 defaultDispatcher: false,
154 dispatcher: false,
155 }); 152 });
156 } 153 }
157 154
diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts
index 8461ec7f..f085c5b1 100644
--- a/language-web/src/main/js/xtext/ContentAssistService.ts
+++ b/language-web/src/main/js/xtext/ContentAssistService.ts
@@ -61,9 +61,9 @@ function computeSpan(prefix: string, entryCount: number) {
61} 61}
62 62
63export class ContentAssistService { 63export class ContentAssistService {
64 updateService: UpdateService; 64 private readonly updateService: UpdateService;
65 65
66 lastCompletion: CompletionResult | null = null; 66 private lastCompletion: CompletionResult | null = null;
67 67
68 constructor(updateService: UpdateService) { 68 constructor(updateService: UpdateService) {
69 this.updateService = updateService; 69 this.updateService = updateService;
diff --git a/language-web/src/main/js/xtext/HighlightingService.ts b/language-web/src/main/js/xtext/HighlightingService.ts
index 451a3a52..fc3e9e53 100644
--- a/language-web/src/main/js/xtext/HighlightingService.ts
+++ b/language-web/src/main/js/xtext/HighlightingService.ts
@@ -7,9 +7,9 @@ import { isHighlightingResult } from './xtextServiceResults';
7const log = getLogger('xtext.ValidationService'); 7const log = getLogger('xtext.ValidationService');
8 8
9export class HighlightingService { 9export class HighlightingService {
10 private store: EditorStore; 10 private readonly store: EditorStore;
11 11
12 private updateService: UpdateService; 12 private readonly updateService: UpdateService;
13 13
14 constructor(store: EditorStore, updateService: UpdateService) { 14 constructor(store: EditorStore, updateService: UpdateService) {
15 this.store = store; 15 this.store = store;
diff --git a/language-web/src/main/js/xtext/OccurrencesService.ts b/language-web/src/main/js/xtext/OccurrencesService.ts
index 804f5ba2..d1dec9e9 100644
--- a/language-web/src/main/js/xtext/OccurrencesService.ts
+++ b/language-web/src/main/js/xtext/OccurrencesService.ts
@@ -34,19 +34,19 @@ function transformOccurrences(regions: ITextRegion[]): IOccurrence[] {
34} 34}
35 35
36export class OccurrencesService { 36export class OccurrencesService {
37 private store: EditorStore; 37 private readonly store: EditorStore;
38 38
39 private webSocketClient: XtextWebSocketClient; 39 private readonly webSocketClient: XtextWebSocketClient;
40 40
41 private updateService: UpdateService; 41 private readonly updateService: UpdateService;
42 42
43 private hasOccurrences = false; 43 private hasOccurrences = false;
44 44
45 private findOccurrencesTimer = new Timer(() => { 45 private readonly findOccurrencesTimer = new Timer(() => {
46 this.handleFindOccurrences(); 46 this.handleFindOccurrences();
47 }, FIND_OCCURRENCES_TIMEOUT_MS); 47 }, FIND_OCCURRENCES_TIMEOUT_MS);
48 48
49 private clearOccurrencesTimer = new Timer(() => { 49 private readonly clearOccurrencesTimer = new Timer(() => {
50 this.clearOccurrences(); 50 this.clearOccurrences();
51 }, CLEAR_OCCURRENCES_TIMEOUT_MS); 51 }, CLEAR_OCCURRENCES_TIMEOUT_MS);
52 52
diff --git a/language-web/src/main/js/xtext/UpdateService.ts b/language-web/src/main/js/xtext/UpdateService.ts
index 838f9d5b..9b672e79 100644
--- a/language-web/src/main/js/xtext/UpdateService.ts
+++ b/language-web/src/main/js/xtext/UpdateService.ts
@@ -32,20 +32,27 @@ export class UpdateService {
32 32
33 xtextStateId: string | null = null; 33 xtextStateId: string | null = null;
34 34
35 private store: EditorStore; 35 private readonly store: EditorStore;
36 36
37 /**
38 * The changes being synchronized to the server if a full or delta text update is running,
39 * `null` otherwise.
40 */
37 private pendingUpdate: ChangeDesc | null = null; 41 private pendingUpdate: ChangeDesc | null = null;
38 42
43 /**
44 * Local changes not yet sychronized to the server and not part of the running update, if any.
45 */
39 private dirtyChanges: ChangeDesc; 46 private dirtyChanges: ChangeDesc;
40 47
41 private webSocketClient: XtextWebSocketClient; 48 private readonly webSocketClient: XtextWebSocketClient;
42 49
43 private updatedCondition = new ConditionVariable( 50 private readonly updatedCondition = new ConditionVariable(
44 () => this.pendingUpdate === null && this.xtextStateId !== null, 51 () => this.pendingUpdate === null && this.xtextStateId !== null,
45 WAIT_FOR_UPDATE_TIMEOUT_MS, 52 WAIT_FOR_UPDATE_TIMEOUT_MS,
46 ); 53 );
47 54
48 private idleUpdateTimer = new Timer(() => { 55 private readonly idleUpdateTimer = new Timer(() => {
49 this.handleIdleUpdate(); 56 this.handleIdleUpdate();
50 }, UPDATE_TIMEOUT_MS); 57 }, UPDATE_TIMEOUT_MS);
51 58
@@ -56,9 +63,11 @@ export class UpdateService {
56 this.webSocketClient = webSocketClient; 63 this.webSocketClient = webSocketClient;
57 } 64 }
58 65
59 onConnect(): Promise<void> { 66 onReconnect(): void {
60 this.xtextStateId = null; 67 this.xtextStateId = null;
61 return this.updateFullText(); 68 this.updateFullText().catch((error) => {
69 log.error('Unexpected error during initial update', error);
70 });
62 } 71 }
63 72
64 onTransaction(transaction: Transaction): void { 73 onTransaction(transaction: Transaction): void {
@@ -68,6 +77,14 @@ export class UpdateService {
68 } 77 }
69 } 78 }
70 79
80 /**
81 * Computes the summary of any changes happened since the last complete update.
82 *
83 * The result reflects any changes that happened since the `xtextStateId`
84 * version was uploaded to the server.
85 *
86 * @return the summary of changes since the last update
87 */
71 computeChangesSinceLastUpdate(): ChangeDesc { 88 computeChangesSinceLastUpdate(): ChangeDesc {
72 return this.pendingUpdate?.composeDesc(this.dirtyChanges) || this.dirtyChanges; 89 return this.pendingUpdate?.composeDesc(this.dirtyChanges) || this.dirtyChanges;
73 } 90 }
@@ -106,6 +123,15 @@ export class UpdateService {
106 throw new Error('Full text update failed'); 123 throw new Error('Full text update failed');
107 } 124 }
108 125
126 /**
127 * Makes sure that the document state on the server reflects recent
128 * local changes.
129 *
130 * Performs either an update with delta text or a full text update if needed.
131 * If there are not local dirty changes, the promise resolves immediately.
132 *
133 * @return a promise resolving when the update is completed
134 */
109 async update(): Promise<void> { 135 async update(): Promise<void> {
110 await this.prepareForDeltaUpdate(); 136 await this.prepareForDeltaUpdate();
111 const delta = this.computeDelta(); 137 const delta = this.computeDelta();
@@ -151,31 +177,36 @@ export class UpdateService {
151 return []; 177 return [];
152 } 178 }
153 const delta = this.computeDelta(); 179 const delta = this.computeDelta();
154 if (delta === null) { 180 if (delta !== null) {
155 // Poscondition of `prepareForDeltaUpdate`: `xtextStateId !== null` 181 log.trace('Editor delta', delta);
156 return this.doFetchContentAssist(params, this.xtextStateId as string); 182 const entries = await this.withUpdate(async () => {
157 } 183 const result = await this.webSocketClient.send({
158 log.trace('Editor delta', delta); 184 ...params,
159 return this.withUpdate(async () => { 185 requiredStateId: this.xtextStateId,
160 const result = await this.webSocketClient.send({ 186 ...delta,
161 ...params, 187 });
162 requiredStateId: this.xtextStateId, 188 if (isContentAssistResult(result)) {
163 ...delta, 189 return [result.stateId, result.entries];
190 }
191 if (isInvalidStateIdConflictResult(result)) {
192 const [newStateId] = await this.doFallbackToUpdateFullText();
193 // We must finish this state update transaction to prepare for any push events
194 // before querying for content assist, so we just return `null` and will query
195 // the content assist service later.
196 return [newStateId, null];
197 }
198 log.error('Unextpected content assist result with delta update', result);
199 throw new Error('Unexpexted content assist result with delta update');
164 }); 200 });
165 if (isContentAssistResult(result)) { 201 if (entries !== null) {
166 return [result.stateId, result.entries]; 202 return entries;
167 } 203 }
168 if (isInvalidStateIdConflictResult(result)) { 204 if (signal.aborted) {
169 const [newStateId] = await this.doFallbackToUpdateFullText(); 205 return [];
170 if (signal.aborted) {
171 return [newStateId, []];
172 }
173 const entries = await this.doFetchContentAssist(params, newStateId);
174 return [newStateId, entries];
175 } 206 }
176 log.error('Unextpected content assist result with delta update', result); 207 }
177 throw new Error('Unexpexted content assist result with delta update'); 208 // Poscondition of `prepareForDeltaUpdate`: `xtextStateId !== null`
178 }); 209 return this.doFetchContentAssist(params, this.xtextStateId as string);
179 } 210 }
180 211
181 private async doFetchContentAssist(params: Record<string, unknown>, expectedStateId: string) { 212 private async doFetchContentAssist(params: Record<string, unknown>, expectedStateId: string) {
@@ -211,6 +242,27 @@ export class UpdateService {
211 }; 242 };
212 } 243 }
213 244
245 /**
246 * Executes an asynchronous callback that updates the state on the server.
247 *
248 * Ensures that updates happen sequentially and manages `pendingUpdate`
249 * and `dirtyChanges` to reflect changes being synchronized to the server
250 * and not yet synchronized to the server, respectively.
251 *
252 * Optionally, `callback` may return a second value that is retured by this function.
253 *
254 * Once the remote procedure call to update the server state finishes
255 * and returns the new `stateId`, `callback` must return _immediately_
256 * to ensure that the local `stateId` is updated likewise to be able to handle
257 * push messages referring to the new `stateId` from the server.
258 * If additional work is needed to compute the second value in some cases,
259 * use `T | null` instead of `T` as a return type and signal the need for additional
260 * computations by returning `null`. Thus additional computations can be performed
261 * outside of the critical section.
262 *
263 * @param callback the asynchronous callback that updates the server state
264 * @return a promise resolving to the second value returned by `callback`
265 */
214 private async withUpdate<T>(callback: () => Promise<[string, T]>): Promise<T> { 266 private async withUpdate<T>(callback: () => Promise<[string, T]>): Promise<T> {
215 if (this.pendingUpdate !== null) { 267 if (this.pendingUpdate !== null) {
216 throw new Error('Another update is pending, will not perform update'); 268 throw new Error('Another update is pending, will not perform update');
@@ -239,6 +291,14 @@ export class UpdateService {
239 } 291 }
240 } 292 }
241 293
294 /**
295 * Ensures that there is some state available on the server (`xtextStateId`)
296 * and that there is not pending update.
297 *
298 * After this function resolves, a delta text update is possible.
299 *
300 * @return a promise resolving when there is a valid state id but no pending update
301 */
242 private async prepareForDeltaUpdate() { 302 private async prepareForDeltaUpdate() {
243 // If no update is pending, but the full text hasn't been uploaded to the server yet, 303 // If no update is pending, but the full text hasn't been uploaded to the server yet,
244 // we must start a full text upload. 304 // we must start a full text upload.
diff --git a/language-web/src/main/js/xtext/ValidationService.ts b/language-web/src/main/js/xtext/ValidationService.ts
index 31c8f716..8e4934ac 100644
--- a/language-web/src/main/js/xtext/ValidationService.ts
+++ b/language-web/src/main/js/xtext/ValidationService.ts
@@ -8,9 +8,9 @@ import { isValidationResult } from './xtextServiceResults';
8const log = getLogger('xtext.ValidationService'); 8const log = getLogger('xtext.ValidationService');
9 9
10export class ValidationService { 10export class ValidationService {
11 private store: EditorStore; 11 private readonly store: EditorStore;
12 12
13 private updateService: UpdateService; 13 private readonly updateService: UpdateService;
14 14
15 constructor(store: EditorStore, updateService: UpdateService) { 15 constructor(store: EditorStore, updateService: UpdateService) {
16 this.store = store; 16 this.store = store;
diff --git a/language-web/src/main/js/xtext/XtextClient.ts b/language-web/src/main/js/xtext/XtextClient.ts
index 03b81b1c..28f3d0cc 100644
--- a/language-web/src/main/js/xtext/XtextClient.ts
+++ b/language-web/src/main/js/xtext/XtextClient.ts
@@ -16,21 +16,21 @@ import { XtextWebSocketClient } from './XtextWebSocketClient';
16const log = getLogger('xtext.XtextClient'); 16const log = getLogger('xtext.XtextClient');
17 17
18export class XtextClient { 18export class XtextClient {
19 private webSocketClient: XtextWebSocketClient; 19 private readonly webSocketClient: XtextWebSocketClient;
20 20
21 private updateService: UpdateService; 21 private readonly updateService: UpdateService;
22 22
23 private contentAssistService: ContentAssistService; 23 private readonly contentAssistService: ContentAssistService;
24 24
25 private highlightingService: HighlightingService; 25 private readonly highlightingService: HighlightingService;
26 26
27 private validationService: ValidationService; 27 private readonly validationService: ValidationService;
28 28
29 private occurrencesService: OccurrencesService; 29 private readonly occurrencesService: OccurrencesService;
30 30
31 constructor(store: EditorStore) { 31 constructor(store: EditorStore) {
32 this.webSocketClient = new XtextWebSocketClient( 32 this.webSocketClient = new XtextWebSocketClient(
33 () => this.updateService.onConnect(), 33 () => this.updateService.onReconnect(),
34 (resource, stateId, service, push) => this.onPush(resource, stateId, service, push), 34 (resource, stateId, service, push) => this.onPush(resource, stateId, service, push),
35 ); 35 );
36 this.updateService = new UpdateService(store, this.webSocketClient); 36 this.updateService = new UpdateService(store, this.webSocketClient);
@@ -52,15 +52,17 @@ export class XtextClient {
52 this.occurrencesService.onTransaction(transaction); 52 this.occurrencesService.onTransaction(transaction);
53 } 53 }
54 54
55 private async onPush(resource: string, stateId: string, service: string, push: unknown) { 55 private onPush(resource: string, stateId: string, service: string, push: unknown) {
56 const { resourceName, xtextStateId } = this.updateService; 56 const { resourceName, xtextStateId } = this.updateService;
57 if (resource !== resourceName) { 57 if (resource !== resourceName) {
58 log.error('Unknown resource name: expected:', resourceName, 'got:', resource); 58 log.error('Unknown resource name: expected:', resourceName, 'got:', resource);
59 return; 59 return;
60 } 60 }
61 if (stateId !== xtextStateId) { 61 if (stateId !== xtextStateId) {
62 log.error('Unexpected xtext state id: expected:', xtextStateId, 'got:', resource); 62 log.error('Unexpected xtext state id: expected:', xtextStateId, 'got:', stateId);
63 await this.updateService.updateFullText(); 63 // The current push message might be stale (referring to a previous state),
64 // so this is not neccessarily an error and there is no need to force-reconnect.
65 return;
64 } 66 }
65 switch (service) { 67 switch (service) {
66 case 'highlight': 68 case 'highlight':
diff --git a/language-web/src/main/js/xtext/XtextWebSocketClient.ts b/language-web/src/main/js/xtext/XtextWebSocketClient.ts
index 839d6518..488e4b3b 100644
--- a/language-web/src/main/js/xtext/XtextWebSocketClient.ts
+++ b/language-web/src/main/js/xtext/XtextWebSocketClient.ts
@@ -27,14 +27,14 @@ const REQUEST_TIMEOUT_MS = 1000;
27 27
28const log = getLogger('xtext.XtextWebSocketClient'); 28const log = getLogger('xtext.XtextWebSocketClient');
29 29
30type ReconnectHandler = () => Promise<void>; 30export type ReconnectHandler = () => void;
31 31
32type PushHandler = ( 32export type PushHandler = (
33 resourceId: string, 33 resourceId: string,
34 stateId: string, 34 stateId: string,
35 service: string, 35 service: string,
36 data: unknown, 36 data: unknown,
37) => Promise<void>; 37) => void;
38 38
39enum State { 39enum State {
40 Initial, 40 Initial,
@@ -47,29 +47,29 @@ enum State {
47} 47}
48 48
49export class XtextWebSocketClient { 49export class XtextWebSocketClient {
50 nextMessageId = 0; 50 private nextMessageId = 0;
51 51
52 connection!: WebSocket; 52 private connection!: WebSocket;
53 53
54 pendingRequests = new Map<string, PendingTask<unknown>>(); 54 private readonly pendingRequests = new Map<string, PendingTask<unknown>>();
55 55
56 onReconnect: ReconnectHandler; 56 private readonly onReconnect: ReconnectHandler;
57 57
58 onPush: PushHandler; 58 private readonly onPush: PushHandler;
59 59
60 state = State.Initial; 60 private state = State.Initial;
61 61
62 reconnectTryCount = 0; 62 private reconnectTryCount = 0;
63 63
64 idleTimer = new Timer(() => { 64 private readonly idleTimer = new Timer(() => {
65 this.handleIdleTimeout(); 65 this.handleIdleTimeout();
66 }, BACKGROUND_IDLE_TIMEOUT_MS); 66 }, BACKGROUND_IDLE_TIMEOUT_MS);
67 67
68 pingTimer = new Timer(() => { 68 private readonly pingTimer = new Timer(() => {
69 this.sendPing(); 69 this.sendPing();
70 }, PING_TIMEOUT_MS); 70 }, PING_TIMEOUT_MS);
71 71
72 reconnectTimer = new Timer(() => { 72 private readonly reconnectTimer = new Timer(() => {
73 this.handleReconnect(); 73 this.handleReconnect();
74 }); 74 });
75 75
@@ -115,9 +115,7 @@ export class XtextWebSocketClient {
115 this.nextMessageId = 0; 115 this.nextMessageId = 0;
116 this.reconnectTryCount = 0; 116 this.reconnectTryCount = 0;
117 this.pingTimer.schedule(); 117 this.pingTimer.schedule();
118 this.onReconnect().catch((error) => { 118 this.onReconnect();
119 log.error('Unexpected error in onReconnect handler', error);
120 });
121 }); 119 });
122 this.connection.addEventListener('error', (event) => { 120 this.connection.addEventListener('error', (event) => {
123 log.error('Unexpected websocket error', event); 121 log.error('Unexpected websocket error', event);
@@ -264,9 +262,7 @@ export class XtextWebSocketClient {
264 message.stateId, 262 message.stateId,
265 message.service, 263 message.service,
266 message.push, 264 message.push,
267 ).catch((error) => { 265 );
268 log.error('Unexpected error in onPush handler', error);
269 });
270 } else { 266 } else {
271 log.error('Unexpected websocket message', message); 267 log.error('Unexpected websocket message', message);
272 this.forceReconnectOnError(); 268 this.forceReconnectOnError();