diff --git a/app/server/lib/ActiveDoc.ts b/app/server/lib/ActiveDoc.ts index 0725b154..a9c41f0c 100644 --- a/app/server/lib/ActiveDoc.ts +++ b/app/server/lib/ActiveDoc.ts @@ -300,6 +300,7 @@ export class ActiveDoc extends EventEmitter { @ActiveDoc.keepDocOpen public async createDoc(docSession: OptDocSession): Promise { this.logDebug(docSession, "createDoc"); + await this._docManager.storageManager.prepareToCreateDoc(this.docName); await this.docStorage.createFile(); await this._rawPyCall('load_empty'); const timezone = docSession.browserSettings ? docSession.browserSettings.timezone : DEFAULT_TIMEZONE; diff --git a/app/server/lib/DocSnapshots.ts b/app/server/lib/DocSnapshots.ts index 2b16cf3c..634e9dc7 100644 --- a/app/server/lib/DocSnapshots.ts +++ b/app/server/lib/DocSnapshots.ts @@ -110,6 +110,17 @@ export class DocSnapshotInventory implements IInventory { constructor(private _doc: ExternalStorage, private _meta: ExternalStorage, private _getFilename: (key: string) => Promise) {} + /** + * Start keeping inventory for a new document. + */ + public async create(key: string) { + await this._mutex.runExclusive(key, async() => { + const fname = await this._getFilename(key); + await this._saveToFile(fname, []); + this._needFlush.add(key); + }); + } + /** * Add a new snapshot of a document to the existing inventory. A prevSnapshotId may * be supplied as a cross-check. It will be matched against the most recent @@ -126,7 +137,7 @@ export class DocSnapshotInventory implements IInventory { await this._mutex.runExclusive(key, async() => { const snapshots = await this._getSnapshots(key, prevSnapshotId); // Could be already added if reconstruction happened. - if (snapshots[0].snapshotId === snapshot.snapshotId) { return; } + if (snapshots[0] && snapshots[0].snapshotId === snapshot.snapshotId) { return; } this._normalizeMetadata(snapshot); snapshots.unshift(snapshot); const fname = await this._getFilename(key); diff --git a/app/server/lib/DocStorageManager.ts b/app/server/lib/DocStorageManager.ts index 9e6eade5..a4b44246 100644 --- a/app/server/lib/DocStorageManager.ts +++ b/app/server/lib/DocStorageManager.ts @@ -87,6 +87,10 @@ export class DocStorageManager implements IDocStorageManager { */ public async prepareLocalDoc(docName: string, docSession: OptDocSession): Promise { return false; } + public async prepareToCreateDoc(docName: string): Promise { + // nothing to do + } + /** * Returns a promise for the list of docNames to show in the doc list. For the file-based * storage, this will include all .grist files under the docsRoot. @@ -212,7 +216,7 @@ export class DocStorageManager implements IDocStorageManager { // nothing to do } - public addToStorage(id: string): void { + public async addToStorage(id: string): Promise { // nothing to do } diff --git a/app/server/lib/HostedStorageManager.ts b/app/server/lib/HostedStorageManager.ts index ec217c9d..a62d77c5 100644 --- a/app/server/lib/HostedStorageManager.ts +++ b/app/server/lib/HostedStorageManager.ts @@ -184,7 +184,8 @@ export class HostedStorageManager implements IDocStorageManager { */ public async addToStorage(docId: string) { if (this._disableS3) { return; } - await this._ext.upload(docId, this.getPath(docId)); + this._uploads.addOperation(docId); + await this._uploads.expediteOperationAndWait(docId); } public getPath(docName: string): string { @@ -234,6 +235,13 @@ export class HostedStorageManager implements IDocStorageManager { } } + public async prepareToCreateDoc(docName: string): Promise { + if (this._inventory) { + await this._inventory.create(docName); + this._onInventoryChange(docName); + } + } + // Gets a copy of the document, eg. for downloading. Returns full file path. // Copy won't change if edits are made to the document. It is caller's responsibility // to delete the result. @@ -656,10 +664,7 @@ export class HostedStorageManager implements IDocStorageManager { metadata } await this._inventory.add(docId, snapshot, prevSnapshotId); - const scheduled = this._pruner.requestPrune(docId); - if (!scheduled) { - await this._inventory.flush(docId); - } + await this._onInventoryChange(docId); } finally { // Clean up backup. // NOTE: fse.remove succeeds also when the file does not exist. @@ -667,6 +672,14 @@ export class HostedStorageManager implements IDocStorageManager { } } + // Make sure inventory change is followed up on. + private async _onInventoryChange(docId: string) { + const scheduled = this._pruner.requestPrune(docId); + if (!scheduled) { + await this._inventory.flush(docId); + } + } + // Extract actionHash, actionNum, and timezone from a document backup. private async _getDocMetadata(fname: string): Promise<{[key: string]: string}> { const result: Record = {}; diff --git a/app/server/lib/IDocStorageManager.ts b/app/server/lib/IDocStorageManager.ts index fe708919..a1dbcca4 100644 --- a/app/server/lib/IDocStorageManager.ts +++ b/app/server/lib/IDocStorageManager.ts @@ -12,6 +12,7 @@ export interface IDocStorageManager { // In the current implementation, it is called in the context of an // AsyncCreate[docName]. prepareLocalDoc(docName: string, docSession: OptDocSession): Promise; + prepareToCreateDoc(docName: string): Promise; listDocs(): Promise; deleteDoc(docName: string, deletePermanently?: boolean): Promise; @@ -24,7 +25,7 @@ export interface IDocStorageManager { // If reason is set to 'edit' the user-facing timestamp on the document should be updated. markAsChanged(docName: string, reason?: 'edit'): void; testReopenStorage(): void; // restart storage during tests - addToStorage(docName: string): void; // add a new local document to storage + addToStorage(docName: string): Promise; // add a new local document to storage prepareToCloseStorage(): void; // speed up sync with remote store getCopy(docName: string): Promise; // get an immutable copy of a document