oficina / docs Public

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
Paul Campbell pushed by paul@keymail.dev 855f95b5ad71f45d8ae8b03f52318b96c5334eba parent 79d6a75
53 files changed, +3914 −84
  • .amadan/ci.d/10-gate +45 −0
  • core/src/abi.rs +77 −3
  • core/src/fragment.rs +195 −0
  • core/src/lib.rs +6 −1
  • core/tests/fragment.rs +146 −0
  • core/tests/wasm_trap_containment.rs +17 −1
  • core/tests/wire_compat.rs +24 −1
  • fixtures/build-editor.mjs +54 −0
  • fixtures/editor-entry.js +298 −0
  • fixtures/generate-fragment.mjs +171 −0
  • fixtures/generate-seed.mjs +85 −2
  • fixtures/package-lock.json +653 −0
  • fixtures/package.json +16 −2
  • fixtures/resync-controller.mjs +111 −0
  • fixtures/resync-controller.test.mjs +155 −0
  • fixtures/save-queue.mjs +118 −0
  • fixtures/save-queue.test.mjs +96 −0
  • fixtures/vectors/fragment/concurrent.expected.json +3 −0
  • fixtures/vectors/fragment/concurrent.update.bin binary
  • fixtures/vectors/fragment/formatted.expected.json +3 −0
  • fixtures/vectors/fragment/formatted.update.bin binary
  • fixtures/vectors/fragment/hello.expected.json +3 −0
  • fixtures/vectors/fragment/hello.update.bin binary
  • fixtures/vectors/fragment/marks.bin binary
  • fixtures/vectors/fragment/nested.bin binary
  • fixtures/vectors/fragment/simple.bin binary
  • fixtures/vectors/fragment/unicode.expected.json +3 −0
  • fixtures/vectors/fragment/unicode.update.bin binary
  • internal/core/core.go +149 −10
  • internal/core/core_internal_test.go +100 −5
  • internal/core/core_test.go +70 −1
  • internal/core/testdata/docs_core.wasm binary
  • internal/docs/app.go +14 −1
  • internal/docs/assets.go +117 −0
  • internal/docs/assets_test.go +58 −0
  • internal/docs/editor_view_test.go +39 −0
  • internal/docs/handlers.go +216 −10
  • internal/docs/migrations/0003_doc_updates_seq_unique.sql +27 −0
  • internal/docs/render.go +39 −0
  • internal/docs/sample_documents_test.go +11 −6
  • internal/docs/seed_test.go +148 −11
  • internal/docs/seeddata/documentation.update.bin binary
  • internal/docs/seeddata/history.update.bin binary
  • internal/docs/seeddata/letter.update.bin binary
  • internal/docs/seeddata/meeting-notes.update.bin binary
  • internal/docs/seeddata/packing-list.update.bin binary
  • internal/docs/seeddata/proposal.update.bin binary
  • internal/docs/static/editor.15280b7eedd1868f.js +14 −0
  • internal/docs/store.go +103 −21
  • internal/docs/store_test.go +12 −3
  • internal/docs/testing.go +24 −0
  • internal/docs/updates_test.go +315 −0
  • internal/docs/views/show.html +179 −6

CI passed — run details

.amadan/ci.d/10-gate

Loading diff…

core/src/abi.rs

Loading diff…

core/src/fragment.rs

Loading diff…

core/src/lib.rs

Loading diff…

core/tests/fragment.rs

Loading diff…

core/tests/wasm_trap_containment.rs

Loading diff…

core/tests/wire_compat.rs

Loading diff…

fixtures/build-editor.mjs

Loading diff…

fixtures/editor-entry.js

Loading diff…

fixtures/generate-fragment.mjs

Loading diff…

fixtures/generate-seed.mjs

Loading diff…

fixtures/package-lock.json

Loading diff…

fixtures/package.json

Loading diff…

fixtures/resync-controller.mjs

Loading diff…

fixtures/resync-controller.test.mjs

Loading diff…

fixtures/save-queue.mjs

Loading diff…

fixtures/save-queue.test.mjs

Loading diff…

fixtures/vectors/fragment/concurrent.expected.json

Loading diff…

fixtures/vectors/fragment/concurrent.update.bin

Loading diff…

fixtures/vectors/fragment/formatted.expected.json

Loading diff…

fixtures/vectors/fragment/formatted.update.bin

Loading diff…

fixtures/vectors/fragment/hello.expected.json

Loading diff…

fixtures/vectors/fragment/hello.update.bin

Loading diff…

fixtures/vectors/fragment/marks.bin

Loading diff…

fixtures/vectors/fragment/nested.bin

Loading diff…

fixtures/vectors/fragment/simple.bin

Loading diff…

fixtures/vectors/fragment/unicode.expected.json

Loading diff…

fixtures/vectors/fragment/unicode.update.bin

Loading diff…

internal/core/core.go

Loading diff…

internal/core/core_internal_test.go

Loading diff…

internal/core/core_test.go

Loading diff…

internal/core/testdata/docs_core.wasm

Loading diff…

internal/docs/app.go

Loading diff…

internal/docs/assets.go

Loading diff…

internal/docs/assets_test.go

Loading diff…

internal/docs/editor_view_test.go

Loading diff…

internal/docs/handlers.go

Loading diff…

internal/docs/migrations/0003_doc_updates_seq_unique.sql

Loading diff…

internal/docs/render.go

Loading diff…

internal/docs/sample_documents_test.go

Loading diff…

internal/docs/seed_test.go

Loading diff…

internal/docs/seeddata/documentation.update.bin

Loading diff…

internal/docs/seeddata/history.update.bin

Loading diff…

internal/docs/seeddata/letter.update.bin

Loading diff…

internal/docs/seeddata/meeting-notes.update.bin

Loading diff…

internal/docs/seeddata/packing-list.update.bin

Loading diff…

internal/docs/seeddata/proposal.update.bin

Loading diff…

internal/docs/static/editor.15280b7eedd1868f.js

Loading diff…

internal/docs/store.go

Loading diff…

internal/docs/store_test.go

Loading diff…

internal/docs/testing.go

Loading diff…

internal/docs/updates_test.go

Loading diff…

internal/docs/views/show.html

Loading diff…