29-s3-followup: module node-data writes become undoable and batched - #227
Merged
Merged
Conversation
…rule - api.flow.nodes() snapshots carry x/y (read-only) — addNodes took positions in, nothing gave them back, so a module building a graph could not ask where it was already occupied (DEVX #16) - api.flow.freeRegion({w, h, graphId}) answers where a block of new nodes lands: left-aligned under the lowest card, a margin from the origin in an empty graph - the rule lives in ONE leaf, src/lib/flowLayout.js (imports nothing); gameRecipes' copy left core in R3a, and the surviving right-of-everything copy in hudActions' addBinding now calls the same function with side 'right' (byte-identical x, pinned by a unit test) - suites: sdk-game-seams +7 checks (positions on both peers, two recipes in a row beside a user node overlap nothing and stack downward, the overlap detector catches a constant-placed block, unknown graph = margin); new unit test tests/unit/flowLayout.test.js (7) - counterfactual: freeRegion returning a constant -> "two recipes in a row land on nothing" and "each block lands BELOW" go red (e2e), 5 unit tests red - hud-actions 65/65 green (addBinding placement); svelte-check 341/47 = base, identical error list; vitest 143 -> 150 at this commit Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… in the seam - api.flow.onChange(fn) (graph documents AND the trigger log — a node firing is what a collected-state list changes on), api.game.onChange(fn) (the game singleton), api.peerVars.onChange(fn) (my row + remote rows): thin subscribe wrappers over stores core already has, each journalled for teardown and also returning an off() for a toolbox that mounts and unmounts (DEVX #17) - coalesced INSIDE the seam (src/lib/coalesce.js, a leaf): the svelte subscribe's synchronous first call is swallowed, any number of ticks before the next frame run the handler once, a flush queued at teardown never runs, a throwing handler cannot break the store write. One FRAME, not a microtask: a microtask was measured to fold a local burst but not thirty edits arriving from a peer (+30 handler calls for 30 messages); a 100ms timer races the frame so a background tab still gets its call - suites: sdk-game-seams +15 checks (60 idle frames with a time node ticking fire nothing; a 30-tick bulk edit = 1 call; the peer's handler fires as edits arrive and folds them, +1 for 30; a node firing = 1; game-state change fires on both peers; 20 setVar in a burst = 1; a peer-var write fires mine and the peer's; the returned off() stops one subscriber; deactivate unsubscribes all three); tests/unit/coalesce.test.js (8) - counterfactual: debounce removed (flush called per tick) -> bulk edit 30, arriving +30, firing +30, setVar 20: five checks red; microtask instead of frame -> "arriving edits are coalesced" red (+30 for 30) - sdk-game-seams 61/61 (base 34), hud-actions 65/65, vitest 158 (base 143), svelte-check 341/47 = base with an identical error list, build green Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…module - A module's node-data write went out on the wire and never touched the undo stack, so the next Ctrl+Z undid whatever came BEFORE it (the sdk-polish measurement: 60 writes, 0 entries, the undo removed the nodes' creation). - setNodeData now records a `flownodes` op:'data' entry with the patched keys' previous values (structuredClone'd, so a module mutating its patch later cannot rewrite history) and `moduleId`. Same `nodedata` message on the wire. - flownodes data items may carry their own graphId (overrides the entry's) so one entry can span graphs - the batch call in S3b needs it. Additive; every existing entry has none and behaves as before. - sdk-game-seams section 4b: one entry, attributed, one undo restores the previous data on BOTH peers, one redo re-applies, unknown id still false. - Counterfactual: record call removed -> 3 red (depth 5 -> 5, top entry was `game`; undo left the edit in place on both peers). - sdk-game-seams 65/65 (base 59/59); vitest 158/158; svelte-check 341/47, identical list; build green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- setNodesData([{id, patch}, ...]) writes each node through the same path and
validation as setNodeData (unknown ids and junk items skipped), records ONE
`flownodes` data entry for the whole batch (items carry their own graphId, so
a group spanning one object graph per collectible is still one step), and
returns how many nodes it wrote. An empty batch records nothing.
- Wire: the ordinary per-node `nodedata`. There is no batched type, and adding
one would leave older peers unconverged until nodesync - the plan's rule
(undo correctness first, wire economy second); sdk-polish measured 60
messages at 7.1 ms, all delivered.
- The flownodes data handler now undoes its items in REVERSE: a batch that
writes one node twice recorded the second item's `before` after the first
write, so forward order restored the intermediate value.
- sdk-game-seams section 4c: 12 nodes over the scene graph + an object graph,
one entry of 13 items spanning 2 graphs, replicated, one undo restores all
on both peers (incl. the twice-written node), one redo, empty batch inert.
- Counterfactual A: record per item -> 3 red (11 -> 24 entries, undo left 12
of 12 edited). Counterfactual B: forward-order undo -> 1 red (the
twice-written node read "grp" after undo).
- sdk-game-seams 72/72 (base 59/59); vitest 158/158; svelte-check 341/47,
identical list; build green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e Ctrl+Z, a late joiner - Section 7b reproduces sdk-polish's measurement shape: sixty nodes over the scene graph and two object graphs, created in three calls (so the entry under the edit is a CREATION), one setNodesData call (60 in ~5 ms), all sixty on the peer; a REAL Ctrl+Z restores all sixty on both peers and the sixty nodes are still there; after redo a late joiner (C dials A) holds the edited values from the ordinary full-state reply, and a later Ctrl+Z reverts them on the joiner too. - Counterfactual 1: skip the history entry -> 7 red; Ctrl+Z undid the creation (40 of 60 nodes left, the edit still in place), exactly the sdk-polish finding. - Counterfactual 2: drop the batch (one entry per node) -> 5 red; one Ctrl+Z reverted one node of sixty (tags ["bulk60","orig"]). - sdk-game-seams 78/78 (base 59/59); ai-flow-physics 38/38 and hud-actions 65/65 (the other flownodes users) held; vitest 158/158; svelte-check 341/47, identical list; build green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 19, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Roadmap 29, lane
29-s3-followup— planplans/core/pending/module-node-data-undo-and-batch.md(scope 1, 2, 4; scope 3 is modules PR below).Stacked on #224 (
feat/29-sdk-polish, S1/S2): this branch fast-forwarded it in, so merge #224 first and this diff shrinks to the last three commits.What landed
5181f78S3aapi.flow.setNodeDatarecords ONEflownodesop:dataundo entry (the patched keys' previous values, structuredClone'd) withmoduleId. Before: a module's write left the stack alone and the next Ctrl+Z undid whatever came before (sdk-polish measured 60 writes, 0 entries).flownodesdata items may carry their owngraphId(additive, history-local).28d2c99S3bapi.flow.setNodesData([{id, patch}, …])→ ONE entry across any number of graphs, returns how many nodes it wrote. Wire unchanged: one ordinarynodedataper node (no batched type exists; a new one would leave older peers unconverged — the plan's "undo correctness first"). The data-entry undo now replays items in reverse so a batch writing one node twice undoes correctly.9c9a345S3d sdk-game-seams 7b: sixty nodes over three graphs, one real Ctrl+Z restores all sixty and nothing else (the creation survives), late joiner holds the edited values, a later undo reaches it.Suites
Counterfactuals
gameentry)For the integrator
api.flow.setNodeDatais now undoable, andapi.flow.setNodesData([{id, patch}])makes a group edit one undo step (the collectible manager's group settings use it)."flow.setNodeData(id, patch)→ one undo step; new rowflow.setNodesData(list)→ number written, one undo step, onenodedataper node on the wire.flownodesdata items may carrygraphId+ the entry amoduleId; data undo replays items in REVERSE (a batch may write one node twice).Modules side: theprototype-app/modules
feat/29-s3-collectible(collectible v1.1.1).🤖 Generated with Claude Code