Skip to content

Copy the page when "+" is pressed on a cloud strategy - #274

Open
SunkenInTime wants to merge 42 commits into
t3/cloud-image-copyfrom
t3/cloud-page-duplicate
Open

SunkenInTime wants to merge 42 commits into
t3/cloud-image-copyfrom
t3/cloud-page-duplicate

Conversation

@SunkenInTime

@SunkenInTime SunkenInTime commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

A new Page 2 made by "+" on a cloud strategy, a copy of page 1

On a local strategy, "+" adds a copy of the page on screen right after it. On a cloud strategy (the web beta and signed-in desktop), it added a blank page. With this PR and #275, cloud "+" copies the page too.

What changes

  • The server makes the copy (Copy the page on the server when "+" adds one #275). "+" queues the page add with the page on screen as its source. The server copies that page's items, lineups and image entries onto the new page in the same transaction that adds it. Copies get copy ids (Copy to next or previous page on cloud strategies #267), so items glide when the page turns.
  • Edits made just before "+" go ahead of the add in the queue, so the copy has them.
  • Once the server accepts the add, the app turns to the new page. "+" follows its own add to the server's answer for up to 5 seconds, even when other work is ahead of it in the queue.
  • Images copy whatever state their upload is in. A copied image shows its original's picture (Let an image show another image's picture #276/Find an image's picture by its picture id #277), so an image still uploading is copied too. Its copy shows the picture from this device's bytes at once, and from the cloud once the upload lands.
  • Edits to the page that the server refused aren't in its copy, and "+" says so. The copy is made from the server's page. This is checked when "+" is pressed and again once the add lands. The refused edits wait in the sync panel as usual.
  • If the add hasn't reached the cloud within 5 seconds (offline, a stalled send, or a backlog), a toast says the page will appear once it does. The add stays queued. The 5 seconds include the send itself.
  • A page too large to copy (over 12 MiB) is refused by the server, and the sync panel says so in its own words instead of calling it a conflict.
  • Switching strategies mid-"+" stops it before it adds or turns to anything.
  • Drawings no longer fade on the turn. A copied stroke counts as its original (TransitionPlanner.drawingsChanged).
  • Local "+" is unchanged.

Why the server copies (changed since the first version of this PR)

The first version copied on the client: it added the page with a direct call, then queued a copy of every item. Three rounds of review kept finding races between "the page exists" and "the copies land", the worst being a timed-out add that could still create a blank page after the app said none was added. Having the server copy in one transaction removes that whole class of problem. The field-merge work (#269) set the pattern: an optional op field, so no protocol bump.

Ground truth

Ran the web build in Edge against the dev deployment, which was running this branch's server code, signed in as a test account.

  • Page 1 had a lineup, an agent and an image. I pressed "+".
  • The app turned to a new Page 2 with the same lineup, agent and image (above).
  • Screenshots every 80 ms through the turn show every item staying in place. Nothing faded out and back in.
  • On the server, the new page, its image, its lineup and the image's asset row all have the same creation timestamp, so they were written in one transaction. The image's id is a copy id (d8b91641-…~cp1~…), and its asset row points at the original's stored file.

Then, with #276/#277: I placed a new image with its upload held for 20 seconds and pressed "+" during the hold. Frames: placed, then the copy during the hold, then after the upload landed:

Image copied by "+" while its upload was held

The "changes didn't save" toast was only exercised in tests, not in the browser.

Tests

  • strategy_page_session_provider_test, group '"+" on a cloud strategy':
    • "+" turns to the server's copy, its images showing their originals' pictures;
    • an answer that comes after the first send still turns to the page;
    • no answer in time says the page hasn't reached the cloud, and leaves the add queued;
    • a send that stalls still answers "+" in time;
    • refused edits to the copied page are named, before the press and in the same send.
  • cloud_sync_error_message_test: a page copy too large to make has its own explanation.
  • collab_sync_models_test: the field survives storage and withOpId, and an add without it sends no field.
  • strategy_op_queue_provider_test: the field survives a restart through the durable outbox.
  • page_copy_id_test: a copied stroke counts as its original.
  • Full suite on the combined branch: 1902 passed, 9 skipped.

Astra reviewed it three times. Round 1 found six issues, all fixed. Round 2 showed my fixes to three of them were still guessing what the server had done; they're now worked out when "+" is pressed. Round 3 found one more case, an edit refused in the same send as the add, which is now fixed. The "still uploading" warning those rounds were about is gone now that copies share pictures.

Known limit

Two copies of very large pages (over 6 MiB each) sent in the same batch share one budget, so the second is refused. "Keep mine" in the sync panel sends it again. It may need pressing twice, because a refused add is retried at the revision it was first sent with. I left the core queue alone for a case this rare.

Merge order

This PR contains every PR below it and needs the server ones deployed first. Order: #276 and #273 (server), #277, #267 (you look first), #268 and #272, #275, then this. #271 is closed: copies share pictures instead.

ci: test web analyze

🤖 Generated with Claude Code

SunkenInTime and others added 12 commits October 9, 2026 23:26
Lineup right-click menus gain Move to page and Copy to page, each listing
every other page. The lineups at the clicked spot go to that page under new
ids, with their names, notes and screenshots; a spot one of them shares with
a lineup that stays is copied, so that lineup keeps it. They go on the other
page before they leave this one. On cloud strategies the other page is read
first and the new groups are queued as work the canvas never drew, so they
are not deleted when they land while that page is on screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A move took the lineups off this page whenever they still existed after
the other page was read, so a teammate's edit made meanwhile was deleted
here while the other page got the older version. It now compares the
lineups and their spots with what was sent and keeps changed ones here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A local write that failed while putting lineups on another page threw
past the menu, so the user saw a generic error rather than being told
nothing was moved. It now reports notSaved. A move also compares the
lineups it sent by id, so the same lineups redrawn in another order no
longer count as changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hive can store a write and then throw while compacting its file, so a
thrown put does not mean the lineups are missing from the other page.
The box decides: if the copy is there, the move or copy went through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lineups at one spot share it, so they always make one group row. A
copy that would need more than one row is now refused before anything is
queued, so a failure after the first row can no longer leave part of a
copy on the other page while the toast says nothing was moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lineup menus offered Move to page and Copy to page with every page listed,
unlike every other placed item. They now get the same "Copy to next page"
and "Copy to previous page" items, in the same place and with the same
toasts. A copied lineup and its spots get ids that carry the originals'
(page_copy_id.dart), so a page that already has the lineup, or a copy of
it, is not offered and gets no other, locally and on cloud.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lineup imported with a very long id was offered "Copy to next page" on a
cloud strategy, and choosing it did nothing, because the copy's outbox key
would pass Hive's 255-character limit. The menu now leaves it out. A test
also pins that a throw spot whose other lineup is already on the next page
doesn't offer it, while that lineup still can go from its own landing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…up-page-move

# Conflicts:
#	test/strategy_page_session_provider_test.dart
A duplicate gave every item and lineup a fresh random id, so items the
source had copied between pages were no longer copies of each other in
the duplicate: its page transitions faded them instead of gliding. Each
now gets a copy id that keeps its root (lib/pageCopyId.ts, mirroring the
app's page_copy_id.dart), unless the root is too long for the app to
store a change under.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a local strategy "+" adds a copy of the page on screen; on a cloud
strategy it added a blank page, because the server keeps item ids unique
across every strategy. The new cloud page now gets a copy of every item
and lineup as the canvas draws them, each under a copy id that keeps its
root, so turning to the page glides rather than fades. Images have their
pictures copied first (images:copyAsset); one that can't be copied yet is
left out and the "+" caller says so. The page opens once the copies land,
waiting at most three seconds. A copied stroke now counts as its original
when deciding whether the drawing layer changes between pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 61e12a87-f0f7-45a9-b91c-524aed2f58b6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

SunkenInTime and others added 9 commits October 10, 2026 13:24
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Astra's review:
- "+" checked once for the new page's ack, so a busy queue or a dropped
  connection meant the page landed later, empty, with nothing said. It
  now waits for the page within the three seconds, keeping the queue
  sending; past them the copy follows once the page lands, and a toast
  says the page is on its way.
- Copies queue through the queue of the strategy open now, so leaving
  the strategy while one was copied sent the rest to the wrong one. Each
  copy now checks the strategy first, and a stopped copy says so.
- The three seconds didn't cover the send or the image picture copies.
  They now bound everything; an image that takes longer is left out.
- Lineup copy ids skipped the storage-length check items get.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Waiting for a late page in the background nudged the queue every half
second, which resets its own retry backoff while offline, and kept
polling after the provider was disposed. The background wait now only
watches, less often, and stops on dispose. The existing page-add test
now expects "+" to wait out its window when the server answers nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Astra's second review found the copy inferring that the new page existed
from the queue: an op leaves the queue when it lands, but also when it
is discarded, replaced, or not yet written, so copies could go to a page
that wasn't there. "+" on a cloud strategy now adds the page with a
direct server call (pages:add), retrying once after a teammate's change,
and copies onto it only once that call succeeds; offline, no page is
added and a toast says so. enqueueOffCanvas checks, as it writes, that
the strategy the copy belongs to is still the active one. The page read
before turning to the new page is bound by the same three seconds, and a
failed turn is logged rather than thrown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A queued page.add may name a page to copy. The server copies that
page's live items and lineups onto the new page, under copy ids that
keep their roots, in the same transaction that adds it, so the page
never appears without its content. A placed image still uploading is
left out; a page too large to copy refuses the add before anything is
written.

"Duplicate strategy" now copies through the same code (lib/contentCopy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"+" queues the page add with the page on screen as its source; the
server copies that page's content as it adds the page. Edits made just
before are queued ahead of the add. Once the page lands the app turns
to it, and says when images still uploading were left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SunkenInTime and others added 4 commits October 10, 2026 14:39
A batch is one transaction, so every page it copies now draws on one
budget: two large copies can no longer push the batch past Convex's
limits, where it would fail, and fail again on every retry. A lineup
image several pages show is charged to a copy once, as it is copied
once, so "Duplicate strategy" refuses no strategy it accepted before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"+" waits for its own add's answer rather than only the first send's,
so a page added behind other work still opens; with no answer in five
seconds it says the page will appear once it reaches the cloud. Edits to
the copied page the server refused are named, since they aren't in the
copy. Images left out are counted, not matched by id, as a copy of an
item with a long id gets a plain one. Switching strategies mid-add stops
it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SunkenInTime SunkenInTime changed the title Copy the page on screen when "+" is pressed on a cloud strategy Copy the page when "+" is pressed on a cloud strategy Oct 10, 2026
SunkenInTime and others added 2 commits October 10, 2026 14:54
Copies in one batch share a budget. A copy after it is spent failed only
after reading a row, so many copies could still read past Convex's
limit. A spent budget now refuses before reading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What the copy may lack is known when "+" is pressed, from the page on
screen: images still uploading, which the server leaves out, and edits
to the page the server refused, which it never had. Saying so then,
rather than guessing afterwards from what landed, holds whether the add
lands at once or later, and isn't fooled by a teammate's changes. The
wait for the server's answer now starts before sending, so a stalled
send still answers "+" in time. A page copy too large to make gets its
own message in the sync panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SunkenInTime and others added 7 commits October 10, 2026 14:58
Edits sent ahead of the add have their answers once the add does, so
"+" checks again then for refusals its copy won't have.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A copied placed image now names its source's picture (assetId) rather
than getting a picture of its own. Within a strategy nothing is copied
or uploaded, so "+" no longer leaves out an image still uploading: the
copy shows the picture whenever the upload lands. A copy into another
strategy ("Duplicate strategy") brings each picture once, under the same
id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copies show their original's pictures now, so "+" leaves no image out
and has nothing to warn about: a copy of an image still uploading shows
it once the upload lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant