Edit a document
Type into a document, reload, and the words are there — while the same document still renders for a reader with no JavaScript. The CRDT root moved from a Y.Text named "content" to a ProseMirror-shaped XmlFragment named "prosemirror", which is what y-prosemirror requires. That reaches the Rust core, the ABI (now v2), the seeded documents, the read view and the wire gate. The core renders the fragment to text and to HTML so the server keeps rendering documents without running JavaScript; the browser loads a document's raw update log and POSTs edits back as Yjs updates, validated through the same core. Transport is plain HTTP, not a WebSocket: a socket is for live collaboration between people, and holding one per open editor is exactly what creates the hibernation problem in §7.3. The update-log format here is what a socket will stream. No pagination and no toolbar — show.html already sequenced pagination to a later layout plan. The Y.Text wire fixtures stay: they test the update ENCODING, which is the actual §0 contract and is independent of which shared type a document holds. The wasm sandbox stopped being a claim. §5.2 has argued it is a security boundary because yrs calls from_utf8_unchecked on wire data; until this branch nothing on the network could reach it. Measured: every single-byte flip of a valid fixture (all 291) POSTed through the real router — 194 accepted, 97 rejected, zero read or render errors after any accept. One flip triggered a genuine trap in yrs (panic_const_div_by_zero in block_store::find_pivot) and was contained: instance discarded, 400 returned, nothing persisted, document readable, no hang. Six bugs of consequence, all one shape — something reporting success about work that never landed — and all six found by using the app, none by the suite: - EditorView appends a sibling instead of replacing its container. - GET /updates returns concatenated updates and a single Y.applyUpdate decodes only the first, so a second edit vanished on reload. This needed TWO edits to appear; every single-edit test passed. - A failed flush retried forever and merged later edits into the doomed batch, so one rejected 1.2MB paste silently took everything after it. - Dropping a batch left the local doc diverged, so later edits returned 204 — success — while never integrating. - resync()'s own failure path could never retry, because a diverged doc produces 204s and never the 4xx that triggers one. A single transient failure recreated the silent loss for the whole session. - The beforeunload keepalive fallback shares sendBeacon's ~64KB quota and could not carry the case its comment claimed. 100KB refused, 30KB through. Each was reproduced before being fixed. The queue and resync state machines are extracted into modules with regression tests in the gate. Two defects were authored by the plan, not the implementation: a test that asserted nothing, and copy telling a person to "copy anything important before leaving this page" after resync had already wiped the screen. Known: the log grows until §4.1 snapshots land; the debounce move to 2s buys headroom to ~2h16m of continuous typing, labelled as headroom; the server can still 204 an update it cannot integrate; two tabs diverge until a socket exists. Discussion: https://amadan.net/oficina/docs/discussions/9
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011F5CivVm4CmL9ByfSSAaXA
53 files changed,
+3914
−84
.amadan/ci.d/10-gate+45 −0core/src/abi.rs+77 −3core/src/fragment.rs+195 −0core/src/lib.rs+6 −1core/tests/fragment.rs+146 −0core/tests/wasm_trap_containment.rs+17 −1core/tests/wire_compat.rs+24 −1fixtures/build-editor.mjs+54 −0fixtures/editor-entry.js+298 −0fixtures/generate-fragment.mjs+171 −0fixtures/generate-seed.mjs+85 −2fixtures/package-lock.json+653 −0fixtures/package.json+16 −2fixtures/resync-controller.mjs+111 −0fixtures/resync-controller.test.mjs+155 −0fixtures/save-queue.mjs+118 −0fixtures/save-queue.test.mjs+96 −0fixtures/vectors/fragment/concurrent.expected.json+3 −0fixtures/vectors/fragment/concurrent.update.binbinaryfixtures/vectors/fragment/formatted.expected.json+3 −0fixtures/vectors/fragment/formatted.update.binbinaryfixtures/vectors/fragment/hello.expected.json+3 −0fixtures/vectors/fragment/hello.update.binbinaryfixtures/vectors/fragment/marks.binbinaryfixtures/vectors/fragment/nested.binbinaryfixtures/vectors/fragment/simple.binbinaryfixtures/vectors/fragment/unicode.expected.json+3 −0fixtures/vectors/fragment/unicode.update.binbinaryinternal/core/core.go+149 −10internal/core/core_internal_test.go+100 −5internal/core/core_test.go+70 −1internal/core/testdata/docs_core.wasmbinaryinternal/docs/app.go+14 −1internal/docs/assets.go+117 −0internal/docs/assets_test.go+58 −0internal/docs/editor_view_test.go+39 −0internal/docs/handlers.go+216 −10internal/docs/migrations/0003_doc_updates_seq_unique.sql+27 −0internal/docs/render.go+39 −0internal/docs/sample_documents_test.go+11 −6internal/docs/seed_test.go+148 −11internal/docs/seeddata/documentation.update.binbinaryinternal/docs/seeddata/history.update.binbinaryinternal/docs/seeddata/letter.update.binbinaryinternal/docs/seeddata/meeting-notes.update.binbinaryinternal/docs/seeddata/packing-list.update.binbinaryinternal/docs/seeddata/proposal.update.binbinaryinternal/docs/static/editor.15280b7eedd1868f.js+14 −0internal/docs/store.go+103 −21internal/docs/store_test.go+12 −3internal/docs/testing.go+24 −0internal/docs/updates_test.go+315 −0internal/docs/views/show.html+179 −6
CI passed — run details
.amadan/ci.d/10-gateLoading diff…
core/src/abi.rsLoading diff…
core/src/fragment.rsLoading diff…
core/src/lib.rsLoading diff…
core/tests/fragment.rsLoading diff…
core/tests/wasm_trap_containment.rsLoading diff…
core/tests/wire_compat.rsLoading diff…
fixtures/build-editor.mjsLoading diff…
fixtures/editor-entry.jsLoading diff…
fixtures/generate-fragment.mjsLoading diff…
fixtures/generate-seed.mjsLoading diff…
fixtures/package-lock.jsonLoading diff…
fixtures/package.jsonLoading diff…
fixtures/resync-controller.mjsLoading diff…
fixtures/resync-controller.test.mjsLoading diff…
fixtures/save-queue.mjsLoading diff…
fixtures/save-queue.test.mjsLoading diff…
fixtures/vectors/fragment/concurrent.expected.jsonLoading diff…
fixtures/vectors/fragment/concurrent.update.binLoading diff…
fixtures/vectors/fragment/formatted.expected.jsonLoading diff…
fixtures/vectors/fragment/formatted.update.binLoading diff…
fixtures/vectors/fragment/hello.expected.jsonLoading diff…
fixtures/vectors/fragment/hello.update.binLoading diff…
fixtures/vectors/fragment/marks.binLoading diff…
fixtures/vectors/fragment/nested.binLoading diff…
fixtures/vectors/fragment/simple.binLoading diff…
fixtures/vectors/fragment/unicode.expected.jsonLoading diff…
fixtures/vectors/fragment/unicode.update.binLoading diff…
internal/core/core.goLoading diff…
internal/core/core_internal_test.goLoading diff…
internal/core/core_test.goLoading diff…
internal/core/testdata/docs_core.wasmLoading diff…
internal/docs/app.goLoading diff…
internal/docs/assets.goLoading diff…
internal/docs/assets_test.goLoading diff…
internal/docs/editor_view_test.goLoading diff…
internal/docs/handlers.goLoading diff…
internal/docs/migrations/0003_doc_updates_seq_unique.sqlLoading diff…
internal/docs/render.goLoading diff…
internal/docs/sample_documents_test.goLoading diff…
internal/docs/seed_test.goLoading diff…
internal/docs/seeddata/documentation.update.binLoading diff…
internal/docs/seeddata/history.update.binLoading diff…
internal/docs/seeddata/letter.update.binLoading diff…
internal/docs/seeddata/meeting-notes.update.binLoading diff…
internal/docs/seeddata/packing-list.update.binLoading diff…
internal/docs/seeddata/proposal.update.binLoading diff…
internal/docs/static/editor.15280b7eedd1868f.jsLoading diff…
internal/docs/store.goLoading diff…
internal/docs/store_test.goLoading diff…
internal/docs/testing.goLoading diff…
internal/docs/updates_test.goLoading diff…
internal/docs/views/show.htmlLoading diff…