Store each cloud lineup as one row - #236
SunkenInTime wants to merge 4 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| bool outranks(CloudLineupRow row, CloudLineupRow? drawn) => | ||
| drawn == null || | ||
| row.revision > drawn.revision || | ||
| (row.revision == drawn.revision && | ||
| row.publicId.compareTo(drawn.publicId) > 0); |
There was a problem hiding this comment.
Unrelated edit hides moved spot
When two lineups share a landing, one teammate can move it while another edits only the notes in the other lineup. Both rows can reach the same revision, but this tie-break selects the notes row’s stale landing position when its public ID is greater. The saved move then disappears from the canvas. Preserve the move before merging.
Artifacts
Authored Dart reproduction source
- The authored test generates both row states and invokes cloud row hydration, showing the precise comparison performed.
Hydration before independent edits
- The executed baseline test shows both rows and the visible landing at (30, 40).
Hydration after independent edits
- The executed edited-state test shows row `a` moved to (99, 99) but the visible landing remains (30, 40), confirming the hidden move.
Cloud row implementation comparison
- The captured command compares the prior separate-landing-row representation with the current whole-row revision and publicId tie-break at lines 77–81.
| if (heldIds.contains(link.id) || | ||
| heldIds.contains(link.originId) || | ||
| heldIds.contains(link.landingId)) { | ||
| held.add(link.id); | ||
| heldIds.addAll([link.originId, link.landingId]); | ||
| grew = true; |
There was a problem hiding this comment.
Holding o1 in a chain o1 → l1, o2 → l1, o2 → l2 also holds the last lineup because each newly held link adds both endpoints to the hold. A teammate’s edit to that last lineup stays off the canvas until o1 is released, even though the last lineup does not touch o1. This is a non-blocking delay to an otherwise independent update.
Artifacts
Authored three-link merge test
- The authored Flutter test calls the real merge function and checks the visible k3 name and held IDs for both conditions, establishing the comparison.
Authored before-and-after test command
- The authored command temporarily installs a non-recursive control, runs both tests, and restores the PR source, making the comparison reproducible.
Test output with non-recursive hold propagation
- The executed control test reports k3 visible with the teammate’s edited name and not held, showing the comparison behavior.
Test output with PR hold propagation
- The executed PR-code test reports k3 visible with its original name and held, confirming the delayed edit.
A lineup synced as an origin row, a landing row and a link row, and the server checked across them: a link needed live ends (LINEUP_LINK_END_MISSING), an end could not go while a link used it (LINEUP_END_IN_USE), and clients opted in to both checks with applyBatch flags. The conflict unit was one part while the user's lineup needs all three. A lineup row now holds the whole lineup, kind "lineup", keyed by its id: name, video, notes, images, and full copies of its origin and landing. Lineups sharing a spot each carry their own copy; the client draws them as one spot again. Every row stands on its own, so the cross-row checks, their flags and error codes, and the endpoint index go. Page ownership (LINEUP_PAGE_MISMATCH) and strategy-scoped keys stay. Duplicating a strategy copies each row under fresh ids from one shared map, so copied lineups that shared a spot still share it. The agent summary counts each origin once per page, read from live lineups through a new by_strategyId_and_deleted index. Image references read data.images of any lineup row. Protocol 5: a client on 4 cannot read the new rows and writes rows the server no longer takes, so it is refused until it reloads. Production holds no lineup rows (checked 2026-10-01) and dev's lineups table is empty, so the narrowed schema needs no backfill. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The client side of one row per lineup. Live sync turns the page's graph into one desired row per link, each carrying its origin and landing as they are now, so moving a shared spot patches every lineup on it, each as its own op against its own revision, and deleting a spot deletes the rows of the lineups on it. Hydration rebuilds the graph from the rows: links from rows, origins and landings deduplicated by id. When copies of one spot disagree (only after concurrent edits) every client draws the copy from the row with the highest revision, ties to the greatest publicId, and live sync compares rows in that drawn form so the choice never authors a write. The user's own pending rows outrank the server until they land. Removed with the split rows: kind-prefixed keys and the drawn-row join, held-back and unsyncable lineups, endpoint restore, undrawn remote rows, endpoint-first and link-first outbox scheduling, the missing-end Keep mine path and its messages, the sync status's unsyncable source, the graph id rewrite on upload, and the Paranoia correction for landings (lineup rows are written at the in-game size and are never corrected). A remote merge no longer holds the whole graph while the user holds any part of it: only lineups touching what is held, closed over the spots they share, keep their screen copy; the rest of the page's lineups update. An outbox record from an older build can still hold an origin, landing or link op. It is never sent: it waits in attention with its own reason until the user discards it, and Keep mine leaves it there. The local model, UI, Hive, .ica and undo are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rites Copies of a shared spot can drift apart (a move that reached only some of its lineups), and the row revision is no measure of which copy is newest. Each end of a lineup row now carries its spot's version, an integer from 1; clients draw the copy with the highest version, then the greatest lineup id. Duplicate copies versions as they are, so a copy draws every spot where the original does. A patch or reorder of a deleted element or lineup was acked onto the tombstone and the edit vanished on the next load. It is now rejected with the new reason "deleted"; an add expecting the tombstone's revision brings the row back. A write that changes nothing on a live row stays a noop whatever revision it expects, so two clients healing the same row never conflict. Clients on protocol 4 still send checkLineupLinkEnds/checkLineupEndDeletes and graph rows. The args are accepted and ignored, and the old kinds pass argument validation to be refused per op in the handler, so such a client gets CLIENT_UPGRADE_REQUIRED rather than a validation error. Storage takes only lineups. The agent summary is refreshed only after a batch that touched a page, an agent element or a lineup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Live sync compared lineup rows in their drawn form, so a fan-out patch still queued for one row looked satisfied once the others landed, and its record was dropped; deleting the lineup holding the drawn copy then showed the stale one. Desired rows now carry each spot's drawn value and version (unchanged keeps the drawn version, a change goes one past it, a new spot starts at 1), versions taken from the rows the canvas was hydrated from, and are compared with the rows as stored. Any row holding another copy is patched until it really holds the drawn one, and a page is healed as soon as it is drawn. Versions never reach the canvas, Hive or .ica. Outbox records holding protocol 4 lineup ops are put in attention when the outbox loads, whatever state an older build left them in, so reconciling the canvas never removes them. Keep mine after a teammate's delete sends an add over the tombstone with the user's payload; a change it cannot bring back says so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
3d41429 to
02adca3
Compare
Stacked on #234. Merge this into #234's branch before #234 merges, so they deploy together.
A lineup used to sync as three kinds of cloud row (its standing spot, its landing spot, and the link with its details), each with references to the others. The server's conflict unit was one part, while what the user saved needed all three. That mismatch produced the run of lineup edge cases: a link arriving before its landing, "end in use" and "end missing" refusals, delete ordering, held-back and unsyncable rows, rows hydration wouldn't draw, endpoints brought back from the dead.
Now each lineup is one self-contained row: its name, notes, video and images, plus full copies of its standing spot and landing spot, each with its own id. Sharing is derived. Two lineups from the same spot carry the same spot id, and hydration draws that spot once. When copies disagree (only after concurrent edits), every client picks the copy from the highest-revision row, then the greatest publicId. Editing a shared spot patches every lineup that uses it, each against its own revision. A teammate's lineup added to a spot you delete keeps its own copy and survives. No row can depend on another, so a lineup conflict is an ordinary element conflict.
The local model, the UI, undo, Hive and .ica don't change.
Why now: production has zero lineup rows (read-only check on 2026-10-01), so nothing needs migrating. Once desktop users get cloud sync, a change like this would need a production migration.
Protocol 5: a protocol-4 tab can't read the new row, so the server now asks those tabs to reload. Protocol 4 never shipped publicly; the live web build is on 3, which #234 already refuses.
Net: about 600 lines out of the client and 330 out of the server; the lineup sync tests are rewritten for the new model.
🤖 Generated with Claude Code
Not safe to merge until the shared-landing move is preserved. The delayed independent edit is non-blocking.
Findings
Summary
Storing each cloud lineup as one row introduces two collaboration problems. A concurrent notes edit can hide a saved move of a shared landing; this must be fixed before merging. Holding one spot can also delay an independent teammate edit elsewhere in a connected lineup chain.
Reviews (1) · Last reviewed commit: "Sync each lineup as one row and draw sha..."