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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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’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.