Contents

JS OPFS and WASM Record Memory Ownership

This note defines the ownership cut between JavaScript OPFS stores and WASM lace-ffi record memory. It exists to keep durable OPFS files, Rust-owned StoreWork result buffers, Lace position watches, and public JS records from becoming overlapping caches.

Durable truth

OPFS files are the durable store truth:

store-format
record-log   StoreRecordId -> RecordHash, offset, length
records      canonical record bytes, append-only outside compaction
indexes-v2   durable posting lists of StoreRecordId values

A JS OPFS record-log entry stores only:

storeRecordId, hash, offset, length

Entries do not retain committed record bytes or fact objects. Query matching and posting-list maintenance parse trusted committed bytes through JS visitors over one bounded record-aligned read page. The page contains consecutive complete records up to 33 MiB, which exceeds the 33,620,814-byte maximum H3 Seal, and is replaced as one immutable buffer; there is no record page LRU or decoded-facts cache. Unbounded entry-level bytes or facts fields are not part of the architecture.

Retained record owners

There are three retained-record owners:

OPFS files           durable committed bytes
JS record read page  one bounded immutable IO cache
public JS Record     user-owned Uint8Array bytes

There is one temporary result-byte owner:

owned StoreWork       pending result buffers until submit/free/error

OPFS files

OPFS stores full canonical record bytes and durable indexes. Ordinary OPFS query/load/reopen uses trusted committed-record parsing in JS and does not call Rust/WASM validation or crypto.

When a full-record result must cross into Rust, JS copies trusted committed record bytes from the read page into StoreWork-owned result memory through reserve/commit APIs. Rust seals/copies them into Rust-owned record values and checks request/hash association, but does not repeat cryptographic record validation. Internal query projection may borrow a read-page subarray; public and StoreWork record results never borrow mutable store cache memory. JS does not keep Rust-owned record caches or expose WASM memory as public records.

Lace state signal

The owning Lace retains the latest monotonic StorePosition or its first terminal position-publication failure. Position advance wakes open operations; terminal failure wakes and terminates them, rejects later work and claims, and is returned by Lace close after active promises drain. The signal retains no committed record bytes, projected fields, candidate hints, or replay ring. The fixed-point race uses subscribe/install/read-latest ordering as defined in open-store-wakes.md.

Public JS records

Each public Record retains one private plain canonical Uint8Array copy and immutable projected metadata. Creation consumes the builder’s independently owned FFI Record directly; external bytes use validating lace_record_parse. Both project from the live FFI Record, copy canonical bytes once into JavaScript, and free the FFI owner. H3 data is a private subarray of the canonical trailing data range, not another payload allocation. Small metadata values are independently owned. Input ArrayBuffers and typed-array ranges never become retained owners; byte/data accessors return independent arrays. No reflected property exposes the private owner. User code never receives internal WASM buffers, OPFS mutable buffers, StoreWork memory, or store cache memory. Observer participants and capture stores likewise remain private.

WASM runtime ownership

The standard JavaScript package lazily initializes one lace-ffi WASM instance per evaluated core ESM module and JavaScript realm. The in-flight initialization promise, settled LaceWasm wrapper, exports, and linear memory are shared by the core, browser, Node, advanced, and internal entry points in that realm. Ordinary record, policy, codec, participant, and store APIs resolve through that owner; they do not fetch, decode, compile, or instantiate another default module. Concurrent first callers join one initialization attempt, and a failed attempt may be retried. The owner is never reset while live Rust pointers may exist. Separate Workers and separate physical package copies retain separate instances. Explicit advanced instantiation and caller-supplied wasm remain isolated ownership choices and do not replace the standard singleton.

Store adoption and execution

Adopting a store claims it once and returns its executor, the Lace’s only access path; a built-in store keeps no public record operation that could bypass its Lace. An executor projects owned StoreWork before the Lace queues it, then executes it and fills its result; it also reads the authoritative current position and closes the store.

A JavaScript Lace may own the opaque Rust indexed-memory primitive exported by lace-ffi, which executes StoreWork synchronously and fills its result itself. The JavaScript stores (Node and OPFS filesystem-shaped, browser local storage, and the linear oracle) share one log executor in store-ffi.mjs: it projects StoreWork, plans and pages FindRecords with the Rust narrowing ladder, matches trusted record bytes, classifies duplicates, and builds positional results. A store supplies only its position, hash-to-id lookup, committed byte reads, admission of new records, and optionally posting lists and query heat. An admitted id without a committed row is a store failure. Request scalars become safe-integer JavaScript numbers once at this edge; the raw u64::MAX read-through means the current position.

Owned StoreWork is not specific to interlace: the log executor runs work issued by any machine, including the RetentionSweep used by compaction. The static compaction entry points (Lace.compactFilesystem, Lace.compactOpfs, Lace.compactBrowserLocalStorage) open the store without a Lace, claim its executor once as Lace adoption would, and hold the store’s profile lock (Node sentinel or Web Lock) across the sweep and the physical rewrite. They execute one sweep StoreWork at a time, then rewrite, then release the lock. The physical rewrite and OPFS’s single writable session are owned by store-compaction.md.

Each concrete operation maps each local protocol side to a retained Lace handle. It publishes mutation position and submits the complete work. The machine-owned StoreCall pointer never crosses the ABI. Finite providers seed that primitive from already validated public Record values. Neither this path nor the linear JS oracle store introduces an unbounded facts or record cache; semantic store terminology lives in store-record-fact-access.md.

Store work ownership

Each endpoint owns at most one machine StoreCall; a direct pair may have one per side concurrently. While one is active, that endpoint accepts no peer input or policy replacement. The shared binding layer exposes instead one move-only owned StoreWork with a hidden machine/side/id ticket and exact StoreOp clone. JS may retain its opaque numeric WASM pointer across await; it never receives or reconstructs a StoreCall pointer.

CurrentStorePosition uses raw zero for before-first, and positional ResolveStoreRecordIds uses raw zero for absence. Exact load results are an ordered sequence of loaded bytes or Missing. Store results are an ordered sequence of Stored or Duplicate plus returned StoreRecordId. These vectors are positional against the request retained by StoreCall; their outcomes do not echo requested hashes, requested ids, input hashes, or response indexes. The post-store high-water, FindRecords discoveries and continuation, returned bytes, and typed errors remain because they carry new information.

During asynchronous OPFS work, request and partial result data stay in StoreWork-owned Rust memory. An explicit busy guard rejects synchronous replacePolicy while asynchronous drive/store ownership is active; replacement is not appended to a promise tail. Each concrete Lace owns one whole-call execution sequence for reads and mutations. A mutation retains its turn through current-position refresh and current-StorePosition publication, including after store or result-construction failure. Reads reserve their own turns, so neither arrival order permits them inside another mutation’s publication interval. Queued work rechecks Lace close/poison and operation retirement before physical execution; unstarted retired work is freed without execution. Retained-position signal reads remain nonblocking and do not access the store. Independent Laces have independent sequences. If the machine is still active afterward, JS submits the work; Rust requires its hidden ticket, validates the complete result before mutating the machine, moves it into StoreCall’s complete response, and submits once. Only afterward may the endpoint process peer input or a current-StorePosition watch. JS performs no independent FindRecords validation; the shared Rust work layer validates the fixed range, continuation, result family, and requested or frozen expected hashes.

Cancellation of one public wait does not abandon execution while its operation remains active. Each Lace retains a concrete active promise set/count; there is no execution-lease class or registry. If cancellation terminates the machine, or timeout, peer failure, local failure, transport failure, or drop wins first, non-cancellable OPFS work may finish. JS still refreshes and publishes any durable position advance, then frees late StoreWork without submission. A store or refresh failure remains operation-local and is retained for close only when its operation retired before delivery. Confirmed-position publication failure is terminal Integration: the Lace retains the first such failure, wakes and terminates every registered open operation, and rejects later work and claims. Neither failure can turn a mutation into a successful submission. A host result construction failure remains Integration while mandatory refresh/publication classifies any committed progress; that failed construction is never submitted. The hidden ticket cannot fill a newer request. Freeing work releases result reservations; closing Lace waits for the concrete active promises before closing the store and returns the retained terminal failure when present.

Current private collectors materialize each accepted RecordSaved before further driving or capture teardown. List retains earlier owned results after projection failure; get materializes only its first saved record. Projection failure becomes a typed partial outcome without FixedPoint. Observer close first retires its interlace, then waits for its active event projection before releasing captured bytes, retaining the first failure through cleanup. This uses existing private capture dictionaries, not adopted-store getters.

Concrete accessors, builders, submission checks, and late-completion identity checks are defined in store-request-response-ffi-boundary.md. OPFS quota, IO, permission, or corruption failures are typed store errors for the whole request.

WASM memory rules

Store result record bytes do not use generic lace_alloc/lace_dealloc. StoreWork-owned reserve APIs may grow WASM memory. JS must reacquire wasm.exports.memory.buffer after reserve and must not call back into WASM between reserve and filling the returned pointer; commit is the next call on that work value.

The reserved buffer belongs to StoreWork and is dropped on commit failure, submission, or work destruction.

OPFS execution

The asynchronous OPFS Lace path is the supported implementation in both Window and Worker realms. It proves owned StoreWork across await and uses the same shared binding layer and durable layout everywhere.

Opening a named OPFS store derives and acquires one stable Web Lock with ifAvailable; contention fails immediately. A caller-supplied directory handle must include an explicit stable lockName or open fails. The owning Lace retains that lock until close has waited for every concrete active promise and released store resources. There is no process-local WeakSet, store registry, execution lease, or detached completion monitor.

A Worker that owns an OPFS Lace owns the complete JavaScript runtime in that realm. Applications invoke Lace operations there and define their own postMessage commands and UI-facing data. Lace handles, session machines, and WASM-owned state do not cross realms.

Node relation

Node’s filesystem-shaped JS store uses the same host-owned store model, owned StoreWork boundary, and log executor. Node uses Node file APIs instead of OPFS and shares the record-log, index, and trusted committed-record projection code with OPFS.