Skip to content

API reference > @kontsedal/olas-core

olas-core package ​

Classes ​

Class

Description

MutationDisposedError

Rejection from mutation.run(...) when the mutation was already disposed — the owning controller is gone, so mutate was never called and **the write did not happen**.

Deliberately NOT an AbortError. Every other cancellation in this library is one, and isAbortError(err) is the documented way to filter them — which is exactly why an abort is the wrong shape here. A superseded or reset run is work the app *chose* to drop; a run against a disposed mutation is work the app asked for and silently did not get, and a blanket abort filter would hide that lost write.

Reaching this usually means a callback outlived its controller — a confirm dialog answered after the panel behind it closed, a retry button in a toast that outlives the view. Two fixes, in order of preference:

  1. Own the mutation somewhere that lives as long as the interaction does. 2. createMutation(ctx, { detached: true }) — runs then survive dispose, and this error is never thrown. SPEC §6.5.

QueryDisabledError

refetch() was called on a subscription whose enabled is false. A disabled subscription has no entry to fetch; its key may not even be computable yet. Filter this error, or disable the control whilesubscription.isEnabled.value is false.

Functions ​

Function

Description

batch(fn)

Batch synchronous signal writes so subscribers see one notification at the end of the batch rather than one per write. Returns whatever fn returns.

bindQuery(ctx, query, options)

Bind a query value to this controller's root, for imperative reads and writes outside a subscription (§5.5, §6.4). options.origin tags the handle's writes for plugins.

bindQuery(ctx, query, options)

Bind an infinite query to this controller's root (§5.11). The handle's methods act on the entry's pages array. options.origin tags the handle's writes for plugins.

computed(fn)

Create a Computed<T> — a read-only derived signal. The provided fn is re-evaluated whenever a signal it read during its last run changes; the resulting value is cached until then.

Spec §20.1. The graph is glitch-free: a computed re-runs at most once per batched-write cycle.

createCache(ctx, fetcher, options)

A controller-local cache — one fetcher, no sharing, no cache key (§5.1).

ts
const report = createCache(ctx, ({ signal, deps }) => deps.api.report({ signal }))

Unlike createQuery this needs **no** query engine: a local cache is not a cache-client entry. It still honours the root'squeryEngine({ defaults }), which the root reads without the client so that reading them cannot pull the engine into the bundle. Its fetches count toward root.waitForIdle() all the same.

createEmitter(options)

Create a standalone emitter. Handlers persist until explicitly unsubscribed (or the emitter is disposed). Use this for emitters that live outside any single controller — typically in deps. Use ctx.emitter() for emitters that should auto-clean with a controller.

Pass onError to receive emit-time handler throws (spec §20.6 — one throwing handler must not block the rest of the fan-out). ctx.emitter() wires this to the root's onError so deps-level emitters get isolation by default when constructed via ctx.

createField(ctx, initial, options)

A reactive form field owned by this controller's lifetime (§8.1).

ts
const email = createField<string>(ctx, '', { validators: [required('Required')] })

T is inferred from initial. With validators, a literal such as '' stays literal (Field<''>), so name the type as above.

A free function rather than a ctx method, so a controller that never builds a field does not ship the forms subsystem.

createFieldArray(ctx, itemFactory, options)

A dynamic list of fields or forms (§8.5).

ts
const lines = createFieldArray(ctx, (initial?: string) => createField(ctx, initial ?? ''))

add(value) and insert(index, value) pass value to the factory, so a factory that ignores its argument builds every item from its own default.

createForm(ctx, schema, options)

A form aggregating fields, nested forms and field arrays (§8.3).

ts
const form = createForm(ctx, { email, password })

createMutation(ctx, def, hooks)

A write owned by this controller's lifetime (§6). Either inline:

ts
const save = createMutation(ctx, {
  mutate: (draft: Draft, { signal, deps }) => deps.api.save(draft, { signal }),
})

or from a module-scope defineMutation(...), with this controller's lifecycle hooks layered on:

ts
const place = createMutation(ctx, createOrder, { onSuccess: () => toast('Placed') })

Needs a query engine: mutations participate in the root's in-flight accounting, which waitForIdle() reads during SSR.

createMutation(ctx, spec)

A write owned by this controller's lifetime, from an inline spec (§6): the write, its policy and its lifecycle hooks in one object. id is optional here. Devtools, error contexts and plugins name the mutation by it.

Needs a query engine on the root.

createQuery(ctx, source, options)

Subscribe this controller to a shared cache entry (§5.2).

ts
const user = createQuery(ctx, userQuery, () => [userId.value])

Needs a query engine on the root:createRoot(def, { deps, queries: queryEngine() }).

With options.select, the subscription reports select(data). The projection runs per subscriber, and the cache keeps the raw value.

createQuery(ctx, source, keyOrOptions)

Subscribe this controller to a shared cache entry (§5.2). The third argument is a key thunk, or { key, enabled, keepDataWhileDisabled }. A key thunk that reads signals re-keys the subscription when they change.

Needs a query engine on the root.

createQuery(ctx, source, keyOrOptions)

Subscribe this controller to an infinite query (§5.11). The subscription adds pages, flat and the paging state and actions to theAsyncState<TPage[]> surface.

Needs a query engine on the root.

createRoot(def, options)

Construct a root controller. Root factories take no props — startup config goes in deps.

deps is checked against AmbientDeps: in an app that augments it withapi: ApiClient, a root whose deps has no api does not compile. Extra members are allowed. createTestController does not check, so a test can pass only the fakes the controller under test reads.

createSelection(options)

Create a Selection<T>. Optional initial seeds the selected set.

handleClick encapsulates the standard click semantics: - plain click → select only id (anchor moves to id) - meta-click → toggle id. The anchor moves to id on add and stays on remove, so the row last clicked stays the anchor, as in a file manager. deselect(id) of the anchor clears it instead. - shift-click → range from anchor to id along ordered (anchor sticks, so subsequent shift-clicks extend from the same origin). With a Map, the range is the ids whose index lies between the two.

Spec §16.5.

debounced(source, windowMs, options)

Lag a signal by ms. The returned signal updates only after the source has been unchanged for ms. Each new write resets the timer.

  • leading: true (default false) emits immediately on the first write, then suppresses further writes until ms has passed since the last emission. Combine with trailing (default true) for "first + last" semantics. - trailing: false disables the trailing emission. Pair with leading: true for "only fire on the leading edge" semantics. - options.signal (AbortSignal) ties the internal effect to a lifecycle — when the signal aborts the effect disposes, the pending timer clears, and the subscriber chain on source drops.

ms goes through the shared expiry scheduler (spec §21.5): Infinity never fires on its own, so only flush() emits, and a window past the 32-bit timer limit waits its full length. NaN runs as 0, with a warning in development.

debouncedValidator(fn, ms)

Wrap an async validator with a debounce. The debounce timer resets on every value change. While debouncing or the request is in flight, the field'sisValidating is true and isValid HOLDS its last settled value (T5.3) — so editing an already-valid field doesn't strobe a submit button to disabled on every keystroke. A field with no prior settled validation defaults to valid.

A fn that throws synchronously, or returns no promise, fails the pass the way a sync validator that throws does: the message shows on the field and the error reaches onError. The pass settles either way.

defineController(factory, options)

Create a controller definition. The factory is stored on the returned object and invoked during createRoot / ctx.child to build instances.

Props defaults to void so a factory written as (ctx) => ... is typed as ControllerDef<void, Api> — the form createRoot requires.

defineInfiniteQuery(spec)

Define a shared paginated query, named by its id. Spec §5.11.

defineMutation(definition)

Define a mutation at module scope. The definition is registered by id, so a plugin can run it with no controller present. The mutation queue replays a run persisted before a reload this way.

ts
// module scope
export const createOrder = defineMutation({
  id: 'order/create',
  mutate: (vars: OrderInput, { signal, deps }) => deps.api.createOrder(vars, { signal }),
  meta: { persist: true },
})

// a controller
const place = createMutation(ctx, createOrder, {
  onSuccess: () => toast('Order placed'),
})

mutate must not close over controller state, because on a replay there is no controller. Reach services through deps instead.

definePlugin(plugin)

Identity helper that types an object literal as an OlasPlugin.

defineQuery(spec)

Define a shared query, cached per root and named by its id. Spec §5.2.

defineScope(options)

Create a scope. The returned value is the typed handle passed toctx.provide(scope, value) and ctx.inject(scope). The scope object is its own identity, so two defineScope() calls — even with identical options — yield distinct scopes.

effect(fn)

Run fn immediately and again whenever any signal it reads changes. Iffn returns a function, that function is called as a cleanup before the next re-run and on dispose.

Returns a dispose function. Inside a controller use ctx.effect(...) instead — that variant is auto-disposed with the controller.

email(message)

Reject strings that don't look like an email. Empty / null pass (use with required to forbid).

isAbortError(err)

True iff err looks like an AbortError. Matches the standard DOMException shape thrown by AbortController AND any object whose name === 'AbortError' — that covers axios / msw / user-thrown plain Errors that signal abort.

Spec: §20.12. Node 17+ exposes a global DOMException, so the instanceof branch works server-side; the name-based branch is the portable fallback.

max(n, message)

Reject numbers greater than n.

maxLength(n, message)

Reject strings / arrays longer than n.

min(n, message)

Reject numbers less than n.

minLength(n, message)

Reject strings / arrays shorter than n. Allows null/undefined (use with required to forbid).

mustBeTrue(message)

Reject any value that isn't boolean true — the "you must check this box" rule for consent / terms-of-service / confirm checkboxes. Unlike required (which now accepts false as a valid boolean), this fails on false.

pattern(re, message)

Reject strings that don't match the supplied RegExp.

queryEngine(options)

Build a query engine, the value createRoot takes as queries to give a root a query cache. options.defaults sets root-wide query defaults (§5.9). Each root that adopts the engine builds its own client, so one engine at module scope can serve several roots.

required(message)

Reject empty values (undefined, null, empty string, empty array). Booleans always pass.

serializeForScript(value)

Serialize a value as a JavaScript expression that is safe inside an inline<script>. Use it for server state inlined into HTML, such as aroot.dehydrate() payload:

ts
const html = `<script>window.__OLAS_STATE__ = ${serializeForScript(root.dehydrate())}</script>`

The result is JSON.parse("…") over the JSON, with every character that could end the string, the script or an attribute written as a \uXXXX escape. Two reasons for JSON.parse over a plain object literal: - a key named __proto__ stays an own property, where an object literal would make its value the object's prototype; - U+2028 and U+2029 cannot reach the script source raw.

Throws what JSON.stringify throws, for a BigInt or a cycle.

signal(initial)

Create a writable Signal<T>. Reads track the current auto-tracking scope (effect / computed); writes notify all subscribers (deduped via Object.is).

Spec §20.1. For a single-pass non-tracked read use signal.peek().

throttled(source, windowMs, options)

Rate-limit a signal so it emits at most once per ms (leading + trailing). The first change passes through immediately. Subsequent changes within the window are coalesced; the latest value is emitted when the window expires.

  • leading: false (default true) skips the immediate leading-edge emission. Useful for "fire only after the window settles" semantics. - trailing: false (default true) skips the windowed trailing emit. Combine with leading: true for "only fire on the leading edge." - options.signal ties the internal effect to a lifecycle.

The returned handle exposes cancel() / flush() — see TimingSignal.ms goes through the shared expiry scheduler, as in debounced: withInfinity, only the leading edge and flush() emit. NaN runs as 0, with a warning in development.

untracked(fn)

Run fn with auto-tracking suppressed — signals read inside don't become dependencies of the surrounding computed / effect. Useful for "read these signals once to log them" or for snapshotting state inside an effect without subscribing to it. For a single-signal peek, prefer signal.peek().

validator(schema)

Wrap any Standard-Schema-compatible schema (Zod 4, Valibot 1, ArkType 2, …) as an Olas validator. Returns **all** issues as FormIssue[], each carrying the schema's own path — so a whole-form schema used as a form-level validator routes each issue onto the matching field (T5.2), and a leaf schema (whose issues have empty paths) still reports on the field it's attached to. An empty array means "valid".

Standard Schema validators may be sync or async; this wrapper threads through whichever the schema returns — Promise<FormIssue[]> only when the underlying validate call is itself async.

signal is accepted to match the Validator<T> shape but isn't forwarded — Standard Schema v1 has no cancellation surface.

Interfaces ​

Interface

Description

AmbientDeps

App-wide deps available on every controller's ctx.deps.

Default shape carries an index signature so untyped reads compile (asunknown). Users augment this interface in their app to add typed services:

ts
declare module '@kontsedal/olas-core' {
  interface AmbientDeps {
    api: ApiClient
    session: SessionStore
  }
}

MutationMeta

Per-mutation plugin settings, carried on MutationSpec.meta. Empty in core: plugin packages add their fields through declaration merging.

QueryMeta

Per-query plugin settings, carried on QuerySpec.meta. Empty in core: plugin packages add their fields through declaration merging.

ts
declare module '@kontsedal/olas-core' {
  interface QueryMeta {
    crossTab?: boolean
  }
}

Type Aliases ​

Type Alias

Description

ActivityEvent

An entry gained its first subscriber, or lost its last one.

AsyncState

The ten reactive signals + four actions a subscriber sees for any async resource (LocalCache<T> or a Query subscription). Spec §20.4.

  • data / error / status — current outcome. - isLoading — true only on the first pending fetch (no data yet). - isFetching — true on any pending fetch. - isStale — true when staleTime has elapsed since the last fetch, hydrated row or canonical write. An optimistic setData does not reset it. - lastUpdatedAt — epoch ms of the last change to data, an optimistic setData included. - hasPendingMutations — at least one mutation has a snapshot on this entry. - isPaused — a fetch is parked waiting for network reconnect. - isEnabled — false while a subscription's enabled returns false; always true for a local cache.

Actions: - refetch() — force a fetch; resolves with the result. - reset() — clear error + status without re-fetching. - cancel() — abort the in-flight fetch, if any, and drop one parked for the network. - firstValue() — resolves at once when data for the current key is already there, even while a background refetch runs or after one failed. Otherwise it resolves on the first success and rejects on the first failure. The previous key's data that keepPreviousData keeps on screen does not count. It is the promise to hand to Suspense or React 19's use(...). It never waits on nothing: on an idle entry with no fetch coming, as after a cancelled first load, it starts one.

status reads 'pending' during every fetch, a background refetch included, while data stays. Test data !== undefined for "something to show".

AsyncStatus

Lifecycle phase of an async resource.

BindQueryOptions

Options for bindQuery(ctx, query, options?) and root.bindQuery(query, options?).

Collection

The reactive surface returned by ctx.collection(...). items is the canonical ordered view (source-order, with any construction-failed items filtered out); size mirrors items.length; get / has are imperative key lookups. Once the owner disposes, items is empty and has is false. SPEC §11.1.

CollectionFactoryApi

Extract the union of every branch's controller Api. Distributes over R.

CollectionFactoryOptions

Heterogeneous form of ctx.collection: a single factory decides per-item which controller + props to construct. When a key's factory result picks a *different* controller than last time, the existing child is disposed and the new one constructed (type-discriminant rebuild).

R is the factory's *return type* (typically inferred as the union of the branches' { controller, props } shapes). Api is then projected out as the union of every branch's controller Api via CollectionFactoryApi<R> — unlike a single Api generic, the union doesn't collapse to the first branch.

CollectionFactoryResult

Constraint for the factory form's return shape.

CollectionHomogeneousOptions

Homogeneous form of ctx.collection: one controller def for every item, with propsOf projecting each item to the controller's Props. Construct happens once per new key — propsOf is **not** re-applied for unchanged keys.

Computed

A read-only derived signal — alias of ReadSignal<T>.

ControllerDef

The handle returned by defineController(...). Pass it to createRoot(...) or ctx.child(...) to instantiate. Phantom types preserve Props / Api for inference via CtrlProps<C> / CtrlApi<C>.

CtrlApi

Extract a controller's Api type.

CtrlProps

Extract a controller's Props type.

Ctx

ctx is the lifecycle-bound surface every controller factory receives. Every primitive constructed through ctx is owned by the controller and disposed when the controller disposes. The primitives are free functions that take ctx first (createField, createQuery, createMutation and the rest); ctx itself carries the tree and the lifetime.

DebugBus

The shape of root.debug. Subscribe to receive every DebugEvent until the returned unsubscribe is called.

The bus replays a snapshot of the *live controller tree* to every new subscriber synchronously inside subscribe(...) — so a panel that mounts after createRoot() sees the existing tree immediately, not just future events. Event types other than controller:* are not buffered.

queryEntries() returns a fresh inspector snapshot — current state of every cached entry. Useful for "what's in the cache right now?" views.

DebugCacheEntry

Snapshot of one live cache entry — produced by root.debug.queryEntries() so devtools panels can show *current data*, not just past fetch events.

DebugEvent

Distribute DebugEventMeta across every variant of the union. Written as a distributive conditional (not a plain DebugEventBody & DebugEventMeta intersection) so each member keeps its literal type discriminant andswitch (event.type) still narrows.

DebugEventBody

The set of event bodies emitted by a root. DebugEvent layersDebugEventMeta onto each (see below). Spec §14. Adding new variants is non-breaking — consumers switch on type and ignore unknowns.

DebugEventMeta

Correlation fields stamped onto — or shared across — every DebugEvent. All optional: consumers building events by hand (and the devtools store'shandle() in tests) need not supply them, and the emitter fills seq/t in on the way out. Adding them is non-breaking.

DeepPartial

T with every property optional, at every depth. An array becomes aReadonlyArray of deep-partial items. Form.set and a form's initial take it, so a caller names only the leaves it writes.

DefineControllerOptions

Optional configuration for defineController.

DehydratedEntry

One entry inside a DehydratedState.

DehydratedState

SSR-serializable snapshot of a root's QueryClient. Produced byroot.dehydrate() on the server; consumed bycreateRoot(def, { hydrate: state }) on the client. Spec §15, §20.9.

Emitter

Synchronous fan-out event bus. Handlers run in the order they subscribed.

Emission iterates a SNAPSHOT of the handler set taken at the start ofemit(). So within one emission: a handler added during emit does NOT fire for the current emission, and a handler removed during emit STILL fires for the current emission (it was captured in the snapshot).

Handlers are stored in a Set keyed by reference — calling on(h) twice with the same h registers it ONCE; a single unsubscribe then removes it.

Emitter<void> exposes emit() (no argument); other shapes exposeemit(value: T).

Spec §7, §20.6.

EmitterErrorReporter

Optional escape hatch for emit-time handler throws. If supplied, a thrown handler is reported here and emission continues with the remaining handlers (spec §20.6 — one throwing handler must not block the rest). If absent, the throw is logged via console.error.

ErrorContext

Context passed to a root's onError handler. kind identifies where in the controller's surface the throw originated; controllerPath is the path from root to the controller that owned the failing code; queryId and key name the cache entry for cache kinds. Spec §12, §20.9.

'plugin' is used for exceptions raised by plugin hooks and reported through host.reportError (@kontsedal/olas-cross-tab and friends); SPEC §13.

The remaining fields are correlation hooks for telemetry adapters (Sentry / OpenTelemetry breadcrumbs / Datadog RUM): eventId is a stable per-dispatch UUID, timestamp is wall-clock ms, attempt and cause describe a failed fetch (cache kinds), pluginName identifies the throwing plugin (set only when kind == 'plugin').

ErrorHandler

Signature of RootOptions.onError.

FetchContext

What a wrapFetch middleware sees about one fetch attempt.

FetchCtx

Per-fetch context: the AbortSignal to honor + the root's deps. Passed as the first argument to every QuerySpec.fetcher invocation so module- level queries can reach their dependencies without resorting to globals.

Field

A reactive form field. Extends ReadSignal<T> for the current value, plus five signals for state (errors / isValid / isDirty / touched / isValidating) and four methods (set, reset, markTouched, revalidate). Created viacreateField(ctx, initial, { validators, validateOn }). Spec §8, §20.7.

FieldArray

A dynamically-sized list of Field or Form items. Created viacreateFieldArray(ctx, itemFactory, options?). The factory is invoked per insertion. Spec §8, §20.7.

A field array is a ReadSignal of its items' values, like a Field:array.value is FieldArrayValue<I>. items holds the item nodes.

FieldArrayItemErrors

The errors of one field-array item: string[] for a field, FormErrors<S> for a form.

FieldArrayOptions

Options for createFieldArray(ctx, itemFactory, options?).

FieldArrayValidator

An array-level validator, for rules such as "at least one item". It sees every item's value. A string result lands in the array's topLevelErrors, and a FormIssue[] routes each message by its path, item index first.

FieldArrayValue

The value of a FieldArray<I>: T[] for Field<T> items, andFormValue<S>[] for Form<S> items.

FieldOptions

Options for createField(ctx, initial, options?).

FieldTransform

A bidirectional T ↔ string transform, suitable for HTML input bindings where DOM values are always strings.

parse(raw) converts the input's string value into the field's type;format(value) converts the field's typed value back into a string for the input. Both must be pure — useFieldInput calls them on every render and every input event respectively.

ts
const numberTransform: FieldTransform<number> = {
  parse: (raw) => Number(raw),
  format: (v) => String(v),
}

Form

A nested form. Created via createForm(ctx, schema, options?). Spec §8, §20.7.

A form is a ReadSignal of its aggregate value, like a Field: form.value is the structurally-typed FormValue<S>, and form.subscribe fires when any leaf changes. errors mirrors the value's shape withstring[] | undefined. flatErrors is a flattened view for rendering a single error summary.

FormErrors

The errors of a Form<S>, in the schema's shape: string[] | undefined for a field, a nested FormErrors for a nested form, and one entry per item for a field array. Every key is optional.

FormIssue

A single validation issue, optionally targeting a descendant of a form tree.

  • An **empty** path means the node the validator is attached to itself — for a leaf Field that's the field; for a Form / FieldArray it's the node's topLevelErrors. - A **non-empty** path routes the message to the matching descendant (Form walks keys, FieldArray walks numeric indices). Unresolvable paths fall back to the owning node's topLevelErrors rather than vanishing.

Segments are object keys (string) or array indices (number). Returned by form-level validators (cross-field rules) and by the Standard-Schemavalidator(...) adapter, which maps each issue.path here. See SPEC §8.3.

FormOptions

Options for createForm(ctx, schema, options?).

FormSchema

What createForm takes: an object of fields, nested forms and field arrays. Its keys name the form's value, its errors and the paths setErrors takes.

FormValidator

A form-level validator, for rules across fields. It sees the wholeFormValue<S>. A string result lands in the form's topLevelErrors, and a FormIssue[] routes each message to the field its path names.

FormValue

The plain value of a Form<S>, under the schema's keys: a field's T, a nested form's FormValue and a field array's array of item values.

InfiniteFetchCtx

Per-fetch context for an infinite query: the page to fetch, theAbortSignal to honor, and the root's deps. See FetchCtx for the regular-query analogue.

InfiniteQuery

Module-scoped handle for a paginated query. Mirrors Query<Args, TPage[]> with paginated setData semantics.

InfiniteQueryActions

Imperative paginated-query operations bound to one root.

InfiniteQuerySpec

Configuration for defineInfiniteQuery({ ... }). Spec §5.11, §20.4.

  • getNextPageParam(lastPage, allPages) returns the param for the next page, or null when there's no more. - getPreviousPageParam (optional) enables bidirectional infinite lists. - itemsOf(page) (optional) flattens pages into items for the subscription.flat convenience signal.

InfiniteQuerySubscription

What createQuery(ctx, infiniteQuery, ...) returns. Extends AsyncState<TPage[]> with paginated controls: fetchNextPage / fetchPreviousPage,hasNextPage / hasPreviousPage, and per-direction isFetching signals.

flat is a convenience: the pages' items, flattened through the spec'sitemsOf. Without itemsOf, flat equals pages (spec §5.11).

InvalidateEvent

A cache entry was invalidated.

ItemInitial

What seeds one field-array item: the field's T, or a DeepPartial of the form's value. add and insert pass it to the item factory.

LazyChild

Handle returned by ctx.lazyChild(...). status walks `idle → loading → (ready | error); apibecomes defined oncestatus === 'ready'`. A disposed handle, by dispose() or by its parent's, reads 'idle' with noapi, and its load() rejects. SPEC §16.5.

LocalCache

A cache owned by one controller — no sharing across the tree. Returned bycreateCache(ctx, fetcher, options?). Disposed automatically with the controller.

LocalCacheOptions

Options for createCache(ctx, fetcher, options?). Spec §5.1.

MutateContext

What a wrapMutate middleware sees about one mutate attempt.

MutateCtx

What mutate receives besides the variables.

Mutation

A running mutation. Created via createMutation(ctx, spec) — the controller owns its lifetime. Each run(vars) returns a Promise; the signals reflect the last-resolved run for UI binding.

Spec §6, §20.5.

MutationConcurrency

How concurrent calls to mutation.run(...) interact: - parallel (default): every call runs concurrently. - latest-wins: a new call aborts any in-flight previous call (AbortSignal fires). - serial: calls queue and run one at a time in order.

Spec §6.1.

MutationDef

A module-scope mutation, returned by defineMutation(...). Run it from a controller with createMutation(ctx, def, hooks?).

MutationDefinition

The half of a mutation that describes the write itself, fordefineMutation: its identity, the write, and its policy. Lifecycle hooks are not part of it. They belong to the controller that runs the mutation and go to createMutation(ctx, def, hooks).

MutationEvent

One step of a mutation run. Every run emits 'start' (after onMutate, before the first mutate call) and then exactly one of 'success','error' (retries exhausted) or 'cancel' (a supersede, reset(), or the owner's disposal; reason says which).

A serial run that has to wait behind another emits 'queued' first, whenrun(...) is called, under the runId it keeps. It then emits 'start' when its turn comes. A queued run that never starts still emits exactly one outcome: 'cancel' when reset() or the owner's disposal drops it, or'error' when its onMutate throws.

MutationHooks

The per-owner half of a mutation: what createMutation(ctx, def, hooks) adds.

MutationHost

Runs mutations registered with defineMutation, with no controller involved.

MutationRef

A mutation as a plugin sees it. id is undefined for an inline spec without one.

MutationRun

Call signature for mutation.run: - When V is void → no args. (mutation.run()) - When V was not constrained (default-inferred as unknown) → optional arg. Lets createMutation(ctx, { mutate: async () => 1 }) call run() *or* run(anything) without a type error. - Otherwise → arg required. (mutation.run(vars))

Defined as a variadic-tuple conditional so consumers see the right shape without writing run(undefined as unknown as void).

MutationSpec

The configuration object passed to createMutation(ctx, spec). See spec §20.5 for the full lifecycle semantics. onMutate may return a Snapshot (fromquery.setData(...)) to enable automatic rollback on error.

NetworkHost

Connectivity and focus, as the query engine sees them.

NetworkMode

How a query behaves with respect to the network reachability signal.

  • online (default) — pause fetches while navigator.onLine is false; automatically resume when reconnect fires (via subscribeReconnect). Inflight fetches are NOT aborted on offline; a bindEntry / acquire that lands while offline simply defers the initial fetch. The deferred entry reports isPaused: true until reconnect. A fetch requested while offline does supersede one already in flight, as a request made online does, so the older response never lands. - always — never gate on connectivity; fetcher runs whenever requested. Useful for queries against localhost / IPC / a service worker that doesn't surface through navigator.onLine. - offlineFirst — start the fetch regardless; if it rejects with a network-shaped error (a fetch TypeError; AbortError excluded) while navigator.onLine is false, park the entry (isPaused: true, status stays idle / last-success) and retry on reconnect rather than surfacing the error. Matches TanStack's offlineFirst policy for app-shell-first PWAs.

OlasPlugin

A plugin: a named setup that runs once for every root the plugin is installed in. The value is a definition, not an instance. Per-root state lives in setup's closure, so one plugin value can be shared by any number of roots, including a HydrationBoundary that rebuilds its root.

ts
const logger = definePlugin({
  name: 'logger',
  setup(host) {
    return {
      onWrite: (e) => console.log(e.query.id, e.source, e.key),
    }
  },
})

createRoot(app, { deps, queries: queryEngine(), plugins: [logger] })

Spec §13.

PluginHooks

What setup returns. Every hook is optional. Observation hooks are synchronous and run after the change is visible to subscribers. A throw is isolated to its plugin and reported to the root's onError. None run once the root starts disposing.

PluginHost

What one root offers a plugin during and after setup.

Query

A module-scoped shared query handle. Bind a subscriber viacreateQuery(ctx, query, () => [...args]). The same Query value can be used by many controllers across many roots — each root has its own cache. Use bindQuery(ctx, query) or root.bindQuery(query) for imperative operations. Unbound operations reject/throw when more than one root has touched the query.

QueryActions

Imperative query operations bound to one root, without a subscription.

QueryDefaults

Defaults for every query, infinite query and createCache under a root, passed as queryEngine({ defaults }). Every field mirrors the same-named field on QuerySpec, and a per-query spec always wins:spec.X ?? defaults.X ?? <built-in default>. Spec §5.9.

Derived via Pick rather than re-declared so the types can't drift fromQuerySpec. None of the picked fields reference Args/T, which is why instantiating with never[] / unknown is safe here.

Deliberately NOT defaultable: - refetchInterval — a root-wide interval would silently start polling every query in the app. Opt in per query. - id / key / fetcher / meta — per-query identity and plugin settings; meaningless as an app-wide default.

Every default applies to infinite queries too. A focus or reconnect refetch of an infinite query re-fetches every loaded page.

QueryEngine

The query engine — pass one to createRoot to give the root a cache:

ts
createRoot(app, { deps, queries: queryEngine({ defaults: { staleTime: 30_000 } }) })

**Why it is a separate value.** query/client.ts is the largest module in the package. This module is the only one that imports QueryClient by value, so a root built without queries leaves the cache engine, the entry state machine, infinite pagination and the refetch triggers out of the bundle.

**It is a definition, not an instance.** Each root that adopts it gets its own QueryClient, so one engine value can be hoisted to module scope and shared — by several roots, or by a HydrationBoundary that rebuilds its root under StrictMode.

**The client is created eagerly**, inside createRoot, before plugin setup and the controller factory run, so a plugin's setup can already reach the cache. That matters: mutationQueuePlugin replays mutations persisted by a previous session from setup, which is a startup obligation and not a response to anything the current session does.

QueryEngineOptions

Options for queryEngine(...).

QueryHost

The root's query cache, addressed by query id and entry key (the output of the query's key(...)). Writes are stamped with the calling plugin's name as their origin.

QueryRef

A query as a plugin sees it: its identity, kind and plugin settings.

QuerySelectOptions

createQuery's options with a select projection: the subscription reportsselect(data) instead of the cached value. The projection runs per subscriber; the cache keeps the raw value.

QuerySpec

Configuration passed to defineQuery({ ... }). The Args tuple is what callers pass as cache keys and to the fetcher. Spec §20.4.

The fetcher's first argument is a FetchCtx (signal + deps); positional cache args come after. This shape lets module-scoped queries readctx.deps.api etc. — no setApiForQuery(api) module-level capture needed.

QuerySubscription

What createQuery(ctx, query, ...) returns: the query's AsyncState<T>.

QuerySubscriptionOptions

Options passed to createQuery(ctx, query, opts) to control the subscription (reactive key, enabled-gating). The key thunk reads signals — re-evaluating when they change re-keys the subscription.

A select projection that maps the underlying data shape to a view shape is accepted via a dedicated overload on createQuery rather than this options bag — the overload threads T → U types through cleanly.

ReadSignal

Read-only reactive value. Reading .value inside a tracking scope (computed / effect) registers a dependency; peek() reads without tracking; subscribe(handler) fires handler immediately with the current value and on every change until the returned unsubscribe is called.

RefetchInterval

Periodic background refetch while an entry has subscribers. A number is a fixed gap in ms. A function is resolved **once per scheduling decision** — on every tick, for the *next* gap — and receives the entry's latest data through a non-subscribing read:

ts
// Poll fast while there's work in flight, slowly when idle.
refetchInterval: (jobs) => (jobs?.some((j) => j.state === 'running') ? 1_000 : 30_000)

The contract:

  • **The gap must be a positive finite number — in both forms.** 0 / NaN / negative / Infinity stops the timer for that entry instead of scheduling a hot loop; dev builds warn. This covers a literal (refetchInterval: 0 never arms, where it used to mean "fetch every macrotask") as well as a thunk's return. Once stopped, the timer restarts only on the entry's next **0→1 subscriber transition** — a subscriber joining an entry that still has others does not re-arm it. - **A thunk must not throw.** A throw is treated exactly like a bad return — the chain stops, with a dev warning naming the throw (and carrying the error) so it can't die silently. Keep the thunk to a pure arithmetic decision over data; do the risky part elsewhere. - **The first resolution is synchronous, at acquire.** The 0→1 subscriber arms the chain before the initial fetch can settle, so the thunk's first call receives undefined (or hydrated/cached data if the entry already has some). Handle that argument rather than assuming a loaded entry. - **Not reactive.** Reading a signal inside the thunk gives its current value for that tick and registers no dependency — changing it later reschedules nothing. Drive the decision off the data argument. - **Per entry, not per subscriber.** The timer belongs to the shared cache entry, so ten controllers on one key share one interval. That's why QuerySubscriptionOptions has no refetchInterval: per-subscriber intervals need a "whose interval wins" rule and every answer to that surprises somebody. Same reason it stays out of QueryDefaults (§5.9) — a root-wide interval polls the entire app. - createCache (LocalCache) has no interval of any kind. This is a defineQuery / defineInfiniteQuery feature only.

For infinite queries T is the pages array (TPage[]) — whatever the entry stores, undefined until the first page lands. Spec §5.9.

RemoveEvent

A cache entry left the cache: its last subscriber went away and gcTime passed.

RetryDelay

Backoff in ms. A number is constant delay; a function computes per-attempt.

RetryPolicy

Retry policy for queries and mutations. false never retries, as 0 does. A number is a max-attempt count (default backoff). A function decides per-attempt (return true to retry).

Root

The handle createRoot(...) returns. The root controller's public api lives on api; everything else is the root's own surface — lifecycle, SSR, scope lookup, imperative query operations and the devtools bus. Keeping the two apart means a controller may return anything, including members nameddispose or suspend, and the root can grow new controls without taking a name from anyone's api. Spec §20.8.

RootOptions

Configuration passed to createRoot(def, options). deps is required and available everywhere as ctx.deps. onError receives errors from effects, mutations, caches, emitter handlers, and construction. hydrate replays aDehydratedState produced on the server. Spec §20.8.

Scope

Typed cross-tree data slot. Provided by an ancestor via ctx.provide(scope, value) and consumed anywhere in its subtree via ctx.inject(scope). Defined at module scope so the identity is stable across calls. See spec §10.3.

ScopeOptions

Options for defineScope.

Selection_2

Multi-select state for tables / lists with bulk actions (spec §16.5).

Plain function — not bound to ctx. Place it in a controller's closure so it dies with the closure. The phantom T parameter brands the selection by item type; IDs are always strings.

Signal

Writable reactive value. value is assignable; set(value) is the functional equivalent; update(fn) reads (peek) and writes the result offn(previous).

Snapshot

Returned by query.setData(...) or localCache.setData(...). Used bymutation.onMutate for optimistic-update rollback (spec §6.4).

  • rollback() restores the previous data state (and clears the "pending mutation" flag on the entry if no other snapshots are live). - finalize() commits the snapshot as the new truth — no rollback, hasPendingMutations clears once all live snapshots on the entry are finalized or rolled back. The snapshots still live below it keep the commit: a later rollback of one restores its baseline with the committed change in it. The mutation runner calls this on success; user code rarely needs to.

Both are idempotent and mutually exclusive (calling one disables the other). Safe to call after the owning entry has been disposed.

StandardSchemaV1

Standard Schema v1 — the cross-library validation contract adopted by Zod 4, Valibot 1, ArkType 2, and others. See https://standardschema.dev.`I` is the schema's input type and O its output type.

We type-only-import the shape so consumers don't take a new runtime dep: any object with a ~standard.validate(value) method conforming to this structure works. validator(schema) wraps one as a Validator.

StandardSchemaV1Issue

One failure a Standard Schema reports: its message, and the path to the value that failed. validator(schema) turns each into a FormIssue.

StandardSchemaV1Result

What a Standard Schema's validate returns: the parsed value, or the issues.

SubmitOptions

Options for Form.submit.

SubmitResult

What Form.submit resolves with. ok: true carries the handler's result.ok: false names why the handler did not succeed: - 'invalid' — pre-submit validation failed; every leaf is marked touched. - 'error' — the handler threw; error is the thrown value. - 'busy' — a submission was already in flight, so this one did not start. - 'disposed' — the form was disposed.

SuspendOptions

Options for root.suspend(options?).

TimingOptions

Options for debounced and throttled.

TimingSignal

A ReadSignal<T> returned by debounced / throttled. Extends the subscription surface with manual cancel() and flush().

  • cancel() drops any pending emission without firing. Useful when a navigation away from the screen should discard the latest in-flight draft instead of writing it through to the debounced output. - flush() immediately emits the pending value (if any). Useful at submit time: "commit whatever the user just typed without waiting for the debounce timer to fire."

Both are no-ops when nothing is pending.

ValidateOn

When a field's validators are first allowed to run.

  • 'change' (default) — validators run on every set(). Matches the current behavior, ideal for "type and see errors live." - 'blur' — first run is gated on markTouched(). After that, subsequent value changes do trigger re-validation. UI binding should call markTouched() on onBlur. - 'submit' — first run is gated on revalidate() / Form.submit(). After that, subsequent value changes re-validate. Use when you want "show errors only after the user explicitly tried to submit."

revalidate() always unlocks the field regardless of mode.

Validator

Checks one value: null when it passes, a message or FormIssue[] when it fails, returned at once or as a promise. signal aborts when a newer run supersedes this one and on dispose, so an async validator can cancel its request.

ValidatorResult

What a Validator may return synchronously. A plain string is a message on the node itself (equivalent to a FormIssue with an empty path);null means "no error"; a FormIssue[] targets specific descendants.

WriteEvent

A cache entry's data changed.

WriteOptions

Options for QueryHost.write, replace and setData.

WriteSource

What produced a cache write:

  • 'fetch' — a fetcher resolved (every page batch, for an infinite query) - 'hydrate' — dehydrated data reached the entry (SSR, a warm start) - 'optimistic' — setData, a guess a mutation may roll back - 'rollback' — an optimistic layer was undone - 'commit' — an optimistic layer was committed as server truth: its snapshot was finalized, as a successful mutation does. It is reported once no optimistic layer on the entry is live, so data holds no pending guess. A commit made while another layer is live is reported by the settle that clears the last one, even when that settle is a rollback, and then in place of the 'rollback'. updatedAt is when the server last answered (fetch, hydrated row or canonical write), since a commit does not restart the stale clock, or 0 when it never did. - 'write' — write, a canonical patch - 'replace' — replace, a canonical whole record

Released under the MIT License.