fix(autosave) : retry constellation version conflicts
CI / Verify (push) Successful in 1m6s
CI / Build immutable images and deploy (push) Successful in 1m49s

This commit is contained in:
2026-09-02 12:59:14 +07:00 Unverified
parent 7708a2df31
commit c1b5ed867e
3 changed files with 42 additions and 1 deletions
+22
View File
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import {
AutosaveCoordinator,
saveWithConflictRetry,
type AutosaveResponse,
type AutosaveStatus,
} from "./autosave";
@@ -114,3 +115,24 @@ describe("AutosaveCoordinator", () => {
expect(invalid.statuses.at(-1)).toMatchObject({ state: "error", issues: ["title"] });
});
});
describe("saveWithConflictRetry", () => {
it("retries one version conflict against the current server version", async () => {
const calls: number[] = [];
const save = vi.fn(async (_value: string, expectedVersion: number) => {
calls.push(expectedVersion);
if (calls.length === 1) return { status: "conflict" as const, currentVersion: 8 };
return { status: "saved" as const, version: 9 };
});
const result = await saveWithConflictRetry(save, "constellation draft", 7);
expect(calls).toEqual([7, 8]);
expect(result).toEqual({ response: { status: "saved", version: 9 }, version: 8, retried: true });
});
it("stops after one retry when another concurrent save wins", async () => {
const save = vi.fn(async (_value: string, expectedVersion: number) => ({ status: "conflict" as const, currentVersion: expectedVersion + 1 }));
const result = await saveWithConflictRetry(save, "constellation draft", 3);
expect(save).toHaveBeenCalledTimes(2);
expect(result.response).toEqual({ status: "conflict", currentVersion: 5 });
});
});
+16
View File
@@ -12,6 +12,22 @@ export type AutosaveStatus =
| { state: "conflict"; currentVersion: number }
| { state: "error"; message: string; issues?: string[] };
export async function saveWithConflictRetry<Value, Response extends { status: string; currentVersion?: number }>(
save: (value: Value, expectedVersion: number) => Promise<Response>,
value: Value,
expectedVersion: number,
): Promise<{ response: Response; version: number; retried: boolean }> {
const first = await save(value, expectedVersion);
if (first.status !== "conflict" || !first.currentVersion || first.currentVersion === expectedVersion) {
return { response: first, version: expectedVersion, retried: false };
}
return {
response: await save(value, first.currentVersion),
version: first.currentVersion,
retried: true,
};
}
interface AutosaveCoordinatorOptions<Draft, Payload, Snapshot> {
initialVersion: number;
createPayload: (draft: Draft, expectedVersion: number) => Payload;