app: edit a document — ProseMirror bound to the shared fragment
Binds ProseMirror to the Yjs XmlFragment (y-prosemirror's ySyncPlugin),
loads the update log on mount, and posts local edits back as
debounced, coalesced updates — dropped bytes go back on the queue on
a failed save, and a beforeunload beacon covers a close mid-debounce.
Two bugs found only by actually typing into the running app and
reloading (not by the test suite, which was green throughout):
- new EditorView(el, {state}) appends a second DOM node inside el
rather than replacing its contents (prosemirror-view only takes
over the given node's own children when passed {mount: el}), so a
click could land on the inert server-rendered read view instead of
the real editable surface. Fixed by mounting with {mount: el}.
- handlers.go's GET /d/{sqid}/updates streams the log as a raw
concatenation of one encoded Yjs update per accepted POST, with no
framing between messages. A single Y.applyUpdate call only decodes
the first message in that buffer — its decoder reads exactly the
client count declared in that message's own header and stops,
silently dropping every later message — so a second edit loaded
fine in memory during the session but vanished on reload. Fixed by
looping Y.readUpdate over one shared lib0/decoding decoder until it
is exhausted, replaying every message the log actually holds.
Also: views/show.html's existing [docs-text] container gets
docs-editor and data-sqid, plus a deferred module script tag for the
bundle — ProseMirror takes over that container's own contents once it
mounts, so the server-rendered HTML is what a scriptless reader sees
and what a scripted reader sees until the editor takes over
(pinned by editor_view_test.go's TestShowRendersContentWithoutScript).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011F5CivVm4CmL9ByfSSAaXA
fixtures/editor-entry.js+130 −24fixtures/package-lock.json+1 −0fixtures/package.json+1 −0internal/docs/editor_view_test.go+39 −0internal/docs/handlers.go+9 −1internal/docs/static/editor.283b4d376ff04289.js+14 −0internal/docs/static/editor.f13f5e0ce6fa7467.js+0 −14internal/docs/views/show.html+19 −1
Intent
<task-notification> <task-id>af1d2dca782186aa3</task-id> <tool-use-id>toolu_017GV37mof3YZVQUKaPLeTQs</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-carlos-docs/70f302d1-9d5d-4eb2-8b38-e09a15fdd1c3/tasks/af1d2dca782186aa3.output</output-file> <status>completed</status> <summary>Agent "Editing Task 5 update endpoints" finished</summary> <note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note> <result>## Summary Status: done. Commit `f1be04a` on branch `editing` (fix round 1, on top of `d1d0884`). `.amadan/ci.d/10-gate` exits 0. **(1) 400/500 classification** — `internal/core/core.go` gains `RejectedError`/`Rejected(err)` mirroring the existing `RecoveredError`/`Recovered(err)`. `store.go`'s `ApplyUpdate` wraps only the *new* update's `h.Apply` error in an unexported `*updateRejection` (a replay failure calls the same `core.Handle.Apply` and would otherwise be indistinguishable). `handlers.go` requires that wrapper *and* `Recovered`/`Rejected`/`ErrUpdateLogFull` before returning 400/Info; everything else is 500/Error with the doc id. Induced a real 500 (`TestInternalApplyFailureReturns500NotBadUpdate`) by writing a garbage row directly into `doc_updates` so the next POST's replay genuinely fails through the real wasm core — no mock. **(2) seq race** — chose "compute and insert inside one transaction" over retry-on-collision: `rastrillo/db`'s write pool caps at one SQLite connection, so `gorm.DB.Transaction` makes a racing request block on `Begin` until the first commits, closing the race by queuing rather than racing-and-retrying. Verified this matters: a bounded 5-attempt retry-loop version I built first genuinely exhausted its budget and returned real 500s under a 21-goroutine race, while the transaction version passed 15/15 clean runs. Added `UNIQUE(doc_id, seq)` (migration `0003`) as a backstop, safe to apply as-is since no live database was ever exposed to the vulnerable code. Confirmed `TestConcurrentUpdatesDoNotCollideOnSeq` fails against the pre-fix two-statement code: reverted locally (never committed) and it failed 3/3 runs (rows going missing / genuine 500s) before the transaction fix, 15/15 clean after. One correction en route: my first race-test draft posted 7 different fixtures concurrently and hit real, unrelated core rejections, because every fixture pins the same `clientID` for reproducibility, making them structurally incompatible when layered together — switched to reposting one fixture N times instead. Full report: `/home/paulca/amadan.net/carlos/docs/.superpowers/sdd/2026-08-31-editing/task-5-report.md`</result> <usage><subagent_tokens>296361</subagent_tokens><tool_uses>90</tool_uses><duration_ms>1082797</duration_ms></usage> </task-notification>
fixtures/editor-entry.jsLoading diff…
fixtures/package-lock.jsonLoading diff…
fixtures/package.jsonLoading diff…
internal/docs/editor_view_test.goLoading diff…
internal/docs/handlers.goLoading diff…
internal/docs/static/editor.283b4d376ff04289.jsLoading diff…
internal/docs/static/editor.f13f5e0ce6fa7467.jsLoading diff…
internal/docs/views/show.htmlLoading diff…