Skip to content

Components (L2)

1. The shared predictor crate (posthaste-replica-core)

The pure local effect lives in posthaste-replica-core — no I/O, compiles to wasm32-unknown-unknown. It is the narrow, domain-free effect-fold leaf below the domain crates: the local effect apply(state, mutation) -> state over the minimal fold records (MessageFoldState, MessageAssertion), the rebase primitive (replay), the coalescing rule, the convergence engine (MessageReplica: accept / apply_base_update / settle / project), and MutationId — the one wire mutation id (no serde mirror of it exists in any other crate). The operation vocabulary does not live here: the typed MailOperation (with MutationRequest/MutationReceipt) is wire vocabulary above the domain types, defined once in posthaste-contract-core.

The minimal MessageFoldState predictor is load-bearing for decoupling: it keeps the fold path domain-free and avoids a domain → replica-core → domain cycle — posthaste-domain-service calls the same predictor for its read-time overlay fold in message_queries, so there is one predictor across deployments (single-local-effect); a hand-written TS/JS reimplementation is prohibited. Only the wasm binding pulls domain types, via the contract crate — never through the fold path. Retirement is per-mutation settlement.

Assertion mutations (set keywords, replace mailboxes, destroy) carry a desired state and fold idempotently; coalescing collapses successive assertions on one entity to the latest, a cancelling pair to nothing. Entity-creating mutations (draft create/update, send) hold a provisional entity under a near-minted id and reconcile to the authoritative id on settlement, deduping by authoritative id so a provisional row and its confirmed row never both show.

2. The replica node (posthaste-replica-projector)

posthaste-replica-projector's MailListReplica is the working-set view layer: ingest served rows as the base, fold the pending set over them, project optimistic rows with a membership predicate. It runs as WASM in the browser and natively in the runtime over the authority-server link (authority-server-link L2 §5) — one replica, two consumers (one-replica-both-seams). The replica is not an authority: no provider gateways, no automation, no supervisor. It predicts the local effect and lets the authority's base correct it.

3. The WASM boundary (posthaste-client-node-wasm)

EntityStoreHandle (posthaste-client-node-wasm, crates/posthaste-client-node-wasm/src/entity_store.rs) is the JS-facing surface over the multi-view entity store (mechanism + projection, §2), deliberately narrow, with values crossing as JSON strings:

  • new() — an empty store; views are registered, not fixed at construction (multiple concurrent views — mail lists, and any other keyed view — share one store).
  • registerViewJson(viewId, argsJson) / closeView(viewId) — open/close a view's predicate + sort + coverage.
  • setViewRowsJson(viewId, rowsJson, watermarkJson) — reconcile a served snapshot into a view (never clobbers optimistic placement; §2's set_view_rows).
  • ingestBatchJson(batchJson) — adopt served message/mailbox bases.
  • acceptMutationJson(acceptJson) — accept an optimistic mutation.
  • settle(mutationId, "confirmed"|"failed") -> bool — retire a pending mutation; true means a failure reverted optimism.
  • hasPending(), messageJson(id), mailboxJson(id), viewRowsJson(viewId), projectViewJson(viewId) — reads over the current optimistic/served state.
  • drainDirtyJson() / drainRetiredJson() — the host's re-render and pending-set-eviction signals.
  • captureMutationDiffJson(...) — the undo/redo diff capture hook (Phase 2).

The handle is pure compute: no transport, no storage. JSON-string passing keeps the dependency to wasm-bindgen alone and the type contract inspectable. The TS wrapper is apps/web/src/runtime/replica/handle.ts; entityStoreAdapter.ts drives it as a RuntimeAdapter.

4. Delta computation and reconciliation

On a served-view change the runtime recomputes the snapshot and, for a link that opted into view_delta, computes the row diff (crates/posthaste-runtime/src/far_end/links.rs::mail_list_delta) and emits RuntimeFrame::ViewDelta instead of a whole ViewReplace for row-local changes (flags, reads, removals). The client applies the delta through the replica: the entity-store replica is the sole owner of the mail list, ingesting the changed rows into the WASM handle and re-projecting (apps/web/src/runtime/replica/entityStoreAdapter.ts); no renderer hook consumes deltas directly.

The runtime still computes the snapshot by re-querying then diffing; the incremental recompute that avoids the per-event re-query is not yet built (see L3 §5).

5. Working-set coverage

The replica holds exactly the bases for the views the renderer currently has open — the mail-list window (with its continuation cursors), open details, open conversations — plus referenced records, not the account. Each served snapshot carries its coverage (the mail-list continuation; a detail/conversation snapshot covers exactly its object), which the replica records to distinguish "absent because unchanged" from "absent because not held." Acquisition is renderer-driven (open a view, extend the window, navigate); there is no background prefetch in this layer.

6. The entityStoreAdapter

entityStoreAdapter (apps/web/src/runtime/replica/entityStoreAdapter.ts) is a third RuntimeAdapter backed by the WASM handle plus host-owned transport and persistence, selected behind VITE_RUNTIME_REPLICA (default off) alongside httpAdapter. The renderer is unchanged: a mailList view's frames are produced locally by projectJson after each accept/settle/ingest, while non-replicated surfaces pass through. Parity tests assert the entity-store adapter yields the same observable frames as httpAdapter for the covered flows.

7. Assertions

ID Sev. Assertion
predictor-single-crate MUST The local effect lives in one crate (posthaste-replica-core) used by both the authority server and the replica; no second predictor implementation exists. The typed operation vocabulary lives once in posthaste-contract-core, above the domain types — not in the effect-fold leaf.
predictor-wasm-clean MUST posthaste-replica-core compiles to wasm32-unknown-unknown with no I/O, native time, or database dependencies.
one-replica-both-seams MUST The client and the runtime fold the same posthaste-replica-projector/posthaste-replica-core components; neither reimplements the projection.
replica-rebase-only MUST The replica applies authority updates by replace-base / retire-on-settlement / recompute; it never patches a derived view or sends cross-layer invalidation.
replica-working-set MUST The replica holds only the bases for currently open views plus referenced records, with per-view authority-authored coverage; it never mirrors the whole account.
wasm-boundary-json MUST The replica's only JS surface is EntityStoreHandle over JSON-string values; it performs no transport or storage itself.