npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@loro-dev/roost

v0.3.0

Published

Browser conversation history and sync for Loro Roost

Readme

Loro Roost for TypeScript

Experimental TypeScript package with IndexedDB storage, a chunked Turn state machine and experimental v1 codec/cryptography. It supports local writes, remote import, seal verification, pending dependencies, outbox and stream-scoped change hints. The sync entry provides explicit Streams authentication and host payload crypto. Lody runtime integration, device support and stable APIs remain separate acceptance work. Wire v1 and storage layout can change. Unsealed updates are provisional and unsigned; production E2EE is not implemented. Author equivocation has no convergence guarantee. The Inbox module adds atomic raw receipt/cursor storage, without authenticating or applying those bytes.

npm install @loro-dev/[email protected]

This source targets the coordinated Rust/TypeScript/Native 0.3.0 release; the registry determines npm availability. Native SQLite storage in this release accepts fresh/v10 containers only. Preserve older databases with their compatible runtime; there is no automatic migration or deletion.

For source development and browser tests:

pnpm install --frozen-lockfile
pnpm check
pnpm test
# Only one engine, when installed:
BROWSERS=chromium pnpm test
# Install test browsers if this machine does not already have them:
pnpm exec playwright install chromium webkit

Tests launch genuine Chromium and WebKit against a loopback-only fixture server and fresh browser contexts. There is no fake-indexeddb, external service, sleep or timing race. A test deadline only terminates failures; synchronization depends on actual request/transaction events. WebKit desktop tests are not iOS device tests.

Transaction API

import { IndexedDbStorage } from '@loro-dev/roost';

const storage = await IndexedDbStorage.open('roost-example');
const key = new Uint8Array([1]);
await storage.transact(true, function* (tx) {
  const previous = yield* tx.get('projection', key);
  yield* tx.put('projection', key, new Uint8Array([previous?.[0] ?? 0, 42]));
  yield* tx.put('outbox', key, new Uint8Array([42]));
}); // Resolves only after IndexedDB's complete event.
await storage.close();

transact(write, program) invokes the synchronous generator once, with no retry. yield* tx.get/put/delete/scan resumes in a request event while IndexedDB permits new requests. This low-level generator contract is deliberately different from Rust's synchronous callback. The Store message API exposes ordinary Promises above it. Do not await network, worker computation, timers or unrelated Promises in a transaction. Programs must do finite synchronous computation; no execution-time or memory budget is imposed here. Empty-scan chains use an iterative driver, not recursive callbacks. Compute outside and recheck any dependent versions in the transaction. No keepalive requests are issued.

The transaction methods use arbitrary byte keys and byte values with a string namespace. get returns undefined for absence; put replaces; delete of an absent key succeeds. scan(space, start, end, limit) uses byte lexicographic order, inclusive start and exclusive end; zero limit or reversed/empty intervals return no rows. Pass undefined for no end; the limit is always explicit. Keys and values are copied, including offset views. Inputs are snapshotted when an operation is created, not when it is eventually yielded.

All namespaces share one private IDB object store with [namespace, binaryKey] keys. Thus projection/oplog/outbox/cursor namespaces can commit or abort together, without defining an object store for each namespace. This is local layout v1, not a new Roost wire/schema version or Rust RSP1 interchange format. Readonly transactions reject mutations. Overlapping writes from different connections are serialized by IndexedDB, including read-modify-write programs.

A thrown exception, tx.abort() (even if caught), native request failure or invalid yield aborts the transaction. Only opaque, single-use operations belonging to that transaction are accepted; promises, async generators and borrowed operations fail. Native abort rejects after rollback even when earlier requests succeeded. A request error is fatal; the generator cannot catch and recover that individual request. Retained transaction views cannot be used after the program.

close() immediately rejects new work and waits for already-created transactions to complete or abort. It is idempotent; existing work is not cancelled. A versionchange event initiates close automatically, allowing another connection to upgrade. Version 1 must contain exactly the expected key/value object store; unknown versions or layouts are refused without resetting user data. Blocked open fails explicitly, and a subsequently opened connection is closed instead of leaked. There is no migration, cross-tab broadcast, quota recovery or data deletion API.

Writes request IndexedDB's strict durability hint. complete proves the browser's commit boundary; it is not proof against device failure, browser storage eviction, quota exhaustion or an OS wiping this origin. Pending messages and keys still need product-level retention and recovery policies.

References: W3C transaction lifecycle, transaction scheduling. Owner: P1-03.

Durable receipt ledger

Inbox.open(storage, stream, source, limits) shares the same IndexedDB backend. Use receive(previousCursor, nextCursor, bytes) to commit raw data and its receive position together. null is the initial cursor; an empty byte cursor is distinct. Exact historical retries return duplicate without rewinding the head; stale predecessors and conflicting cursor reuse fail. Sequences and retained-byte quotas are bigint values; lengths and page sizes must be explicit safe integers. state() and records(afterSequence, limit) expose committed local receipts.

Source names are routing metadata, not identity proofs. No authentication, Roost import, verification or transport is performed. Record/index bytes count towards a per-namespace logical quota; physical storage and the total number of namespaces need host policy. There is no GC or implicit reset. The browser-private layout does not share Rust inbox state records.

Experimental v1 codec

encodeBatch / decodeBatch in codec.ts implement current Rust v1 bytes. JsonValue in json.ts uses Map for objects, explicit bigint u64/i64 integers and an explicit f64 variant (including negative zero). Native JS numbers and plain objects are not implicitly converted; the eventual application JSON API is not settled. UTF8 and surrogate validation is strict, scalar-key ordering is canonical, and decode refuses noncanonical spellings rather than repairing them.

Codec acceptance is syntax only: it does not authenticate the source, validate a seal, apply a Turn or enforce business ownership. Browser tests build the Rust codec_interop example and compare real Rust/TS bytes in both directions. The fixture includes all operations and finite floating-point samples, not an exhaustive IEEE proof. Running these tests requires the root Cargo dependencies to have been fetched; the oracle build uses --offline --locked.

Hash, signatures and IDs

crypto.ts provides nodePreimage, nodeHash, isValidOwner and verifySealSignature; identity.ts provides an in-memory Identity signer and nextTurnId with explicit clock, observed maximum and random-source inputs. The strict signature wrapper uses maintained noble primitives to match Rust dalek verification; noble's default verifier alone has different acceptance rules. A decodable public key is not an authorized device. A valid signature over a claimed hash is not proof that received content matches that hash.

Fixed vectors and strict counterexamples run in both browsers. These helpers do not persist keys or authenticate transports. The Store below persists admitted observed maxima atomically with message state.

Message Store

new Store(storage, allows, {clock, randomBytes, verifyHashIntegrity?}) shares the same IndexedDB owner as Inbox. Clock returns bigint milliseconds; randomBytes returns 16 bytes. Both callbacks are explicit, synchronous host inputs. The allowlist callback checks a claimed owner, not proof of authorship. The user accepts claimed-author spoofing during streaming; per-device source authentication is not a prerequisite for importing provisional updates. Service access controls remain.

store.stream(id) offers writer(identity), importBatch, readTurn, readTurns, scanTurns, readValue, readText, snapshot, eventsAfter, pendingTurns, missingDependencies, exportUpdates and exportTurns. Writer create, edit, seal and writeBatch return committed update references. Sequences, text positions and array positions are bigint. Text positions count Unicode scalars. Objects and numbers use the lossless JsonValue representation above; no native JS object/Number coercion is implied.

Local batches commit content blocks, Turn headers, operation log, pending indexes, observed ID maximum, outgoing log and events in one transaction. A failed later operation rolls back earlier operations. Import isolates semantic frame rejection, but storage failures abort the entire batch. Remote import never echoes to outbox. Hash recomputation defaults on; disabling it never disables seal signature checks. Corrupt persisted metadata is an error, not an empty or repaired Turn.

Text uses 1024-scalar blocks and arrays use 128-reference pages. Tail append and text ranges access necessary blocks; variable-length middle splices may rewrite the affected suffix. Objects retain field tables. Whole snapshots and seals may read complete content. Stored bytes/depth summaries enforce the complete Turn budget before writes, including Rust-compatible metadata overhead.

subscribeOutgoing(consumer).nextBatch(limits) returns bytes plus an opaque, Store/stream/consumer-scoped receipt; ack(receipt) advances that consumer only. An unacknowledged prefix survives reopen. When a durable sender has pinned a batch, ordinary ack cannot advance past it; use sender confirmation.

watch() registers a subscription before taking its consistent snapshot/event cursor. Hints coalesce; use the durable event log or reread state after a hint. Hints are local to this Store, not cross-tab broadcasts. Close subscriptions when finished. store.close() rejects new work, releases subscriptions and waits for started transactions; it does not close the shared IndexedDB owner. After all clients (including Inbox) finish, call storage.close() separately. Host callbacks cannot reenter or close Store, and cannot return Promises.

Scope/evidence: projection, state machine, real SQLite/browser interop.

Durable one-step sender

new DurableSender(outgoing).sendNext(limits, async bytes => { ... }) first pins an exact batch in IndexedDB, then invokes the host outside a transaction. A fulfilled callback asserts that the remote endpoint durably accepted those bytes under the host's transport contract. Only then does a transaction advance the consumer and remove the pending pair. The helper itself cannot prove remote durability or identity. It is not a background network loop.

After failure or reopen, the same consumer retries the original bytes even after new edits or larger limits. Limits too small for the pinned batch fail without sending. Concurrent senders may deliver duplicates; a late confirmation cannot remove a later pinned batch. Remote acceptance followed by local confirmation failure retains a safe duplicate retry.

The callback receives a copy. Visible mutation or transfer/detachment followed by success causes a local failure, retaining the original batch. Callback throws stay transport failures with their original cause, including non-Error JS values. SendError.kind distinguishes local from transport; neither means an earlier remote write was rolled back. Stop the host loop and await in-flight sendNext calls before closing shared storage. Store.close does not wait for external transport; confirmation after close fails locally and the pinned batch survives restart.

Evidence: browser sender Owner.

Durable replay and batch framing

DurableReplay.open(inbox, stream, limits).step() processes one complete RST1 batch receipt. Import commits first; its report and processed position commit together afterwards. A crash between them safely repeats import. Quota/storage failures keep the receipt for retry. Processed does not mean applied or verified. Use the exact same storage owner and stream, and await outstanding steps before closing storage. Original receipts and reports are retained; there is no GC.

BatchFramer in framing.ts accepts windows up to 64 KiB and returns at most one complete batch per push, with the number of consumed bytes. It handles fragmented and concatenated batches without changing payload bytes. Call finish only at final EOF, not on cancellation or a temporary empty response. It is an in-memory parser; durable tail handling is provided by DurableFraming below. The actual Streams adapter remains unfinished. Never pass arbitrary HTTP chunks directly to DurableReplay.

Evidence: replay, framing and remaining work.

Durable fragmented receipts

DurableFraming.open(rawInbox, batchInbox) uses two namespaces on the same storage owner and logical stream. Call step() to process at most one 64KiB window or receipt transition. restoring rebuilds an unfinished batch after restart; progress and batch mean more work may remain; idle means no currently saved raw bytes remain to scan. invalid preserves the bad tail and stops.

The completed batch and scan checkpoint commit in one transaction. Use batchInbox with DurableReplay, never rawInbox. Original raw offsets remain opaque; derived cursors are explicitly local receipt/byte positions. Quota or native transaction failure leaves the checkpoint for retry. Await an active step before closing the owner; overlapping calls on one bridge return busy. Only call finish() after confirmed final remote EOF, repeating until end/invalid. New raw data after end is an explicit consistency error, not silently ignored.

No background transport, automatic repair or GC is included. A restart may reread one unfinished batch; the first raw record read still loads its complete body. Implementation and real-browser evidence.

Consumer entry points and pinned SDK

Import the experimental application surface from @loro-dev/roost and synchronization from @loro-dev/roost/sync. The business adapter is @loro-dev/roost/lody-history. Storage keys and internal record DTOs are not application exports. Pin the exact artifact until the Lody consumer contract is accepted. Content uses lossless JsonValue; ordinary JavaScript Number/object conversion is not implicit.

Link.uploadCycle runs durable discovery before flushing target work. Executor.discoverCycle reads local Changed events, so applied P2P/server imports with older Turn IDs are discovered after restart without a discoverTurns call. Tasks and each target's event checkpoint commit together; target coverage remains the confirmed lower bound of exact operations, and imported updates do not enter the core outbox. Discovery page size bounds each page, while maxSend bounds only sends. Partial replicas keep directed Link.bridgeTurns and its waitingParent result for missing dependencies. See the discovery and upgrade contract.

Browser HistoryReader.fetchWindow(source, budgetBytes, pageLimitBytes, stop, { before, completePrefixes: false }) resumes at the previous window's complete message boundary. This option stops at the byte budget without filtering extra prefix-repair pages into the window range. Use explicit completePrefixes to repair pending window turns. Persist the oldest contiguous boundary only after intake commits, and initialize Link.catchUpAt from the original window tail, never a later head() result. The Node window API is unchanged.

pnpm pack:consumer -- /absolute/output/directory builds the npm tarball and checks its public entries. It depends on the exact registry release @loro-dev/[email protected], with no bundled SDK. pnpm check:consumer installs that tarball into a fresh project with a fresh npm cache and checks the root, ./sync, and ./lody-history exports.

IndexedDB container version is now 2. Opening a valid version-1 database upgrades only its version, retaining all records; incompatible layouts abort the upgrade. Older version-1 openers are refused. Existing connections must close on versionchange; a blocked upgrade is an error, never a reason to delete data.

The experimental ./lody-history entry provides the shared browser/Node business envelope. NodeLodyHistory persists business bindings, diagnostics and its processed local event cursor through the single SQLite owner. Each page consumes at most 128 events and atomically commits its derived rows and cursor, comparing the prior cursor to reject stale concurrent work. Identity lookups use database keys; normal reopen processes only the event backlog. Existing databases without this index need one resumable initial rebuild. First-page reads still wait for outstanding indexing, so their cost includes that backlog, not only the returned page.

Browser and Node batch recovery scan a persistent pending-receipt index instead of every completed receipt. P2P imports and delayed pending application enter the same local event log, even for old TurnIDs; these private indexes and cursors are not copied between peers. Imported updates still do not enter the core outbox automatically. Concurrent plans retain their original processed cursor, retrying when intervening events invalidate their absence/revision reads. Neither this entry nor the generic package automatically replaces Lody runtime storage.

commitHistoryBatch(batchId, commands, {recovery: 'local'}) explicitly keeps pending command bodies in the authoritative lody_history_local_batch_plans_v1 journal of that owner database. Preparation creates the compact pending receipt and journal together. Apply commits all effects, publication references, receipt seal and an empty journal tombstone together. A lost reply therefore preserves either recoverable pending commands or the original committed outcome. Committed receipts and their retained updates contain no command bodies. New batches also omit unused baseline snapshots in both modes. No historical signed data is pruned.

The default remains recovery: 'replicated', preserving recovery commands in the receipt. Retries keep the first preparation's recovery mode. Local mode requires backing up the whole owner database to move unfinished work; exporting Turns does not export its journal. A same-owner imported pending local receipt is observed without executing or failing it, and an explicit retry of that unfinished batch requires its originating database. Committed publication still rebuilds from the verified seal and target references on other replicas. Stop older SDKs before writing local-mode pending work; use matching browser/Node revisions. This option changes the business adapter, not core wire framing or seals.

Imported messages now rebuild local publication membership during read-side catch-up or recoverPendingBatches() (which returns Promise<void>). New direct messages carry original Create intent; batch members and explicitly promoted private messages use verified sealed publication receipts. A receiver can derive these indexes without an admitted local signing identity, and never replays a remote author's pending commands or adds imported operations to outbox. Missing targets/seqs wait in a persistent reverse index. One receipt's previously hidden members become visible together; ordinary P2P edits to already visible messages still follow their applied prefixes. See the format and legacy compatibility contract.

The independent publication cursor performs a one-time, resumable catch-up for existing databases; initial reads wait for it, so the upgrade can cost a history scan. Subsequent reopens consume only new local events. Old committed receipts are supported, including zero-ID placeholders, but an old standalone sealed message cannot reveal whether its author chose publish:false. It stays hidden on a new receiver until the original author repeats accept with its original input and synchronizes the resulting signed publication Turn. New hidden messages also stay hidden until explicitly promoted; sealing alone does not publish them. Recovery does not change a message's verification level. Implementation evidence: publication recovery plan.

Published message pages use the publication index for membership/order and batch read current Turn bodies. append and finish do not maintain a second body snapshot. A local event-cursor fence retries reads that overlap a commit, so a page cannot combine membership from before a batch with content from after it. Already published incomplete Turns expose their applied prefix. Unsealed content remains provisional; the projected sealed flag is not a verification level.

Node active-branch pages privately reuse sealed bodies after matching authoritative headers, including sequence, seal and parent edges. The per-history cache keeps at most 64 bodies and 8 MiB of source JSON UTF-16 bytes (an input budget, not exact heap usage). Oversized and unsealed bodies are not retained. Returned bodies are independent copies. Page membership, cursors, availability and event fences are still checked on every read; first hydration performs the usual JSON validation.

After Node binding/publication indexes settle at one event cursor, repeated catch-up calls still read the journal directly. They reuse the settled checkpoint only if that fresh read finds no event. New events or failed reads retain normal indexing, dependency wakeup and retry behavior. A publication page that adds no work may commit its cursor and ready marker together, with the old ready/revision in the same CAS; pending or newly discovered work keeps the full drain path. observedEventCursor() returns that private settled cursor after the same fresh event check; it needs no second persisted-cursor read. A failed event query still rejects, and a new history handle must establish its own checkpoint.

With a matching derived-indexes-v1 Native host, NodeLodyHistory.commitContentBatch can commit edits, application receipts and private SDK checkpoints in one durable transaction. It only plans a fast checkpoint for content-subtree edits of at most 16 already published message Turns, with settled binding/publication checkpoints, matching Create/binding/member records and no waiting target dependency. Other writes keep ordinary catch-up. Callers may provide derivedIndexPuts for their own cursor-fenced projections; the SDK owns all private table names and formats. Native independently checks the entire actual event interval before installing any derived put. derivedIndexesApplied is false when that proof fails; complete catch-up still processes all events. SDK reuse is installed only after a confirmed matching reply, and never after a lost reply. This is an additive local API; subprocess/browser hosts retain their existing paths and advertise no new support.

After confirmed application, Node retains one private checkpoint plan (at most 16 IDs and 256 KiB). Each subsequent eligible write still compares every original proof row inside Native; only the committed progress values advance. An index-only CAS change refreshes the plan once after Native proves the attempt committed no write. Unknown outcomes clear reuse and never trigger an automatic retry. Fresh plans are detached once from host index results and command IDs before reuse. Every host call receives its own copied proof; subsequent private plans can share the retained SDK evidence without copying it again after each commit.

Use the shared cache for a live list on either adapter:

const view = history.createMessageView(40);
const initial = await view.initialize(); // latest 40, oldest first
// In the host's change callback/poll loop:
const delta = await view.refresh();
// Patch delta.changed; delta.messages supplies the full retained order.
// Unchanged rows retain object identity and their bodies are not read again.
const older = await view.loadOlder(); // another page when hasMoreOlder is true
view.dispose();

refresh consumes bounded pages of the database's local event log and deduplicates affected TurnIDs before reading their published bodies. One changed message in a 40-message cache reads only that body, including when many append events were coalesced. Edits older than the loaded interval read no bodies. Newly published messages in the interval are added; older ones remain available through loadOlder. New arrivals do not evict loaded rows. Methods serialize, and failures or disposal leave the previous cache/cursor intact. Sustained concurrent commits can return stale after four attempts; the host may retry. Treat returned rows and nested content as read-only. The in-memory cursor belongs to this cache, not a standalone restart checkpoint. A fresh view loads a fresh page after reopen.

readProjectedMessagesById(ids) also provides a deduplicated, consistent point read for up to 1000 TurnIDs, excluding unpublished IDs. Hosts still own wakeup, polling and rendering. Browser hosts should subscribe before initializing the view; the Node adapter currently uses host polling/change hints. The external Lody UI is not wired by this library change.

New sessions can use the adapter's active-view primitives without changing the existing LoroDoc path:

const first = await history.acceptToView({
  viewId: sessionId,
  expectedRevision: 0n,
  expectedHead: null,
  operationId: requestId,
  input: { kind: 'message', businessId: userId, segmentId: 'primary', parents: [], content },
});
const page = await history.readActiveBranchPage(sessionId, { latest: true, limit: 40 });
if (!page.complete) throw new Error('Active branch is incomplete');
const older = page.cursor
  ? await history.readActiveBranchPage(sessionId, { before: page.cursor, limit: 40 })
  : null;

Use forkAndActivate for an edit/resend. Its replacement Create must include the retained base Turn as a parent; the old suffix remains stored and immutable. The active-view revision/head is checked with the same atomic write and event cursor CAS as the Turn Create. Repeating an operation ID is idempotent, while a different input under that ID is a conflict. LodyHistory and NodeLodyHistory expose the same contract. The caller still owns queue, steer and ACP lifecycle decisions.

For immutable input delivery, use acceptInputToView with the same BranchAcceptInput shape, then query readInputAcceptance({ viewId, operationId, input }). Both browser and Node adapters bind an immutable input fingerprint and operation receipt in the same transaction as the branch mutation. Input is copied before asynchronous work begins. The fingerprint includes owner, business/segment identity, content and attachment identity; the receipt key binds view and operation identity. Mutable parents, expected head and revision are excluded.

Repeating an identical accept returns its original receipt even after edits, later appends or active-branch removal. Changed input conflicts. Read-only lookup returns undefined when unobserved, never inserts history, and does not prove a pending accept cannot later commit. A host may retry the identical operation without inventing a new input. Receipts remain owner-local derived indexes; hosts own access checks, retained-generation lookup, execution and guide recovery.

LodyHistory.readOnly(stream) indexes and projects admitted native history without a signing identity; authoring through that instance rejects. The host still owns stream authorization and seal verification. restoreActiveBranch is available in both browser and Node adapters: it atomically creates and seals a non-message branch_state record with its local branch/operation indexes. Remote indexers reconstruct the rollback, including an empty restored view, from that native record. branch_state is readable as an internal history identity but is excluded from HistoryWriteIdentity, AcceptHistory and batch commands; generic authoring rejects it at runtime as well. Use restoreActiveBranch as the only write path. It does not edit sealed Turns or change the Rust state machine/wire.

Active-branch pages return startPosition, totalCount, hasMoreOlder, and an opaque ancestry cursor alongside the selected messages. Positions/counts refer to logical businessId rows: use primary for the initial segment and distinct successor IDs for late output. Project all returned segments of a business ID into one row. Linear branches written with this metadata read a bounded page; legacy/nonlinear ancestry may rebuild a complete directory. readActiveBranch remains available for explicit full snapshots, fenced by the durable event cursor so a concurrent import cannot mix branch membership and bodies. A partial replica missing an older boundary reports incomplete and preserves the logical count; the host fetches or repairs that boundary instead of declaring shorter history. Generic published-message reverse pages include superseded messages and cannot substitute for active-branch pages.

For a confirmed Native linear creation, advanceActiveBranchDirectory(page, result, limit) can return a directory transition with retainedFrom, one new message, and its committed through cursor. Retain page.messages.slice(retainedFrom) and append message; use the returned state/count/older cursor. This transforms membership only, retaining the consumer's own checkpoint-bound old bodies. It is not a fresh read or proof that no later event exists. The owner must still observe and process subsequent events, including state/permission corrections. Results and page state are matched against private host proofs; copied results, forks, partial pages and uncertain intervals return undefined and require ordinary page reads. Older cursors retain late successor segments when the window advances.

An append preserves older cursors while adding any intervening late successors. A fork that removes the cursor's ancestry rejects it as stale; refresh the loaded window and obtain a new cursor. The last reverse page can contain fewer than limit rows and never extends past the cursor. Lody uses these APIs in its Node owner and read-only browser replica. Static checks of that composition do not establish live-service synchronization or product performance acceptance.

New records in the private lody_history_published_v1 table have a version: "2" membership-only value. Current readers also accept legacy full-snapshot values; there is no separate startup scan solely to convert these membership values (the publication catch-up above still applies). Reading an unchanged legacy row incurs its old index payload until republished, but never uses that payload as current content. New receipt publication lists contain compact references; legacy lists and batch recovery command/base snapshots remain readable. Stop older adapters before opening stores written with this layout or the new indexChanged local events, and ship matching browser/Node client and owner revisions.

For a reproducible SQLite comparison, build the Node owner at the repository root with cargo +1.98.1 build --locked --bin roost-node-owner, then run in ts/:

pnpm build
node scripts/history-publication-bench.mjs --history 1000 --samples 7 --output /tmp/publication-bench.json

The checkout must contain Git commit 4a282c826d54434f596ab6183ead7222e5a397f4; the script reads that local object and does not fetch it. Archive/packed or shallow checkouts missing that commit cannot load the baseline. The example compares that adapter with current code using the same Rust owner. It covers 4/16/64 KiB messages, 40-row reads, owner reopen, single-row refresh and imported publication recovery. --history, --sizes, --samples and --binary control the fixture. Output includes latency, row counts, actual body/field/Create reads, event records and IPC requests. It asserts one body read for a single change and zero old-event/Create reads after caught-up reopen. The old adapter's empty imported list is a correctness failure, not equivalent work completed faster. This measures the adapter/storage path without Lody UI. --history sets the local page/refresh/reopen fixture size. Each import sample uses a fresh receiver with 40 messages; the batch variant sends two receipts of 20 messages each. Reopen timing excludes owner process startup/ready and leaves the OS filesystem cache warm. Latencies are observations, not CI thresholds. Recorded example: 2026-09-17 results and measurement limits.

Host payload crypto may return a Promise (AsyncPayloadCrypto), including WebCrypto. Encryption and decryption are awaited outside storage transactions; failed transforms never fall back to plaintext. See RSE1.