collab: repair duplicated layout header groups after seed and on remote layout merges (ysync 0011 follow-up; shared → f14401b)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0189z2siCsezE39ugcDKNBMs
This commit is contained in:
parent
8fa6bae084
commit
c981e3dc75
3 changed files with 59 additions and 1 deletions
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
import * as Y from "yjs";
|
||||
import {
|
||||
docToFile,
|
||||
duplicateSingletonHeadIndices,
|
||||
fileToDoc,
|
||||
itemsWireToDelta,
|
||||
kicadLibSymbolsMap,
|
||||
|
|
@ -226,6 +227,32 @@ describe("bindKicadCollab — two editors over relayed Y.Docs", () => {
|
|||
expect(edB.store["r-1"]).toBeDefined();
|
||||
});
|
||||
|
||||
it("a layout-only write racing a file seed converges to ONE header block (no double version)", () => {
|
||||
// Session 1 (old client) save-all wrote a layout-only doc; session 2 file-seeded
|
||||
// BEFORE that state arrived. Merged, the layout carried the header twice and
|
||||
// KiCad rejected the materialized sheet ("Expecting … Got version").
|
||||
const { a, b, edA, edB, bindA, bindB } = setup();
|
||||
const file = `(kicad_wks (version 20220228) (generator "pl_editor")
|
||||
(setup (textsize 1.5 1.5) (linewidth 0.15))
|
||||
(rect (uuid "r-1") (name "border") (start 0 0 ltcorner) (end 0 0 rbcorner))
|
||||
)
|
||||
`;
|
||||
const seedDoc = fileToDoc(file);
|
||||
// Hold the relay: build the two histories independently, then merge.
|
||||
const hollow = new Y.Doc();
|
||||
syncLayoutToY(seedDoc, hollow, "layout-save");
|
||||
Object.assign(edA.store, seedDoc.items);
|
||||
bindA.seed(seedDoc); // A file-seeds an (apparently) empty room
|
||||
Y.applyUpdate(a, Y.encodeStateAsUpdate(hollow), "relay"); // the stale layout lands
|
||||
const text = docToFile(yToDoc(a));
|
||||
expect((text.match(/\(version /g) ?? []).length).toBe(1);
|
||||
expect(text).toBe(docToFile(seedDoc));
|
||||
// The doc itself was repaired (not just the render): B sees a single header.
|
||||
expect(duplicateSingletonHeadIndices(yToDoc(b).layout)).toEqual([]);
|
||||
bindB.seed();
|
||||
expect(edB.store["r-1"]).toBeDefined();
|
||||
});
|
||||
|
||||
it("a seeded room that was legitimately emptied is still adopted (not mistaken for hollow)", () => {
|
||||
const { a, edA, edB, bindA, bindB } = setup();
|
||||
const file = `(kicad_wks (version 20220228) (generator "pl_editor")
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ import {
|
|||
kicadItemsMap,
|
||||
kicadLibSymbolsMap,
|
||||
parseItemsWireDelta,
|
||||
repairLayoutY,
|
||||
seedDocToY,
|
||||
SEXPR_VERSION_SUPPORTED,
|
||||
upsertLibSymbolsToY,
|
||||
wireItemUuids,
|
||||
wireLibSymbols,
|
||||
Y_KDOC_LAYOUT,
|
||||
Y_KDOC_META,
|
||||
Y_KDOC_REVERT_AT,
|
||||
Y_KDOC_REVERT_NONCE,
|
||||
|
|
@ -235,6 +237,26 @@ export function bindKicadCollab(
|
|||
};
|
||||
items.observeDeep(observer);
|
||||
|
||||
// Layout convergence (ysync 0011 follow-up): a remote merge that lands a
|
||||
// second copy of the header block (a layout-only save-sync racing a file
|
||||
// seed) would materialize as a file KiCad refuses to load. Repair on every
|
||||
// remote layout change and after seed(); peers delete the same entries, so
|
||||
// the doc converges to one header. A viewer never writes.
|
||||
const layout = doc.getArray(Y_KDOC_LAYOUT);
|
||||
const repairLayout = (why: string): void => {
|
||||
if (readOnly || destroyed) return;
|
||||
try {
|
||||
if (repairLayoutY(doc, ORIGIN)) clog(`layout repaired (duplicate header groups) — ${why}`);
|
||||
} catch (err) {
|
||||
cwarn("layout repair failed", err);
|
||||
}
|
||||
};
|
||||
const onLayout = (_ev: unknown, txn: Y.Transaction) => {
|
||||
if (txn.origin === ORIGIN) return;
|
||||
repairLayout("remote layout change");
|
||||
};
|
||||
layout.observe(onLayout);
|
||||
|
||||
// Validity-revert notice (kicad-validity 0001 B3): the backend stamps
|
||||
// kdoc_meta.revertNonce when it rolls the doc back to the last valid state
|
||||
// (the content itself arrives through the normal item sync above). Watched
|
||||
|
|
@ -261,6 +283,14 @@ export function bindKicadCollab(
|
|||
revMeta.observe(onRevertMeta);
|
||||
|
||||
function seed(seedDoc?: KicadDoc, opts?: { editorMatchesDoc?: boolean }): void {
|
||||
try {
|
||||
seedInner(seedDoc, opts);
|
||||
} finally {
|
||||
repairLayout("post-seed");
|
||||
}
|
||||
}
|
||||
|
||||
function seedInner(seedDoc?: KicadDoc, opts?: { editorMatchesDoc?: boolean }): void {
|
||||
seeded = true; // open the UP gate; everything below runs synchronously
|
||||
// `ydocHasState` (meta + layout + items), NOT `items.size`: a populated
|
||||
// drawing sheet (pl_editor .kicad_wks) has zero uuid items, so an items-only
|
||||
|
|
@ -422,6 +452,7 @@ export function bindKicadCollab(
|
|||
seed,
|
||||
destroy: () => {
|
||||
destroyed = true; // gates the DOWN hook — see bug 07 note above
|
||||
layout.unobserve(onLayout);
|
||||
detachSeedArbitration?.();
|
||||
detachSeedArbitration = undefined;
|
||||
items.unobserveDeep(observer);
|
||||
|
|
|
|||
Loading…
Reference in a new issue