GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — fix(files): stop the collaborative editor rewriting and reflowing a document on open by icecrasher321 · Pull Request #6652 · simstudioai/sim Conversation
|
| value | |
|---|---|
| cluster If-Match token | 1786594348911 |
file's content_updated_at |
1786594418409 |
| unpersisted entries in the stream | 103 |
The durable markdown froze there — and the editor's placeholder is built from it, so every reload painted the pre-edit document and the live one corrected it on screen. That is what still looked like a "replay" after the relay fix.
useStaleKeyRecovery was requested from inside the failing read's own queryFn — where react-query drops it. Instrumented: the recovery fired, and no fetch happened. So nothing re-resolved the record and the reader sat on a dead key showing "Failed to load file content" until something unrelated refetched. It now runs off that cycle and cancels a record read already in flight (which could only hand back the key we know is dead), and while the record is re-resolving the surface reports loading, not failure.
A document rebuilt from markdown is a different document to Yjs — its items carry new client ids — so a client still holding the old one merges the file into itself, twice, on both sides, and the relay persists that. Confirmed through the real converter. The rebuild happened whenever the stored binary was absent (a file opened but never edited had no stored document, so every cold open minted a new one) or stale (any external write).
The seed now stores what it builds and resumes it with a CRDT diff when the markdown moved on out-of-band, so there is only ever one. As a backstop the document carries an identity the join ack names, and a client offered a different one refuses to sync instead of merging — read-only until a reload, never corruption.
The inline route sent no-cache with no validator, so every open re-downloaded the whole image — ~1 MB per open of a real document, with the image area blank until it landed. A ?key= names one storage object and a content write never rewrites one (updateWorkspaceFileContent always mints a fresh key), so those bytes are immutable; ?fileId= names the file, whose bytes move under it, so that form still revalidates. Verified in a browser: inline FROM CACHE on repeat opens, previously 1012 KB every time.
collab-doc, workspace-files, copilot/tools/server/files, the file-viewer, the serve/inline routes and the query hooks.T|BBB|AAA|CCC both) where it previously revealed the pre-move order; a plain open no longer rewrites the file (byte-identical, key unchanged).tsc, check:api-validation, check:react-query, check:client-boundary, check:import-specifiers, biome — all clean.The first view of an image the browser has never fetched still reflows once: its box is reserved from stored dimensions, and those are written the first time the browser measures it. Fixing that means carrying the intrinsic size on the image node so the very first paint reserves the box — a schema change, deliberately not folded in here.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
PR SummaryHigh Risk Overview Relay ( Collab pipeline — CRDT seeds and agent writes use Client — Readiness closes on App polish — Workspace layout prefetches the file list for SSR/header parity; file detail route gets its own loading chrome; inline images keyed by storage object get long-lived private cache; Season Sans uses Reviewed by Cursor Bugbot for commit cd413c5. Configure here. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Greptile SummaryThe PR normalizes collaborative documents before seeding, strengthens relay hydration and persistence recovery, and improves file-route rendering and content delivery.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/realtime/src/handlers/file-doc.ts | Makes joins await complete stream hydration and memoized seeding while preserving authorization and room-lifecycle checks. |
| apps/realtime/src/handlers/file-doc-store.ts | Adds repeatable stream catch-up with numeric Redis stream-ID ordering and duplicate-entry bookkeeping protection. |
| apps/sim/lib/collab-doc/seed.ts | Persists and resumes stable collaborative document state rather than rebuilding a new CRDT identity. |
| apps/sim/lib/collab-doc/persist.ts | Avoids redundant durable writes and reconciles stale persistence versions when durable content still matches the relay projection. |
| apps/sim/app/api/workspaces/[id]/files/inline/route.ts | Applies long-lived private caching only to storage-key-addressed immutable content while file-ID requests continue revalidating. |
| apps/sim/hooks/queries/workspace-files.ts | Moves stale-key record re-resolution outside the failing query cycle and presents recovery as loading until re-resolution completes. |
sequenceDiagram
participant Browser
participant Sim as Sim App
participant Relay as Realtime Relay
participant Redis
participant Storage
Browser->>Relay: Join file document
Relay->>Redis: Catch up complete stream
alt Stream has no seed
Relay->>Sim: Request normalized seed
Sim->>Storage: Read durable markdown
Sim-->>Relay: Seed update and version
Relay->>Redis: Store seed document
end
Relay-->>Browser: Join success and complete Yjs state
Browser->>Relay: Collaborative updates
Relay->>Redis: Append CRDT updates
Relay->>Sim: Persist projected markdown with version
Sim->>Storage: Write new content object
Sim-->>Relay: Current persisted version
Relay->>Redis: Store synchronized version
Reviews (5): Last reviewed commit: "fix(files): name every collaborative doc..." | Re-trigger Greptile
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
361f0f7 to
9674e20
Compare
9674e20 to
105051b
Compare
|
bugbot run |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
105051b to
9a6c25d
Compare
|
bugbot run |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
9a6c25d to
6cb2a7b
Compare
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@cursor review |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 6cb2a7b. Configure here.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
…ocument on open Opening a file rewrote it. Binding an editor to a seeded document emits a Yjs update of its own — ProseMirror appends an empty paragraph to any doc that does not end in one — which the relay saw as a real edit and persisted. Every open therefore uploaded the file under a FRESH storage key and deleted the old one, 404ing the page's own in-flight content read, bumping "Last Updated" just from viewing, and churning a blob per open. Worse, a trailing blank line cannot serialize, so the file never recorded that paragraph and nothing reconciled the two: each client that seeded without seeing another's contribution stacked one more. A real document reached 18 against the placeholder's 1 — measured as the pane growing several hundred pixels the instant the live editor took over. - Seed and merge through the editor's own normal form (`editorNormalForm`), so binding is a no-op and `canonicalizeYDoc` collapses an accumulated run back to one. Placed at the collab boundary, not in `parseMarkdownToDoc`: only the CRDT has to agree with the editor — every other consumer of the parse renders through a real editor that normalizes itself. - Skip a persist whose projection already matches the durable bytes. Byte length is the free reject, so the compare read only happens when a no-op write is actually on the table. - Revoke collaborative readiness on a fatal join. The sticky `syncedOnce` latch outlived the document: after a readiness timeout the provider drops `synced` so the gate closes, but the latch re-opened it on the offline fallback's seed flag — handing back an EDITABLE editor on a document the provider had abandoned, with client autosave gated off because collaboration is nominally on. Keystrokes went nowhere and vanished on reload, with no error shown. - Recover from a superseded storage key instead of stranding the reader: a 404 re-resolves the file record, so the read re-keys onto the current object. And do not focus-refetch durable bytes while the relay owns durability. - Prefetch the workspace file list in the layout, where the sidebar already reads it. `HydrationBoundary` defers an already-seen query to an effect that SSR never runs, so a page-level prefetch of that key could not reach the server render — the file route rendered a spinner and disagreed with the client about the header's markup (a hydration mismatch). - Load the document font with `display: block`. A swap repaints prose in metric-adjusted Arial first, so paragraphs re-wrap when the real face lands. Also: a detail-route `loading.tsx` (the segment was inheriting the list chrome), `normalize.ts` renamed to `field.ts` now that it holds only the field constant, and the duplicate `COLLAB_DOC_FIELD` in the streaming path folded into it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…locking Opening a file right after an edit replayed it — the block you had just moved moved again, in front of you. A room rebuilds itself from the file's Redis stream one entry at a time, into the same Y.Doc that fans every update out to its room, and the join attached the socket before that finished and before the server seed. So a client was never sent the document; it was sent the document's history, and it watched that replay. The new join-readiness test reproduces it exactly — ['AAA', 'BBBAAA'] where one state was due — and fails without the fix. Underneath it, persistence had deadlocked. The If-Match token is a REMEMBERED timestamp: held in the room, which dies with it, and in a cluster key written fire-and-forget. A relay that exits in the moments after a successful write comes back holding a version older than the file's, every persist then fails the CAS, and because a conflict neither writes nor advances the token, that document can never be persisted again. Measured on a live file: token 1786594348911 against a content version of 1786594418409, with 103 unpersisted entries still in the stream. The durable markdown froze there — and the editor's placeholder is built from it, so every reload painted the pre-edit document and the live one corrected it on screen. - Assemble a room before attaching a client to it. The join awaits the stream catch-up and the seed, so the first sync IS the finished document, in one message. The seed is memoized on the room, so concurrent joins wait for the same one instead of the second being served an empty doc; a task that loses the seed lock PULLS the winner's seed from the stream rather than waiting for the tailer to push it, which is what left a freshly uploaded file read-only until its readiness deadline lapsed. - Drop the client-side quiet-frame gate that was standing in for this. It was unsound in both directions: it delayed a document that was already correct, and it opened mid-flight anyway whenever updates arrived more than a frame apart, which is what a cross-region Redis and a long room history produce. - Await the cluster version write after a successful persist. It is the only record that survives a teardown, and one round trip after a blob write is not a cost worth a wedged document. - On a version conflict, ask the CONTENT, not the clock. If the file still holds the bytes this document last projected — the tag written with every persist — then nothing out-of-band exists to protect, so re-sync the token and write. Any other bytes and the conflict stands exactly as before. - Actually run the stale-storage-key recovery. It was requested from inside the failing read's own queryFn, where react-query drops it, so nothing re-resolved the record and the reader sat on a dead key showing "Failed to load file content" until something unrelated refetched. It now runs off that cycle and cancels a read already in flight, which could only hand back the dead key. While the record is re-resolving the surface reports loading, not failure. - One document per file, for its whole life. A document rebuilt from markdown is a DIFFERENT document to Yjs — its items carry new client ids — so a client still holding the old one merges the file into itself, twice, on both sides. The seed now stores what it builds (until that row existed, a file opened but never edited was rebuilt on every open) and resumes it with a CRDT diff when the markdown moved on out-of-band. A document also carries an identity the join ack names, so a tab that outlived its room is refused rather than merged. - Let a browser keep an embedded image. The inline route sent no-cache with no validator, so every open re-downloaded the whole image — measured at ~1 MB per open of a real document, with the image area blank until it landed. A `key` names one storage object and a content write never rewrites one, so those bytes are immutable; a `fileId` names the file, whose bytes move, so that form still revalidates.
6cb2a7b to
35f1597
Compare
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
bugbot run |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@cursor review |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
…e URL names Two findings from review, both real. A document stored before identities existed is returned by the seed's fast path on every open, and that path never named one — so those files could never acquire an identity, and the join-ack guard could never fire for them. That is the population most likely to have a tab that outlived its room, which is the case the guard exists for. The fast path now names an unnamed document and stores it, once: minting without storing would name it differently on every open and the guard would start refusing clients that hold the very same document. The inline route marked a response immutable whenever the caller passed a key, but a key is resolved to a FILE and the file's current key is what gets streamed. A content write landing between those two reads would serve the new bytes under a URL naming the old object — and cached for a year, that is wrong forever. The flag is now what it always meant: the URL names the exact object that was streamed.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@cursor review |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit cd413c5. Configure here.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
cd413c59
Deployed
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.