diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-10-31 15:02:16 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-10-31 19:26:15 +0100 |
commit | 36a2f8a6e0c19f728ddd8e88ccd45fa2a7aea283 (patch) | |
tree | 6f27bf231184646287bb502e0533f7f16e2db026 /language-web/src/main/js/xtext/UpdateService.ts | |
parent | fix(web): undo/redo button accessibility issue (diff) | |
download | refinery-36a2f8a6e0c19f728ddd8e88ccd45fa2a7aea283.tar.gz refinery-36a2f8a6e0c19f728ddd8e88ccd45fa2a7aea283.tar.zst refinery-36a2f8a6e0c19f728ddd8e88ccd45fa2a7aea283.zip |
chore(web): refactor xtext client
Diffstat (limited to 'language-web/src/main/js/xtext/UpdateService.ts')
-rw-r--r-- | language-web/src/main/js/xtext/UpdateService.ts | 116 |
1 files changed, 88 insertions, 28 deletions
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. |