feat : refactor to use autosave.ts for stygian

This commit is contained in:
2026-10-05 15:49:06 +07:00 Unverified
parent 261f0d717d
commit d44a2597ba
4 changed files with 130 additions and 66 deletions
+41 -4
View File
@@ -53,13 +53,13 @@ describe("AutosaveCoordinator", () => {
expect(saves).toEqual([{ value: "สอง", expectedVersion: 1 }]);
});
it("serializes a new edit behind an in-flight save", async () => {
it("waits for the idle delay before saving an edit made during a request", async () => {
vi.useFakeTimers();
let resolveFirst: ((value: AutosaveResponse) => void) | undefined;
const saves: string[] = [];
const coordinator = new AutosaveCoordinator({
initialVersion: 4,
debounceMs: 10,
debounceMs: 100,
createPayload: (value: string, expectedVersion) => ({ value, expectedVersion }),
save: async ({ value, expectedVersion }) => {
saves.push(`${value}:${expectedVersion}`);
@@ -72,15 +72,52 @@ describe("AutosaveCoordinator", () => {
},
});
coordinator.schedule("แรก");
await advance(10);
await advance(100);
coordinator.schedule("ล่าสุด");
await advance(10);
await advance(50);
expect(saves).toEqual(["แรก:4"]);
resolveFirst?.({ status: "saved", version: 5 });
await advance(0);
expect(saves).toEqual(["แรก:4"]);
await advance(49);
expect(saves).toEqual(["แรก:4"]);
await advance(1);
expect(saves).toEqual(["แรก:4", "ล่าสุด:5"]);
});
it("uses an externally updated version and drops invalid pending drafts", async () => {
vi.useFakeTimers();
const { coordinator, saves } = harness();
coordinator.updateVersion(8);
coordinator.schedule("invalid");
coordinator.cancelPending();
await advance(750);
expect(saves).toEqual([]);
coordinator.schedule("valid");
await advance(750);
expect(saves).toEqual([{ value: "valid", expectedVersion: 8 }]);
});
it("does not save the same draft twice when it is queued during a request", async () => {
vi.useFakeTimers();
let resolveFirst: ((value: AutosaveResponse) => void) | undefined;
const save = vi.fn(() => new Promise<AutosaveResponse>((resolve) => {
resolveFirst = resolve;
}));
const coordinator = new AutosaveCoordinator({
initialVersion: 1,
debounceMs: 100,
createPayload: (draft: string) => draft,
save,
});
coordinator.schedule("same");
await advance(100);
coordinator.schedule("same");
resolveFirst?.({ status: "saved", version: 2 });
await advance(100);
expect(save).toHaveBeenCalledTimes(1);
});
it("retries only transient failures with capped exponential delays", async () => {
vi.useFakeTimers();
const { coordinator, save } = harness([
+17 -4
View File
@@ -48,6 +48,10 @@ export class AutosaveCoordinator<Draft, Payload, Snapshot = unknown> {
return this.version;
}
updateVersion(version: number): void {
this.version = Math.max(this.version, version);
}
schedule(draft: Draft): void {
if (this.disposed) return;
this.pending = draft;
@@ -61,6 +65,14 @@ export class AutosaveCoordinator<Draft, Payload, Snapshot = unknown> {
);
}
cancelPending(): void {
this.pending = undefined;
this.generation += 1;
this.clearDebounce();
if (this.retryTimer !== undefined) this.saving = false;
this.clearRetry();
}
async flush(): Promise<void> {
this.clearDebounce();
if (this.disposed || this.saving || this.pending === undefined) return;
@@ -101,11 +113,12 @@ export class AutosaveCoordinator<Draft, Payload, Snapshot = unknown> {
if (this.disposed) return;
if (response.status === "saved") {
this.version = response.version;
this.updateVersion(response.version);
this.saving = false;
this.options.onSaved?.(response.snapshot, response.version);
this.emit({ state: "saved", version: response.version, savedAt: new Date() });
if (this.pending !== undefined) void this.flush();
if (Object.is(this.pending, draft)) this.cancelPending();
this.options.onSaved?.(response.snapshot, this.version);
this.emit({ state: "saved", version: this.version, savedAt: new Date() });
if (this.pending !== undefined && this.debounceTimer === undefined) void this.flush();
return;
}