Skip to content

feat(runtime): write coalescing — durable intent buffer, merged commits, converged settlement #146

Description

@FelineStateMachine

Context

The CRDT store is append-only: every committed write is permanent history, so fine-grained writes to large values grow quadratically (a keystroke-bound update of a growing 5 KB note costs ~4 MB of history for one note; a naive notes app reaches GB in a night). The journal/effects layers are self-pruning and are not the growth vector. The point of no return is commit — so growth control lives in front of it: opt-in write coalescing that merges rapid verb calls into one committed write.

1. API — an opt-in decorator per verb

export const editNote = s.coalesced(
  s.mutation("editNote", app.schema.notes.update, { effects: [...] }),
  { window: "2s" },
)
  • Opt-in per verb, never default. The wrapping form keeps coalescing visible at the declaration and off the core s.mutation signature
  • Author contract (the opt-in's precondition, documented and cheap-checked at runtime): coalesceable patches are absolute — static set-column-to-value objects, never read-modify-write — so replay after a crash is sound regardless of what synced in between

2. Durable intent buffer (not a source of truth)

Verb calls append to a durable staging log immediately; the coalescer merges per (row, verb) and commits one write when the window closes. Strict lifecycle: append → (replay on boot if a crash left staged intents) → commit merged → delete entries at local durability. The store remains the only source of truth; the log is an outbox buffer and is never read as app state.

  • Storage: OPFS (same driver hierarchy as the journal), NOT localStorage
  • Encryption spike (load-bearing unknown, do first): staged values must be encrypted at rest — plaintext staging would reverse the journal's no-plaintext decision. Determine whether account-derived key material is reachable from the runtime; if not, this feature ships in-memory-only (window-bounded loss on crash, like any debounced editor) until it is
  • Boot replay of staged intents, merged and committed once

3. Settlement convergence

  • All WriteHandles from coalesced calls resolve with the merged write's fate; one journal entry; effects fire exactly once, for the merged write — intermediate calls have no effect-level identity (documented contract: effects see final merged state)
  • Intent-log entries are marked resolved against the merged write id; usePendingWrites surfaces the coalescing state honestly (N calls, one pending write)
  • Rejection: the merged write fails as one unit — one rejected, one onRejected; the engine's own rollback covers row state. No serial re-send fallback (rejection is not caused by coalescing; un-coalescing cannot cure it and would manufacture N rejections and N× history). Known limitation, documented: a merged patch spanning columns with mixed policy outcomes fails whole

4. Flush barriers and multi-tab

  • Flush triggers: window expiry; any non-coalesced write to the same row (ordering barrier — an edit must never commit after the delete it preceded); explicit flush; pagehide/visibilitychange (shrinks the crash-loss window for tab-close to near zero)
  • Multi-tab: serialize per-row buffers with the existing scope-keyed Web Locks convention so two tabs never double-commit interleaved history

5. v2, designed but deferred: offline-length staging

Extending the window while unsynced would collapse an offline editing session into one commit at reconnect (near-SQLite growth offline). Gated on a real gap: staged values are invisible to live queries, so long windows require overlaying staged state onto query snapshots — significant machinery. v1 is short-window only; the overlay design is the entry criterion for v2.

6. Docs and diagnostics

  • Nouns-and-verbs guidance: a verb is an intent, not a keystroke; when to opt into coalescing; the absolute-patch contract; the effects-see-final-state contract
  • Dev-mode write-rate diagnostic: surface (never block) verbs writing many times per second to the same row
  • Reference app: the notes-style flow demonstrates a coalesced verb

7. Measurement spike

  • Golden-style test scripting an edit session (coalesced vs uncoalesced vs debounced) measuring actual OPFS bytes — establishes the real bytes-per-edit constant for the docs growth guidance

Out of scope

  • History compaction of already-committed transactions (signed hash chain; no vendor mechanism at the pinned alpha) — recorded as an upstream wish
  • Collaborative text columns (s.text delta CRDT) — the per-datatype answer to keystroke granularity, tracked with the schema follow-ups

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions