Skip to content

Commit aaf4e94

Browse files
committed
refactor: Ensure writes happen in clock order
1 parent f72b619 commit aaf4e94

File tree

1 file changed

+60
-31
lines changed

1 file changed

+60
-31
lines changed

src/library.ts

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export interface Document {
5353
readonly tree: MarkdownTree;
5454
readonly metadata: Readonly<DocumentMetadata>;
5555
readonly storedMetadata: Readonly<DocumentMetadata>;
56-
readonly dirty: boolean;
5756
updateMetadata(updater: (metadata: DocumentMetadata) => boolean): void;
5857
}
5958

@@ -345,6 +344,27 @@ export class IdbLibrary
345344
new TypedCustomEvent('post-edit-update', {detail: {node, change}}),
346345
);
347346
}
347+
348+
#writeQueue: (() => Promise<void>)[] = [];
349+
#writing = false;
350+
async enqueueWrite(write: () => Promise<void>) {
351+
this.#writeQueue.push(write);
352+
if (this.#writing) return;
353+
this.#writing = true;
354+
while (true) {
355+
const write = cast(this.#writeQueue.pop());
356+
await write();
357+
if (!this.#writeQueue.length) {
358+
this.#writing = false;
359+
return;
360+
}
361+
let preIdle = NaN;
362+
do {
363+
preIdle = this.#writeQueue.length;
364+
await new Promise((resolve) => requestIdleCallback(resolve));
365+
} while (preIdle != this.#writeQueue.length);
366+
}
367+
}
348368
}
349369

350370
class IdbDocument implements Document {
@@ -369,15 +389,14 @@ class IdbDocument implements Document {
369389
return this.#metadata;
370390
}
371391
readonly tree: MarkdownTree;
372-
dirty = false;
373392
updateMetadata(
374393
updater: (metadata: DocumentMetadata) => boolean,
375-
markDirty = true,
394+
scheduleSave = true,
376395
) {
377396
const newMetadata = structuredClone(this.metadata);
378397
if (!updater(newMetadata)) return;
379398
this.#metadata = newMetadata;
380-
if (markDirty) this.metadataChanged();
399+
if (scheduleSave) this.metadataChanged();
381400
}
382401
get name() {
383402
return (
@@ -400,7 +419,7 @@ class IdbDocument implements Document {
400419
this.tree.connect();
401420
this.tree.setRoot(this.tree.add<DocumentNode>(root), false);
402421
}
403-
noAwait(this.markDirty());
422+
noAwait(this.scheduleSave());
404423
}
405424
async save() {
406425
const {root, caches} = this.tree.serializeWithCaches();
@@ -428,7 +447,7 @@ class IdbDocument implements Document {
428447
});
429448
}
430449
private metadataChanged() {
431-
noAwait(this.markDirty());
450+
noAwait(this.scheduleSave());
432451
}
433452
private treeChanged(change: TreeChange) {
434453
if (change === 'edit') {
@@ -438,34 +457,44 @@ class IdbDocument implements Document {
438457
return true;
439458
}, false);
440459
}
441-
noAwait(this.markDirty());
460+
noAwait(this.scheduleSave());
442461
}
443-
private pendingModifications = 0;
444-
private async markDirty() {
445-
this.dirty = true;
446-
if (this.pendingModifications++) return;
447-
while (true) {
448-
const preSave = this.pendingModifications;
449-
// Save immediately on the fist iteration, may help keep tests fast.
450-
const oldMetadata = this.#storedMetadata;
451-
await this.save();
452-
this.library.dispatchEvent(
453-
new TypedCustomEvent('document-change', {
454-
detail: {document: this, oldMetadata},
455-
}),
456-
);
457-
if (this.pendingModifications === preSave) {
458-
this.pendingModifications = 0;
459-
this.dirty = false;
462+
463+
#pendinSaveOldMetadata: DocumentMetadata | undefined;
464+
#pendingSaveClock: number | undefined;
465+
private async scheduleSave() {
466+
const writeClock = this.#metadata.clock ?? 0;
467+
if (this.#pendingSaveClock === undefined) {
468+
this.#pendinSaveOldMetadata = this.#storedMetadata;
469+
} else {
470+
assert(this.#pendinSaveOldMetadata);
471+
if (writeClock === this.#pendingSaveClock) {
472+
// If the clock has not changed, we can join the scheduled write.
460473
return;
461474
}
462-
// Wait for an idle period with no modifications.
463-
let preIdle = NaN;
464-
do {
465-
preIdle = this.pendingModifications;
466-
// TODO: maybe a timeout is better?
467-
await new Promise((resolve) => requestIdleCallback(resolve));
468-
} while (preIdle != this.pendingModifications);
469475
}
476+
this.#pendingSaveClock = writeClock;
477+
noAwait(
478+
this.library.enqueueWrite(async () => {
479+
if (writeClock !== this.#pendingSaveClock) {
480+
assert(
481+
this.#pendingSaveClock !== undefined &&
482+
this.#pendingSaveClock > writeClock,
483+
);
484+
// A write for a newer clock will supersede this.
485+
return;
486+
}
487+
this.#pendingSaveClock = undefined;
488+
await this.save();
489+
if (this.#pendingSaveClock !== undefined) return;
490+
const oldMetadata = cast(this.#pendinSaveOldMetadata);
491+
this.#pendinSaveOldMetadata = undefined;
492+
this.library.dispatchEvent(
493+
new TypedCustomEvent('document-change', {
494+
detail: {document: this, oldMetadata},
495+
}),
496+
);
497+
}),
498+
);
470499
}
471500
}

0 commit comments

Comments
 (0)