From d510b07aededd59443e877c4e7c7b6e2b9822dfe Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Fri, 26 Aug 2022 17:19:36 +0200 Subject: refactor(frontend): custom mutex implementation Lets us track priorities of tasks without cancellation. --- subprojects/frontend/src/xtext/UpdateService.ts | 23 ++++++----- .../frontend/src/xtext/UpdateStateTracker.ts | 48 +++++++--------------- 2 files changed, 28 insertions(+), 43 deletions(-) (limited to 'subprojects/frontend/src/xtext') diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts index d8782d90..f1abce52 100644 --- a/subprojects/frontend/src/xtext/UpdateService.ts +++ b/subprojects/frontend/src/xtext/UpdateService.ts @@ -1,9 +1,10 @@ import type { ChangeDesc, Transaction } from '@codemirror/state'; -import { E_CANCELED, E_TIMEOUT } from 'async-mutex'; import { debounce } from 'lodash-es'; import { nanoid } from 'nanoid'; import type EditorStore from '../editor/EditorStore'; +import CancelledError from '../utils/CancelledError'; +import TimeoutError from '../utils/TimeoutError'; import getLogger from '../utils/getLogger'; import UpdateStateTracker from './UpdateStateTracker'; @@ -66,7 +67,7 @@ export default class UpdateService { this.updateFullTextOrThrow().catch((error) => { // Let E_TIMEOUT errors propagate, since if the first update times out, // we can't use the connection. - if (error === E_CANCELED) { + if (error instanceof CancelledError) { // Content assist will perform a full-text update anyways. log.debug('Full text update cancelled'); return; @@ -87,7 +88,7 @@ export default class UpdateService { } if (!this.tracker.lockedForUpdate) { this.updateOrThrow().catch((error) => { - if (error === E_CANCELED || error === E_TIMEOUT) { + if (error instanceof CancelledError || error instanceof TimeoutError) { log.debug('Idle update cancelled'); return; } @@ -163,11 +164,15 @@ export default class UpdateService { return this.fetchContentAssistFetchOnly(params, this.xtextStateId); } try { - return await this.tracker.runExclusiveHighPriority(() => - this.fetchContentAssistExclusive(params, signal), + return await this.tracker.runExclusive( + () => this.fetchContentAssistExclusive(params, signal), + true, ); } catch (error) { - if ((error === E_CANCELED || error === E_TIMEOUT) && signal.aborted) { + if ( + (error instanceof CancelledError || error instanceof TimeoutError) && + signal.aborted + ) { return []; } throw error; @@ -261,9 +266,7 @@ export default class UpdateService { } formatText(): Promise { - return this.tracker.runExclusiveWithRetries(() => - this.formatTextExclusive(), - ); + return this.tracker.runExclusive(() => this.formatTextExclusive()); } private async formatTextExclusive(): Promise { @@ -294,7 +297,7 @@ export default class UpdateService { try { await this.updateOrThrow(); } catch (error) { - if (error === E_CANCELED || error === E_TIMEOUT) { + if (error instanceof CancelledError || error instanceof TimeoutError) { return { cancelled: true }; } throw error; diff --git a/subprojects/frontend/src/xtext/UpdateStateTracker.ts b/subprojects/frontend/src/xtext/UpdateStateTracker.ts index a529f9a0..5d4ce49e 100644 --- a/subprojects/frontend/src/xtext/UpdateStateTracker.ts +++ b/subprojects/frontend/src/xtext/UpdateStateTracker.ts @@ -5,9 +5,9 @@ import { StateEffect, type Transaction, } from '@codemirror/state'; -import { E_CANCELED, Mutex, withTimeout } from 'async-mutex'; import type EditorStore from '../editor/EditorStore'; +import PriorityMutex from '../utils/PriorityMutex'; const WAIT_FOR_UPDATE_TIMEOUT_MS = 1000; @@ -31,7 +31,7 @@ export interface Delta { } export default class UpdateStateTracker { - xtextStateId: string | undefined; + private _xtextStateId: string | undefined; /** * The changes marked for synchronization to the server if a full or delta text update @@ -54,12 +54,16 @@ export default class UpdateStateTracker { /** * Locked when we try to modify the state on the server. */ - private readonly mutex = withTimeout(new Mutex(), WAIT_FOR_UPDATE_TIMEOUT_MS); + private readonly mutex = new PriorityMutex(WAIT_FOR_UPDATE_TIMEOUT_MS); constructor(private readonly store: EditorStore) { this.dirtyChanges = this.newEmptyChangeSet(); } + get xtextStateId(): string | undefined { + return this._xtextStateId; + } + private get hasDirtyChanges(): boolean { return !this.dirtyChanges.empty; } @@ -69,7 +73,7 @@ export default class UpdateStateTracker { } get lockedForUpdate(): boolean { - return this.mutex.isLocked(); + return this.mutex.locked; } get hasPendingChanges(): boolean { @@ -111,7 +115,7 @@ export default class UpdateStateTracker { } invalidateStateId(): void { - this.xtextStateId = undefined; + this._xtextStateId = undefined; } /** @@ -180,7 +184,7 @@ export default class UpdateStateTracker { if (remoteChanges !== undefined) { this.applyRemoteChangesExclusive(remoteChanges); } - this.xtextStateId = newStateId; + this._xtextStateId = newStateId; this.pendingChanges = undefined; } @@ -205,7 +209,10 @@ export default class UpdateStateTracker { } } - runExclusive(callback: () => Promise): Promise { + runExclusive( + callback: () => Promise, + highPriority = false, + ): Promise { return this.mutex.runExclusive(async () => { try { return await callback(); @@ -215,31 +222,6 @@ export default class UpdateStateTracker { this.pendingChanges = undefined; } } - }); - } - - runExclusiveHighPriority(callback: () => Promise): Promise { - this.mutex.cancel(); - return this.runExclusive(callback); - } - - async runExclusiveWithRetries( - callback: () => Promise, - maxRetries = 5, - ): Promise { - let retries = 0; - while (retries < maxRetries) { - try { - // eslint-disable-next-line no-await-in-loop -- Use a loop for sequential retries. - return await this.runExclusive(callback); - } catch (error) { - // Let timeout errors propagate to give up retrying on a flaky connection. - if (error !== E_CANCELED) { - throw error; - } - retries += 1; - } - } - throw E_CANCELED; + }, highPriority); } } -- cgit v1.2.3-54-g00ecf