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
|
|
@ -1 +1 @@
|
||||||
Subproject commit 031faaedd37365ab7391707c2e61463b0f842d9d
|
Subproject commit f14401b95d66c4997f66b6b0f7755745f4c59294
|
||||||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
import {
|
import {
|
||||||
docToFile,
|
docToFile,
|
||||||
|
duplicateSingletonHeadIndices,
|
||||||
fileToDoc,
|
fileToDoc,
|
||||||
itemsWireToDelta,
|
itemsWireToDelta,
|
||||||
kicadLibSymbolsMap,
|
kicadLibSymbolsMap,
|
||||||
|
|
@ -226,6 +227,32 @@ describe("bindKicadCollab — two editors over relayed Y.Docs", () => {
|
||||||
expect(edB.store["r-1"]).toBeDefined();
|
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)", () => {
|
it("a seeded room that was legitimately emptied is still adopted (not mistaken for hollow)", () => {
|
||||||
const { a, edA, edB, bindA, bindB } = setup();
|
const { a, edA, edB, bindA, bindB } = setup();
|
||||||
const file = `(kicad_wks (version 20220228) (generator "pl_editor")
|
const file = `(kicad_wks (version 20220228) (generator "pl_editor")
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,13 @@ import {
|
||||||
kicadItemsMap,
|
kicadItemsMap,
|
||||||
kicadLibSymbolsMap,
|
kicadLibSymbolsMap,
|
||||||
parseItemsWireDelta,
|
parseItemsWireDelta,
|
||||||
|
repairLayoutY,
|
||||||
seedDocToY,
|
seedDocToY,
|
||||||
SEXPR_VERSION_SUPPORTED,
|
SEXPR_VERSION_SUPPORTED,
|
||||||
upsertLibSymbolsToY,
|
upsertLibSymbolsToY,
|
||||||
wireItemUuids,
|
wireItemUuids,
|
||||||
wireLibSymbols,
|
wireLibSymbols,
|
||||||
|
Y_KDOC_LAYOUT,
|
||||||
Y_KDOC_META,
|
Y_KDOC_META,
|
||||||
Y_KDOC_REVERT_AT,
|
Y_KDOC_REVERT_AT,
|
||||||
Y_KDOC_REVERT_NONCE,
|
Y_KDOC_REVERT_NONCE,
|
||||||
|
|
@ -235,6 +237,26 @@ export function bindKicadCollab(
|
||||||
};
|
};
|
||||||
items.observeDeep(observer);
|
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
|
// Validity-revert notice (kicad-validity 0001 B3): the backend stamps
|
||||||
// kdoc_meta.revertNonce when it rolls the doc back to the last valid state
|
// 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
|
// (the content itself arrives through the normal item sync above). Watched
|
||||||
|
|
@ -261,6 +283,14 @@ export function bindKicadCollab(
|
||||||
revMeta.observe(onRevertMeta);
|
revMeta.observe(onRevertMeta);
|
||||||
|
|
||||||
function seed(seedDoc?: KicadDoc, opts?: { editorMatchesDoc?: boolean }): void {
|
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
|
seeded = true; // open the UP gate; everything below runs synchronously
|
||||||
// `ydocHasState` (meta + layout + items), NOT `items.size`: a populated
|
// `ydocHasState` (meta + layout + items), NOT `items.size`: a populated
|
||||||
// drawing sheet (pl_editor .kicad_wks) has zero uuid items, so an items-only
|
// drawing sheet (pl_editor .kicad_wks) has zero uuid items, so an items-only
|
||||||
|
|
@ -422,6 +452,7 @@ export function bindKicadCollab(
|
||||||
seed,
|
seed,
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
destroyed = true; // gates the DOWN hook — see bug 07 note above
|
destroyed = true; // gates the DOWN hook — see bug 07 note above
|
||||||
|
layout.unobserve(onLayout);
|
||||||
detachSeedArbitration?.();
|
detachSeedArbitration?.();
|
||||||
detachSeedArbitration = undefined;
|
detachSeedArbitration = undefined;
|
||||||
items.unobserveDeep(observer);
|
items.unobserveDeep(observer);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue