fix: prevent stale drawing saves

This commit is contained in:
2026-05-31 11:47:20 +00:00
parent c13c137e24
commit 6911033c1a
8 changed files with 391 additions and 56 deletions
+43 -16
View File
@@ -1,4 +1,5 @@
import { type DrawingStore } from "./db";
import { type DrawingMeta } from "./shared";
import { HttpError, normalizeTitle, parseJsonBody, parseSceneText } from "./scene";
type RouteRequest = Request & {
@@ -22,6 +23,29 @@ function errorResponse(error: unknown): Response {
return json({ ok: false, error: "Internal server error" }, { status: 500 });
}
function toMeta(drawing: DrawingMeta): DrawingMeta {
return {
id: drawing.id,
title: drawing.title,
createdAt: drawing.createdAt,
updatedAt: drawing.updatedAt,
revision: drawing.revision,
};
}
function parseExpectedRevision(raw: Record<string, unknown>): number | undefined {
if (!Object.hasOwn(raw, "expectedRevision")) {
return undefined;
}
const value = raw.expectedRevision;
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
throw new HttpError(400, "expectedRevision must be a non-negative integer");
}
return value;
}
export function createApi(store: DrawingStore) {
return {
health() {
@@ -36,10 +60,7 @@ export function createApi(store: DrawingStore) {
const drawing = store.createDrawing();
return json(
{
id: drawing.id,
title: drawing.title,
createdAt: drawing.createdAt,
updatedAt: drawing.updatedAt,
...toMeta(drawing),
...drawing.scene,
},
{ status: 201 },
@@ -55,10 +76,7 @@ export function createApi(store: DrawingStore) {
}
return json({
id: drawing.id,
title: drawing.title,
createdAt: drawing.createdAt,
updatedAt: drawing.updatedAt,
...toMeta(drawing),
...drawing.scene,
});
},
@@ -77,29 +95,38 @@ export function createApi(store: DrawingStore) {
Object.hasOwn(raw, "elements") ||
Object.hasOwn(raw, "appState") ||
Object.hasOwn(raw, "files");
const expectedRevision = parseExpectedRevision(raw);
if (!hasTitle && !hasScene) {
throw new HttpError(400, "Nothing to update");
}
const scene = hasScene ? parseSceneText(text) : undefined;
const updated = store.updateDrawing(id, {
const result = store.updateDrawing(id, {
scene,
title: hasTitle ? normalizeTitle(raw.title) : undefined,
expectedRevision,
});
if (!updated) {
if (!result.ok && result.reason === "not_found") {
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
}
if (!result.ok && result.reason === "conflict") {
return json(
{
ok: false,
error: "Drawing changed externally",
drawing: toMeta(result.drawing),
},
{ status: 409 },
);
}
const updated = result.drawing;
return json({
ok: true,
drawing: {
id: updated.id,
title: updated.title,
createdAt: updated.createdAt,
updatedAt: updated.updatedAt,
},
drawing: toMeta(updated),
});
} catch (error) {
return errorResponse(error);