Skip to content

Commit 70e5955

Browse files
committed
fix: fixes timing issues when multiple users leave the same document together
1 parent ca83546 commit 70e5955

File tree

1 file changed

+33
-16
lines changed

1 file changed

+33
-16
lines changed

packages/server/src/Hocuspocus.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class Hocuspocus {
6262
};
6363

6464
loadingDocuments: Map<string, Promise<Document>> = new Map();
65+
unloadingDocuments: Map<string, Promise<void>> = new Map();
6566

6667
documents: Map<string, Document> = new Map();
6768

@@ -440,7 +441,7 @@ export class Hocuspocus {
440441
this.debouncer.isDebounced(debounceId) ||
441442
document.saveMutex.isLocked();
442443
const shouldUnload =
443-
document.getConnectionsCount() == 0 && !hasPendingWork;
444+
document.getConnectionsCount() === 0 && !hasPendingWork;
444445
if (shouldUnload) {
445446
this.unloadDocument(document);
446447
}
@@ -493,25 +494,41 @@ export class Hocuspocus {
493494

494495
async unloadDocument(document: Document): Promise<any> {
495496
const documentName = document.name;
496-
if (!this.documents.has(documentName)) return;
497497

498-
try {
499-
await this.hooks("beforeUnloadDocument", {
500-
instance: this,
501-
documentName,
502-
document,
503-
});
504-
} catch (e) {
498+
if (!this.documents.has(documentName) || document.saveMutex.isLocked())
505499
return;
506-
}
507500

508-
if (document.getConnectionsCount() > 0) {
509-
return;
510-
}
501+
if (this.unloadingDocuments.has(documentName))
502+
return this.unloadingDocuments.get(documentName);
503+
504+
// we need to make sure that the logic runs just once, even if multiple clients disconnect together
505+
const actualUnloadingLogic = async () => {
506+
try {
507+
await this.hooks("beforeUnloadDocument", {
508+
instance: this,
509+
documentName,
510+
document,
511+
});
512+
} catch (e) {
513+
return;
514+
}
515+
516+
if (document.getConnectionsCount() > 0) {
517+
return;
518+
}
519+
520+
this.documents.delete(documentName);
521+
document.destroy();
522+
await this.hooks("afterUnloadDocument", { instance: this, documentName });
523+
};
524+
525+
const unloading = actualUnloadingLogic();
526+
527+
this.unloadingDocuments.set(documentName, Promise.resolve(unloading));
528+
529+
await unloading;
511530

512-
this.documents.delete(documentName);
513-
document.destroy();
514-
await this.hooks("afterUnloadDocument", { instance: this, documentName });
531+
this.unloadingDocuments.delete(documentName);
515532
}
516533

517534
async openDirectConnection(

0 commit comments

Comments
 (0)