From 542f976e75dff82304dd4e106ea4156c065d4f34 Mon Sep 17 00:00:00 2001 From: Aofei Sheng Date: Tue, 20 Aug 2024 16:26:03 +0800 Subject: [PATCH] fix(Project): prevent potential data corruption in `saveToCloud` and `saveToLocalCache` Fixes #761 --- spx-gui/src/models/project/history.ts | 2 +- spx-gui/src/models/project/index.ts | 6 ++++-- spx-gui/src/utils/disposable.ts | 16 ++++++++++++---- spx-gui/src/utils/exception.ts | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/spx-gui/src/models/project/history.ts b/spx-gui/src/models/project/history.ts index ce47f32b9..0402c759e 100644 --- a/spx-gui/src/models/project/history.ts +++ b/spx-gui/src/models/project/history.ts @@ -27,7 +27,7 @@ export type State = { } export class History { - private mutex = new Mutex() + readonly mutex = new Mutex() constructor( private project: Project, diff --git a/spx-gui/src/models/project/index.ts b/spx-gui/src/models/project/index.ts index 2d465c618..3751b7851 100644 --- a/spx-gui/src/models/project/index.ts +++ b/spx-gui/src/models/project/index.ts @@ -380,7 +380,8 @@ export class Project extends Disposable { /** Save to cloud */ async saveToCloud() { - const [metadata, files] = this.export() + const [metadata, files] = await this.history.mutex.runExclusive(this.export) + if (this.isDisposed) throw new Error('disposed') const saved = await cloudHelper.save(metadata, files) this.applyMetadata(saved.metadata) this.lastSyncedFilesHash = await hashFiles(files) @@ -396,7 +397,8 @@ export class Project extends Disposable { /** Save to local cache */ private async saveToLocalCache(key: string) { - const [metadata, files] = this.export() + const [metadata, files] = await this.history.mutex.runExclusive(this.export) + if (this.isDisposed) throw new Error('disposed') await localHelper.save(key, metadata, files) } diff --git a/spx-gui/src/utils/disposable.ts b/spx-gui/src/utils/disposable.ts index fcdddd464..48e14466b 100644 --- a/spx-gui/src/utils/disposable.ts +++ b/spx-gui/src/utils/disposable.ts @@ -6,15 +6,23 @@ export type Disposer = () => void export class Disposable { - _disposers: Disposer[] = [] + private disposers: Disposer[] = [] + + private _isDisposed = false + get isDisposed() { + return this._isDisposed + } addDisposer = (disposer: Disposer) => { - this._disposers.push(disposer) + if (this._isDisposed) throw new Error('disposed') + this.disposers.push(disposer) } dispose = () => { - while (this._disposers.length > 0) { - this._disposers.pop()?.() + if (this._isDisposed) return + this._isDisposed = true + while (this.disposers.length > 0) { + this.disposers.pop()?.() } } } diff --git a/spx-gui/src/utils/exception.ts b/spx-gui/src/utils/exception.ts index 0651d37a8..3d94bae70 100644 --- a/spx-gui/src/utils/exception.ts +++ b/spx-gui/src/utils/exception.ts @@ -30,7 +30,7 @@ export class DefaultException extends Exception { } /** - * Cancelled is a special exception, it stands for a "cancel operation" because of user ineraction. + * Cancelled is a special exception, it stands for a "cancel operation" because of user interaction. * Like other exceptions, it breaks normal flows, while it is supposed to be ignored by all user-feedback components, * so the user will not be notified of cancelled exceptions. */