diff --git a/packages/netlify-cms-core/src/__tests__/backend.spec.js b/packages/netlify-cms-core/src/__tests__/backend.spec.js index c318f2a4..6d96d1a2 100644 --- a/packages/netlify-cms-core/src/__tests__/backend.spec.js +++ b/packages/netlify-cms-core/src/__tests__/backend.spec.js @@ -114,7 +114,9 @@ describe('Backend', () => { }); describe('getLocalDraftBackup', () => { - const { localForage } = require('netlify-cms-lib-util'); + const { localForage, asyncLock } = require('netlify-cms-lib-util'); + + asyncLock.mockImplementation(() => ({ acquire: jest.fn(), release: jest.fn() })); beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/netlify-cms-core/src/backend.ts b/packages/netlify-cms-core/src/backend.ts index 8d90de42..ff41519c 100644 --- a/packages/netlify-cms-core/src/backend.ts +++ b/packages/netlify-cms-core/src/backend.ts @@ -34,6 +34,8 @@ import { getPathDepth, Config as ImplementationConfig, blobToFileObj, + asyncLock, + AsyncLock, } from 'netlify-cms-lib-util'; import { basename, join, extname, dirname } from 'path'; import { status } from './constants/publishModes'; @@ -178,6 +180,7 @@ export class Backend { authStore: AuthStore | null; config: Config; user?: User | null; + backupSync: AsyncLock; constructor( implementation: Implementation, @@ -196,6 +199,7 @@ export class Backend { if (this.implementation === null) { throw new Error('Cannot instantiate a Backend with no implementation'); } + this.backupSync = asyncLock(); } async status() { @@ -535,39 +539,56 @@ export class Backend { } async persistLocalDraftBackup(entry: EntryMap, collection: Collection) { - const key = getEntryBackupKey(collection.get('name'), entry.get('slug')); - const raw = this.entryToRaw(collection, entry); - if (!raw.trim()) { - return; + try { + await this.backupSync.acquire(); + const key = getEntryBackupKey(collection.get('name'), entry.get('slug')); + const raw = this.entryToRaw(collection, entry); + + if (!raw.trim()) { + return; + } + + const mediaFiles = await Promise.all( + entry + .get('mediaFiles') + .toJS() + .map(async (file: MediaFile) => { + // make sure to serialize the file + if (file.url?.startsWith('blob:')) { + const blob = await fetch(file.url as string).then(res => res.blob()); + return { ...file, file: blobToFileObj(file.name, blob) }; + } + return file; + }), + ); + + await localForage.setItem(key, { + raw, + path: entry.get('path'), + mediaFiles, + }); + const result = await localForage.setItem(getEntryBackupKey(), raw); + return result; + } catch (e) { + console.warn('persistLocalDraftBackup', e); + } finally { + this.backupSync.release(); } - - const mediaFiles = await Promise.all( - entry - .get('mediaFiles') - .toJS() - .map(async (file: MediaFile) => { - // make sure to serialize the file - if (file.url?.startsWith('blob:')) { - const blob = await fetch(file.url as string).then(res => res.blob()); - return { ...file, file: blobToFileObj(file.name, blob) }; - } - return file; - }), - ); - - await localForage.setItem(key, { - raw, - path: entry.get('path'), - mediaFiles, - }); - return localForage.setItem(getEntryBackupKey(), raw); } async deleteLocalDraftBackup(collection: Collection, slug: string) { - await localForage.removeItem(getEntryBackupKey(collection.get('name'), slug)); - // delete new entry backup if not deleted - slug && (await localForage.removeItem(getEntryBackupKey(collection.get('name')))); - return this.deleteAnonymousBackup(); + try { + await this.backupSync.acquire(); + await localForage.removeItem(getEntryBackupKey(collection.get('name'), slug)); + // delete new entry backup if not deleted + slug && (await localForage.removeItem(getEntryBackupKey(collection.get('name')))); + const result = await this.deleteAnonymousBackup(); + return result; + } catch (e) { + console.warn('deleteLocalDraftBackup', e); + } finally { + this.backupSync.release(); + } } // Unnamed backup for use in the global error boundary, should always be