aboutsummaryrefslogtreecommitdiffstats
path: root/language-web
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-30 17:08:11 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-31 19:26:14 +0100
commit80b99f1f2ae86f0ce9652266daa22040b26a5894 (patch)
tree50cd24b2cfb91606cfc5c6bdd5209fabc93cd5b5 /language-web
parentchore(web): move logger to utils/ (diff)
downloadrefinery-80b99f1f2ae86f0ce9652266daa22040b26a5894.tar.gz
refinery-80b99f1f2ae86f0ce9652266daa22040b26a5894.tar.zst
refinery-80b99f1f2ae86f0ce9652266daa22040b26a5894.zip
chore(web): refactor UpdateService
extract utils/ConditionVariable from xtext/UpdateService as a generally useful utility class for waiting for conditions
Diffstat (limited to 'language-web')
-rw-r--r--language-web/src/main/js/utils/ConditionVariable.ts64
-rw-r--r--language-web/src/main/js/xtext/ContentAssistService.ts10
-rw-r--r--language-web/src/main/js/xtext/UpdateService.ts52
3 files changed, 82 insertions, 44 deletions
diff --git a/language-web/src/main/js/utils/ConditionVariable.ts b/language-web/src/main/js/utils/ConditionVariable.ts
new file mode 100644
index 00000000..0910dfa6
--- /dev/null
+++ b/language-web/src/main/js/utils/ConditionVariable.ts
@@ -0,0 +1,64 @@
1import { getLogger } from './logger';
2import { PendingTask } from './PendingTask';
3
4const log = getLogger('utils.ConditionVariable');
5
6export type Condition = () => boolean;
7
8export class ConditionVariable {
9 condition: Condition;
10
11 defaultTimeout: number;
12
13 listeners: PendingTask<void>[] = [];
14
15 constructor(condition: Condition, defaultTimeout = 0) {
16 this.condition = condition;
17 this.defaultTimeout = defaultTimeout;
18 }
19
20 async waitFor(timeoutMs: number | null = null): Promise<void> {
21 if (this.condition()) {
22 return;
23 }
24 const timeoutOrDefault = timeoutMs || this.defaultTimeout;
25 let nowMs = Date.now();
26 const endMs = nowMs + timeoutOrDefault;
27 while (!this.condition() && nowMs < endMs) {
28 const remainingMs = endMs - nowMs;
29 const promise = new Promise<void>((resolve, reject) => {
30 if (this.condition()) {
31 resolve();
32 return;
33 }
34 const task = new PendingTask(resolve, reject, remainingMs);
35 this.listeners.push(task);
36 });
37 // We must keep waiting until the update has completed,
38 // so the tasks can't be started in parallel.
39 // eslint-disable-next-line no-await-in-loop
40 await promise;
41 nowMs = Date.now();
42 }
43 if (!this.condition()) {
44 log.error('Condition still does not hold after', timeoutOrDefault, 'ms');
45 throw new Error('Failed to wait for condition');
46 }
47 }
48
49 notifyAll(): void {
50 this.clearListenersWith((listener) => listener.resolve());
51 }
52
53 rejectAll(error: unknown): void {
54 this.clearListenersWith((listener) => listener.reject(error));
55 }
56
57 private clearListenersWith(callback: (listener: PendingTask<void>) => void) {
58 // Copy `listeners` so that we don't get into a race condition
59 // if one of the listeners adds another listener.
60 const { listeners } = this;
61 this.listeners = [];
62 listeners.forEach(callback);
63 }
64}
diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts
index 9cbb385f..ec6b80d2 100644
--- a/language-web/src/main/js/xtext/ContentAssistService.ts
+++ b/language-web/src/main/js/xtext/ContentAssistService.ts
@@ -3,7 +3,7 @@ import type {
3 CompletionContext, 3 CompletionContext,
4 CompletionResult, 4 CompletionResult,
5} from '@codemirror/autocomplete'; 5} from '@codemirror/autocomplete';
6import type { ChangeSet, Transaction } from '@codemirror/state'; 6import type { Transaction } from '@codemirror/state';
7import escapeStringRegexp from 'escape-string-regexp'; 7import escapeStringRegexp from 'escape-string-regexp';
8 8
9import type { UpdateService } from './UpdateService'; 9import type { UpdateService } from './UpdateService';
@@ -62,7 +62,7 @@ export class ContentAssistService {
62 } 62 }
63 63
64 onTransaction(transaction: Transaction): void { 64 onTransaction(transaction: Transaction): void {
65 if (this.shouldInvalidateCachedCompletion(transaction.changes)) { 65 if (this.shouldInvalidateCachedCompletion(transaction)) {
66 this.lastCompletion = null; 66 this.lastCompletion = null;
67 } 67 }
68 } 68 }
@@ -142,8 +142,8 @@ export class ContentAssistService {
142 return from >= transformedFrom && to <= transformedTo && span && span.exec(text); 142 return from >= transformedFrom && to <= transformedTo && span && span.exec(text);
143 } 143 }
144 144
145 private shouldInvalidateCachedCompletion(changes: ChangeSet) { 145 private shouldInvalidateCachedCompletion(transaction: Transaction) {
146 if (changes.empty || this.lastCompletion === null) { 146 if (!transaction.docChanged || this.lastCompletion === null) {
147 return false; 147 return false;
148 } 148 }
149 const { from: lastFrom, to: lastTo } = this.lastCompletion; 149 const { from: lastFrom, to: lastTo } = this.lastCompletion;
@@ -152,7 +152,7 @@ export class ContentAssistService {
152 } 152 }
153 const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); 153 const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo);
154 let invalidate = false; 154 let invalidate = false;
155 changes.iterChangedRanges((fromA, toA) => { 155 transaction.changes.iterChangedRanges((fromA, toA) => {
156 if (fromA < transformedFrom || toA > transformedTo) { 156 if (fromA < transformedFrom || toA > transformedTo) {
157 invalidate = true; 157 invalidate = true;
158 } 158 }
diff --git a/language-web/src/main/js/xtext/UpdateService.ts b/language-web/src/main/js/xtext/UpdateService.ts
index 3ab1daf9..838f9d5b 100644
--- a/language-web/src/main/js/xtext/UpdateService.ts
+++ b/language-web/src/main/js/xtext/UpdateService.ts
@@ -7,8 +7,8 @@ import { nanoid } from 'nanoid';
7 7
8import type { EditorStore } from '../editor/EditorStore'; 8import type { EditorStore } from '../editor/EditorStore';
9import type { XtextWebSocketClient } from './XtextWebSocketClient'; 9import type { XtextWebSocketClient } from './XtextWebSocketClient';
10import { ConditionVariable } from '../utils/ConditionVariable';
10import { getLogger } from '../utils/logger'; 11import { getLogger } from '../utils/logger';
11import { PendingTask } from '../utils/PendingTask';
12import { Timer } from '../utils/Timer'; 12import { Timer } from '../utils/Timer';
13import { 13import {
14 IContentAssistEntry, 14 IContentAssistEntry,
@@ -40,7 +40,10 @@ export class UpdateService {
40 40
41 private webSocketClient: XtextWebSocketClient; 41 private webSocketClient: XtextWebSocketClient;
42 42
43 private updateListeners: PendingTask<void>[] = []; 43 private updatedCondition = new ConditionVariable(
44 () => this.pendingUpdate === null && this.xtextStateId !== null,
45 WAIT_FOR_UPDATE_TIMEOUT_MS,
46 );
44 47
45 private idleUpdateTimer = new Timer(() => { 48 private idleUpdateTimer = new Timer(() => {
46 this.handleIdleUpdate(); 49 this.handleIdleUpdate();
@@ -59,9 +62,8 @@ export class UpdateService {
59 } 62 }
60 63
61 onTransaction(transaction: Transaction): void { 64 onTransaction(transaction: Transaction): void {
62 const { changes } = transaction; 65 if (transaction.docChanged) {
63 if (!changes.empty) { 66 this.dirtyChanges = this.dirtyChanges.composeDesc(transaction.changes.desc);
64 this.dirtyChanges = this.dirtyChanges.composeDesc(changes.desc);
65 this.idleUpdateTimer.reschedule(); 67 this.idleUpdateTimer.reschedule();
66 } 68 }
67 } 69 }
@@ -221,13 +223,7 @@ export class UpdateService {
221 [newStateId, result] = await callback(); 223 [newStateId, result] = await callback();
222 this.xtextStateId = newStateId; 224 this.xtextStateId = newStateId;
223 this.pendingUpdate = null; 225 this.pendingUpdate = null;
224 // Copy `updateListeners` so that we don't get into a race condition 226 this.updatedCondition.notifyAll();
225 // if one of the listeners adds another listener.
226 const listeners = this.updateListeners;
227 this.updateListeners = [];
228 listeners.forEach((listener) => {
229 listener.resolve();
230 });
231 return result; 227 return result;
232 } catch (e) { 228 } catch (e) {
233 log.error('Error while update', e); 229 log.error('Error while update', e);
@@ -238,39 +234,17 @@ export class UpdateService {
238 } 234 }
239 this.pendingUpdate = null; 235 this.pendingUpdate = null;
240 this.webSocketClient.forceReconnectOnError(); 236 this.webSocketClient.forceReconnectOnError();
241 const listeners = this.updateListeners; 237 this.updatedCondition.rejectAll(e);
242 this.updateListeners = [];
243 listeners.forEach((listener) => {
244 listener.reject(e);
245 });
246 throw e; 238 throw e;
247 } 239 }
248 } 240 }
249 241
250 private async prepareForDeltaUpdate() { 242 private async prepareForDeltaUpdate() {
251 if (this.pendingUpdate === null) { 243 // If no update is pending, but the full text hasn't been uploaded to the server yet,
252 if (this.xtextStateId === null) { 244 // we must start a full text upload.
253 return; 245 if (this.pendingUpdate === null && this.xtextStateId === null) {
254 }
255 await this.updateFullText(); 246 await this.updateFullText();
256 } 247 }
257 let nowMs = Date.now(); 248 await this.updatedCondition.waitFor();
258 const endMs = nowMs + WAIT_FOR_UPDATE_TIMEOUT_MS;
259 while (this.pendingUpdate !== null && nowMs < endMs) {
260 const timeoutMs = endMs - nowMs;
261 const promise = new Promise((resolve, reject) => {
262 const task = new PendingTask(resolve, reject, timeoutMs);
263 this.updateListeners.push(task);
264 });
265 // We must keep waiting uptil the update has completed,
266 // so the tasks can't be started in parallel.
267 // eslint-disable-next-line no-await-in-loop
268 await promise;
269 nowMs = Date.now();
270 }
271 if (this.pendingUpdate !== null || this.xtextStateId === null) {
272 log.error('No successful update in', WAIT_FOR_UPDATE_TIMEOUT_MS, 'ms');
273 throw new Error('Failed to wait for successful update');
274 }
275 } 249 }
276} 250}