aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js/xtext/UpdateService.ts
diff options
context:
space:
mode:
Diffstat (limited to 'language-web/src/main/js/xtext/UpdateService.ts')
-rw-r--r--language-web/src/main/js/xtext/UpdateService.ts116
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.