Optional ReadonlyadaptPer-engine overrides for the auto-adapt heuristic thresholds.
Since 0.5.0 — inert. Its only trigger was
backend: 'auto', and #280 removed the backend option along with the
TS floor it selected between. Nothing reads this field; it is retained
for one release beside import('./auto-adapt.js').AdaptThresholds
/ shouldMigrate, which stay exported, and is removed in 0.6.0.
Was consumed only when backend === 'auto'. Each field on
AdaptThresholds can be flipped here for a single engine
instance without mutating the process-wide env. Keys not present
fall back to the value parsed once at module load from
process.env via CAUSL_WASM_* env vars (the
MODULE_THRESHOLD_OVERRIDES snapshot in ./auto-adapt.ts).
Construction-time merge only — the wrapper captures the merged
snapshot once and never re-reads process.env over its lifetime.
Optional ReadonlybatchedC.4 (#1505) — per-graph batched-flush opt-in for the WASM backend (epic #1493, option-c batched-commit boundary scaffolding).
A PURELY EXPLICIT per-graph wire-tempo opt-in (#163). It is consumed by
loadWasmBackend({ batchedFlush }) and by the ASYNC factory
createCauslWasm(). (It used to name the backend: 'auto' wrapper as a
third consumer; #280 removed the backend option and the wrapper module
with it.)
…and, since causl/causl-wasm-ts#363 finding 21, by the SYNCHRONOUS
factories too. It had been dropped by both of them: perGraphParamsForSync
(packages/core/wasm/index.ts) returned { ...handle, retentionCaps }
unconditionally, so a per-call window was discarded and the PRELOAD's
baked one used in its place. Before #280 the sync path re-threaded it
under createOpts.engine !== undefined; removing the engine option
removed the one arm that did, and nothing caught it because the
NEIGHBOURING per-call option commitHistoryCap kept working
(mergePerCallRetentionCaps, per-call wins) and because — see the
paragraph below — no observable behaviour depends on the window.
Measured after await preloadCauslWasm({ bridge: 'wasmgc-classic' }),
probing __wasmBackendForTests(g).__batchedFlushConfigForTests():
BEFORE AFTER
createCausl({ batchedFlush:{afterN:7} }) undefined { afterN: 7, … } createCauslWasmSync(u, {…afterN:3}) undefined { afterN: 3, … } await createCauslWasm({…afterN:5}) { afterN:5, … } { afterN: 5, … }
A per-call value wins PER FIELD; a field the call omits inherits the
preload's baked window for that field — the same terms as the retention
caps, which is what this paragraph claimed before
causl/causl-wasm-ts#363 and what the code now does. It had been a
whole-object ??, so a PARTIAL per-call window discarded the baked
sibling field and picked up the class default in its place. Measured, with
preloadCauslWasm({ …, batchedFlush: { afterN: 9, intervalMs: 99 } }):
BEFORE AFTER
createCausl({ batchedFlush:{afterN:7} }) {7, 16} {7, 99} createCausl({ batchedFlush:{intervalMs:5} }) {1, 5} {9, 5}
16 and 1 are BatchedFlushOptions' own defaults, so the pre-fix
answers were indistinguishable from "the preload baked nothing".
createCauslWasm takes the option on EITHER of its two bags (the loader
bag or create); create wins per field. Pinned by
test/wasm-implicit-pergraph-caps-141.test.ts.
Omitting it installs no queue of its own — but on the wasm factories
an omitted option INHERITS the window preloadCauslWasm() baked, so a
graph can carry a queue this call never asked for. Pass it explicitly to
override a baked window; there is no per-call spelling that clears one.
None of this is adopter-observable: the rust-ssot engine
— since #280 the only engine this package ships, and no longer
selected by an option — crosses the wire PER COMMIT and never
consults a batched-flush window; the window is honoured only by the
shadow / cross-backend-determinism-gate marshaler path (#163). commit() /
read() / subscribe() results are unchanged whether or not it is
set. Per-graph, not global (option-c doc §2.3).
The shape is { afterN?: number; intervalMs?: number } — kept as
a structural type here so ./types.js does not depend on the
@causl/causl-wasm-ts/wasm subpath (which the main barrel must never pull
in). The canonical declaration is BatchedFlushOptions in
packages/core/wasm/index.ts.
Optional ReadonlycommitBound on the internally-retained commit log. Default 0, which
is no retention at all: commitLog stays empty and every
readAt / snapshotAt answers evicted.
Retention is opt-in, not opt-out. SPEC §5.1 Amendment 2 makes
0 the default and says adopters using readAt / snapshotAt /
commitLog must explicitly ask for it; Phase F.6, the phase that
fills and evicts the chain, runs iff this value is > 0. So this
is the master switch, and snapshotRetentionCap on its own opts
you into nothing.
Pass a positive cap at construction to get history. MEASURED, on a graph with one input, one derived and two commits:
| passed | commitLog |
readAt / snapshotAt |
|---|---|---|
| nothing (the default) | empty | evicted at every t |
{ commitHistoryCap: 1000 } |
2 rows | evicted at every t |
{ snapshotRetentionCap: 1000 } |
empty | evicted at every t |
{ commitHistoryCap: 1000, snapshotRetentionCap: 1000 } |
2 rows | retained |
The second row is the one that surprises: a positive
commitHistoryCap alone gives you the log and still evicts every
historical read, because the snapshot cap it gates is itself 0 by
default. For readAt / snapshotAt, set both.
createCausl({ commitHistoryCap: 1000, snapshotRetentionCap: 1000 })
The cap is read from EVERY bag on every factory that takes one, so a call that names it is never silently built without it:
createCausl({ commitHistoryCap: 1000 }) // the bag
createCauslWasmSync(handle, { commitHistoryCap: 1000 }) // 2nd arg
preloadCauslWasm({ commitHistoryCap: 1000 }) // baked
createCauslWasm({ commitHistoryCap: 1000 }) // loader bag
createCauslWasm({ create: { commitHistoryCap: 1000 } }) // create bag
createCauslWasm is the only factory with TWO bags, and it accepts the
cap in EITHER. When both name it, create wins per field: a cap only
the loader bag names is inherited, so
{ commitHistoryCap: 1000, create: { snapshotRetentionCap: 2 } } builds
{ 1000, 2 }. The same per-field rule governs the neighbouring
CreateCauslOptions.batchedFlush. Until
causl/causl-wasm-ts#411 the create bag's caps were read by neither
half of the graph, so a call that named them only there built a graph
with NO retention and no error. That is the shape a reader takes as
"retention is broken" rather than as "the option did not take".
preloadCauslWasm BAKES the cap into its handle, and a later
createCauslWasmSync that omits it inherits that baked value per field
(boot inheritance). The async createCauslWasm does NOT inherit a baked
cap: it resolves its window from its own two bags on every call, so the
first preload's caps never leak into an unrelated later graph.
Optional ReadonlydisposedBound on retained disposed-node tombstones. Default 1000.
Disposal records a tombstone keyed by node id so that subsequent
public-surface access surfaces a typed NodeDisposedError rather
than UnknownNodeError. Under churn with fresh ids each
lifecycle (timestamped keys, family(uuid()), generated row ids
in a virtualized list), an unbounded tombstone map is a monotonic
retention root. The cap is the same shape as commitHistoryCap:
a FIFO ring on insertion order. Past the cap, very-old tombstones
are evicted and their ids fall back to UnknownNodeError — the
"log rotated" arm — which is acceptable because the typed disposal
error is most useful immediately after disposal, not years later.
Optional ReadonlyenableEnable the dev-only H1 hazard warning (#1155, #1241).
Per docs/wasm-backend-adopter-audit.md H1 and SPEC §15.1 (PR #1129),
the engine does NOT guarantee reference-identity stability for values
returned from Graph.read: a commit may produce a structurally
equivalent but distinct object, and adopters who cache a read()
return across a commit boundary will silently desynchronise from the
graph's current value. The symptom is subtle — no error fires; the
cached reference simply stops tracking the live state.
When enabled, every non-null object/function returned by read()
outside a tracking projection is recorded as a WeakRef along
with the read-time GraphTime. After each commit advances now, the
engine walks live WeakRefs and emits one console.warn per survivor
whose recorded GraphTime predates the post-commit clock — naming the
offending node id and pointing at SPEC §15.1. The warning never
throws; the contract is informational only.
Default is false (opt-in) per the panel review of #1241. PR
#1238 originally shipped with an auto-detected dev/prod default,
but the canonical @causl/react adapter holds the read()
return inside useSyncExternalStore's snapshot cache for tearing
detection — that single retained reference triggered the warning
on every commit for any adapter usage.
The follow-up (#1241) ships three coordinated fixes:
enableH1HazardWarning is now false; adopters
who want the dev safety net opt in explicitly with
createCausl({ enableH1HazardWarning: true }).__causl_* adapter-exemption seam (used by
@causl/react's canonical hooks) suppresses H1 tracking for
reads inside an adapter's getSnapshot boundary, so opt-in
adopters do not see false positives from official adapters.process.env.NODE_ENV !== 'production' literal blocks so
esbuild / terser can dead-code-eliminate the WeakRef apparatus
in production builds.Pass true explicitly to engage the dev-only WeakRef tracker; pass
false (or omit the option) to keep the engine on the production
hot path with no per-read() bookkeeping.
Bookkeeping cost when armed: one WeakRef allocation per qualifying
read() call (primitives, null, reads inside a tracking
projection, and reads inside the adapter-exemption seam are
skipped); one O(N) walk per commit over the survivor list with
dead-ref pruning. Empirically <1% on linear-chain × 1000 traces
in dev mode.
Optional ReadonlyexperimentalEngine-instance overrides for the CAUSL_* env-var flag protocol
(#706). Each field on CauslFlags can be flipped here for
a single engine instance without mutating the process-wide env.
Keys not present fall back to the value parsed once at module
load from process.env (the MODULE_FLAGS
snapshot in ./flags.ts).
The experimental prefix mirrors the experimentalFlags naming
adopted by other long-lived TypeScript libraries: callers should
read each entry as "this knob is opt-in measurement only; the
default is the safe behaviour, and the field exists so an
adopter or a benchmark can flip it without an env-var dance."
Construction-time merge only — the engine captures the merged
snapshot in its closures and never re-reads process.env for
that flag again over the engine's lifetime.
Optional ReadonlynameStable identifier for this graph instance. Surfaced as graphId
on every IR node and commit in the schema-3 IR
(graph.exportModel()). Optional — the engine assigns a UUID v4
when absent.
Validity rule: must match /^[A-Za-z0-9_.:-]{1,256}$/. The engine
throws InvalidGraphNameError at construction if the regex
does not match. The character set is the intersection of "safe in
JSON", "safe in URL fragments", and "safe in filesystem paths" —
the three places adopters have been observed pasting a graphId.
The 256-char cap mirrors the §12.2 teaching-cost cap on public
surface names.
The precedence rule is application-supplied wins: a name passed here lands on the IR; absence falls back to a UUID v4 the engine mints once at construction. The field is read-only on the graph instance once construction returns — there is no public mutator and no rebrand operation.
Optional ReadonlyonHook fired when an observer (per-node or per-commit) throws.
Defaults to console.error. Pass a no-op to silence.
Optional ReadonlysnapshotBound on retained per-commit snapshots used by readAt(t) and the
DevTools bridge. Default 0, and it does nothing on its own.
The effective window is
commitHistoryCap > 0 ? snapshotRetentionCap : 0, so a graph that
sets this and leaves CreateCauslOptions.commitHistoryCap at
its default still answers evicted everywhere. Set both, or
neither. See the measured table on commitHistoryCap.
WHERE to pass it is identical to its sibling's, including
createCauslWasm({ create: { … } }) and the per-field merge across that
factory's two bags (causl/causl-wasm-ts#411). See the shape list on
CreateCauslOptions.commitHistoryCap.
Optional ReadonlystrictAs of #670 / #705 this option is a no-op. The
pre-#705 strict-cycle gate ran an O(|nodes|) forward DFS at every
derived() registration to refuse a back-edge before the entry
landed; the cost was load-bearing on linear-chain × 1000
(420 ms median against a 5 ms audit floor) and structurally
lethal on linear-chain × 10000 (V8 stack overflow on the
registration recursion). Phase D's augmented Kahn pass now
catches the same race-class at first-commit-time without
paying the registration-time cost — see SPEC §9.1 row 8 (and
its Amendment 1) for the contract. The option remains accepted
on createCausl({ strictCycles }) for one major version so
adopter call sites do not have to be edited in lockstep with
the gate removal; both true and false produce identical
first-commit-time semantics. A future amendment will remove
the surface entirely (semver-major).
Options accepted by
createCausl.Remarks
Intentionally minimal — every additional public option is a teaching cost paid by every future user. The bar for adding a field here is the same as for adding a public method: name the unavoidable concept the engine cannot express without it, or take the cost of growing every README and every consumer's mental model.