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
|
||||
// writers (onSaved onboarding, onSavedText layout sync) — saves, were
|
||||
// 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, {
|
||||
slug,
|
||||
saveBytes: readOnly ? undefined : saveBytes,
|
||||
uploadPolicy: (relPath) => (roomBacked.has(relPath) ? "room" : "upload"),
|
||||
log: append,
|
||||
onStatus: setStatus,
|
||||
...(readOnly
|
||||
|
|
|
|||
|
|
@ -552,3 +552,33 @@ describe("registerSaveHook lifetime", () => {
|
|||
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;
|
||||
/** Durable per-path safety block; only hook retirement clears it. */
|
||||
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 {
|
||||
const maxRetainedBytes = opts.maxRetainedBytes ?? MAX_RETAINED_SAVE_BYTES;
|
||||
|
|
@ -323,6 +332,16 @@ export function registerSaveHook(
|
|||
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;
|
||||
try {
|
||||
data = win.FS?.readFile(absPath) as Uint8Array;
|
||||
|
|
|
|||
Loading…
Reference in a new issue