fix(backup): synchronize calls to localForage (#3932)

This commit is contained in:
Erez Rokah 2020-06-21 12:42:30 +03:00 committed by GitHub
parent 330fadd1d7
commit 86562ad47a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 53 additions and 30 deletions

View File

@ -114,7 +114,9 @@ describe('Backend', () => {
}); });
describe('getLocalDraftBackup', () => { 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(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();

View File

@ -34,6 +34,8 @@ import {
getPathDepth, getPathDepth,
Config as ImplementationConfig, Config as ImplementationConfig,
blobToFileObj, blobToFileObj,
asyncLock,
AsyncLock,
} from 'netlify-cms-lib-util'; } from 'netlify-cms-lib-util';
import { basename, join, extname, dirname } from 'path'; import { basename, join, extname, dirname } from 'path';
import { status } from './constants/publishModes'; import { status } from './constants/publishModes';
@ -178,6 +180,7 @@ export class Backend {
authStore: AuthStore | null; authStore: AuthStore | null;
config: Config; config: Config;
user?: User | null; user?: User | null;
backupSync: AsyncLock;
constructor( constructor(
implementation: Implementation, implementation: Implementation,
@ -196,6 +199,7 @@ export class Backend {
if (this.implementation === null) { if (this.implementation === null) {
throw new Error('Cannot instantiate a Backend with no implementation'); throw new Error('Cannot instantiate a Backend with no implementation');
} }
this.backupSync = asyncLock();
} }
async status() { async status() {
@ -535,39 +539,56 @@ export class Backend {
} }
async persistLocalDraftBackup(entry: EntryMap, collection: Collection) { async persistLocalDraftBackup(entry: EntryMap, collection: Collection) {
const key = getEntryBackupKey(collection.get('name'), entry.get('slug')); try {
const raw = this.entryToRaw(collection, entry); await this.backupSync.acquire();
if (!raw.trim()) { const key = getEntryBackupKey(collection.get('name'), entry.get('slug'));
return; const raw = this.entryToRaw(collection, entry);
if (!raw.trim()) {
return;
}
const mediaFiles = await Promise.all<MediaFile>(
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<BackupEntry>(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<MediaFile>(
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<BackupEntry>(key, {
raw,
path: entry.get('path'),
mediaFiles,
});
return localForage.setItem(getEntryBackupKey(), raw);
} }
async deleteLocalDraftBackup(collection: Collection, slug: string) { async deleteLocalDraftBackup(collection: Collection, slug: string) {
await localForage.removeItem(getEntryBackupKey(collection.get('name'), slug)); try {
// delete new entry backup if not deleted await this.backupSync.acquire();
slug && (await localForage.removeItem(getEntryBackupKey(collection.get('name')))); await localForage.removeItem(getEntryBackupKey(collection.get('name'), slug));
return this.deleteAnonymousBackup(); // 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 // Unnamed backup for use in the global error boundary, should always be