standalone: room-backed files skip the save upload (save-flow uploadPolicy)
In ydoc mode the collab room owns a document's state — items reach it at commit time, layout heads at save time (syncLayoutFromSave). The raw CAS PUT on Ctrl+S was only ever the file-list registration + fallback copy, but for a file the listing already marks hasYdoc/isLive it bumps the revision for nothing, can 409 the target into the durable save-blocked banner, and leaves a shadow row the ydoc supersedes on every read. registerSaveHook gains `uploadPolicy(relPath) → "upload" | "room"`; WasmTool marks the boot listing's ydoc/live files "room" (ydoc mode only). Files with no row yet (created sheets, the synthesized .kicad_pro) still upload; onSavedText still runs for room-backed saves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Wd1r3ewftpV1DBSEArpRa
This commit is contained in:
parent
473e1eb935
commit
5650e6193f
3 changed files with 60 additions and 0 deletions
|
|
@ -1991,9 +1991,20 @@ export function WasmTool({
|
||||||
// Read-only sessions register neither upload nor the save-driven room
|
// Read-only sessions register neither upload nor the save-driven room
|
||||||
// writers (onSaved onboarding, onSavedText layout sync) — saves, were
|
// writers (onSaved onboarding, onSavedText layout sync) — saves, were
|
||||||
// any reachable past the wasm lock, stay MEMFS-only.
|
// any reachable past the wasm lock, stay MEMFS-only.
|
||||||
|
// Room-backed files (ydoc/live in the boot listing) never upload on
|
||||||
|
// save in ydoc mode: the room owns their state (save-flow uploadPolicy).
|
||||||
|
// A file created this session, or one whose room is first seeded now,
|
||||||
|
// has no ydoc row yet and still uploads — that is the registration +
|
||||||
|
// first fallback copy the backend file list needs.
|
||||||
|
const roomBacked = new Set(
|
||||||
|
docSource === "ydoc"
|
||||||
|
? files.filter((f) => f.hasYdoc || f.isLive).map((f) => f.path)
|
||||||
|
: [],
|
||||||
|
);
|
||||||
const saveHookHandle = registerSaveHook(win, {
|
const saveHookHandle = registerSaveHook(win, {
|
||||||
slug,
|
slug,
|
||||||
saveBytes: readOnly ? undefined : saveBytes,
|
saveBytes: readOnly ? undefined : saveBytes,
|
||||||
|
uploadPolicy: (relPath) => (roomBacked.has(relPath) ? "room" : "upload"),
|
||||||
log: append,
|
log: append,
|
||||||
onStatus: setStatus,
|
onStatus: setStatus,
|
||||||
...(readOnly
|
...(readOnly
|
||||||
|
|
|
||||||
|
|
@ -552,3 +552,33 @@ describe("registerSaveHook lifetime", () => {
|
||||||
expect(statuses.at(-1)).toBe("Saved board.kicad_pcb ✓");
|
expect(statuses.at(-1)).toBe("Saved board.kicad_pcb ✓");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("registerSaveHook uploadPolicy", () => {
|
||||||
|
it("a room-backed path is NOT uploaded; other paths still are", () => {
|
||||||
|
const saveBytes = vi.fn(async (_p: string, _b: Uint8Array) => SAVE_COMMITTED);
|
||||||
|
const onSavedText = vi.fn();
|
||||||
|
const status: string[] = [];
|
||||||
|
const win: SaveHookWindow = {
|
||||||
|
FS: { readFile: () => new Uint8Array([40, 41]) } as unknown as SaveHookWindow["FS"],
|
||||||
|
kicadCollab: {},
|
||||||
|
};
|
||||||
|
registerSaveHook(win, {
|
||||||
|
slug: SLUG,
|
||||||
|
saveBytes,
|
||||||
|
onSavedText,
|
||||||
|
uploadPolicy: (relPath) => (relPath === "root.kicad_sch" ? "room" : "upload"),
|
||||||
|
log: () => {},
|
||||||
|
onStatus: (t) => status.push(t),
|
||||||
|
});
|
||||||
|
win.kicadCollab!.onSave!(`${PROJ}/root.kicad_sch`);
|
||||||
|
win.kicadCollab!.onSave!(`${PROJ}/root.kicad_pro`);
|
||||||
|
// Layout save-sync still observed the room-backed save.
|
||||||
|
expect(onSavedText.mock.calls.map((c) => c[0])).toEqual([
|
||||||
|
"root.kicad_sch",
|
||||||
|
"root.kicad_pro",
|
||||||
|
]);
|
||||||
|
expect(saveBytes).toHaveBeenCalledTimes(1);
|
||||||
|
expect(saveBytes.mock.calls[0]?.[0]).toBe("root.kicad_pro");
|
||||||
|
expect(status.some((s) => /root\.kicad_sch.*collab room/.test(s))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,15 @@ export function registerSaveHook(
|
||||||
maxPaths?: number;
|
maxPaths?: number;
|
||||||
/** Durable per-path safety block; only hook retirement clears it. */
|
/** Durable per-path safety block; only hook retirement clears it. */
|
||||||
onBlocked?: (block: SaveBlock) => void;
|
onBlocked?: (block: SaveBlock) => void;
|
||||||
|
/**
|
||||||
|
* Per-path upload policy. "room" ⇒ the file's live state is owned by its
|
||||||
|
* collab room (ydoc-backed in the listing): the save stays MEMFS-only and
|
||||||
|
* is NOT uploaded — the items already reached the room at commit time and
|
||||||
|
* `onSavedText` reconciled the layout heads, so a raw PUT would only bump
|
||||||
|
* the revision, risk a spurious CAS block on the target, and leave a stale
|
||||||
|
* shadow row the ydoc supersedes on every read. Absent/"upload" ⇒ upload.
|
||||||
|
*/
|
||||||
|
uploadPolicy?: (relPath: string) => "upload" | "room";
|
||||||
},
|
},
|
||||||
): SaveHookHandle {
|
): SaveHookHandle {
|
||||||
const maxRetainedBytes = opts.maxRetainedBytes ?? MAX_RETAINED_SAVE_BYTES;
|
const maxRetainedBytes = opts.maxRetainedBytes ?? MAX_RETAINED_SAVE_BYTES;
|
||||||
|
|
@ -323,6 +332,16 @@ export function registerSaveHook(
|
||||||
statusClearTimer = undefined;
|
statusClearTimer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.uploadPolicy?.(relPath) === "room") {
|
||||||
|
opts.log(`[save] ${relPath} is room-backed — synced via collab room, not uploaded`);
|
||||||
|
opts.onStatus(`Saved ${relPath} ✓ (collab room)`);
|
||||||
|
statusClearTimer = setTimeout(() => {
|
||||||
|
statusClearTimer = undefined;
|
||||||
|
if (generation === statusGeneration) opts.onStatus("");
|
||||||
|
}, 2500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let data: Uint8Array;
|
let data: Uint8Array;
|
||||||
try {
|
try {
|
||||||
data = win.FS?.readFile(absPath) as Uint8Array;
|
data = win.FS?.readFile(absPath) as Uint8Array;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue