Rindle

API index and search · Build metadata

@rindle/react

0.0.0 · Public export map; development manifest version (0.0.0).

Source revision 05d0bf2c2e56 · build details
Source revision: 05d0bf2c2e56.
TypeScript input SHA-256: aabe6cfcc4172b870d5e272142958e9ea8d8784c2aa23133156e5a7ee633318e
Generated 2026-09-04T23:58:25.590Z with TypeScript 6.0.3. Public TypeScript checks and declaration emit passed. Package runtime tests are separate.

Entry point source

assembleDurableText

FunctionDeclaration · Source: packages/client/src/stream.ts:87 · Supporting declarations

The durable half of the splice for the mapped-table layout: the compacted body followed by whatever chunk rows have not been folded into it yet.

Chunks are ALWAYS the suffix after body — the closing checkpoint rewrites body and drops the chunks it absorbed in ONE transaction — so a reader never observes a torn state where a chunk both is and is not in the body.

export declare function assembleDurableText(message: {
    body?: string | null;
} | null | undefined, chunks?: ReadonlyArray<{
    seq: number;
    text: string;
}>): string;

DEFAULT_STREAM_ENDPOINT

VariableDeclaration · Source: packages/react/src/stream.ts:37 · Supporting declarations

Where {@link useStreamedText} subscribes by default — DEFAULT_RINDLE_API_ROUTES.stream, mirrored here rather than imported so the browser never pulls @rindle/api-server.

export declare const DEFAULT_STREAM_ENDPOINT = "/api/rindle/stream";

eventSourceTransport

FunctionDeclaration · Source: packages/react/src/stream.ts:81 · Supporting declarations

The default SSE transport. Absent EventSource (SSR, an older runtime, a test without jsdom) it attaches nothing and the reader stays on the durable plane — correct, just chunkier.

export declare function eventSourceTransport(onError?: (err: unknown) => void): StreamTransport;

Frag

FunctionDeclaration · Source: packages/react/src/index.ts:605 · Supporting declarations

Render-prop sugar over {@link useFragment}: does the null check once. from is a fragment ref (or null/undefined — an absent to-one relationship, an emptied .one(), or a row deleted out from under a live read); when the row is present children(data) renders, otherwise fallback (default nothing). Keeps the per-row subscription isolation — a child-only edit re-renders just this read.

export declare function Frag<F extends AnyFragment>({ of, from, fallback, releaseDelayMs, children }: {
    of: F;
    from: FragmentRef<F> | null | undefined;
    fallback?: ReactNode;
    /** Per-call-site grace window — see {@link QueryReleaseOptions}. */
    releaseDelayMs?: number;
    children: (data: FragmentData<F>) => ReactNode;
}): ReactNode;

Fragment

InterfaceDeclaration · Source: packages/client/src/query.ts:533 · Supporting declarations

A reusable, typed selection over a table — Relay's "fragment", promoted to a first-class value. Two verbs compose a fragment, on one axis — does its data belong to THIS row or to a related one:

  • SAME node — q.include(Frag): merge its selection into this node. This is also how you root a fragment onto a base query — queries.t.where.id(x).include(Frag).
  • CHILD node — q.sub(alias, child, corr, Frag): nest it under a relationship (sub's 4th arg).

A Fragment is therefore also a build transform ((q: Query<C>) => Query<C, Rels>); that call signature is the mechanism by which sub accepts a fragment as its build. Prefer include to root a fragment (canonical + merged) over calling it directly. It additionally carries its table (for {@link FragmentRef}/useFragment typing and masking). Composing fragments assembles ONE {@link Ast} → one materialization → one /query — the whole point (no request waterfall; design §1).

export interface Fragment<C extends AnyCols, Rels = {}, Sel extends string = never, LocalRels = Rels> {
    (q: Query<C>): Query<C, Rels, false, Sel, LocalRels>;
    readonly table: TableLike<C>;
    readonly [FRAGMENT_BRAND]: true;
}

FragmentData

TypeAliasDeclaration · Source: packages/client/src/query.ts:544 · Supporting declarations

The data returned by useFragment(fragment, ref): the fragment's own selected columns plus immediate relationship values. Relationships whose builder is another fragment surface as opaque {@link FragmentRef}s, so child-owned payload is read only by the child fragment reader.

export type FragmentData<F> = F extends Fragment<infer C, unknown, infer Sel, infer LocalRels> ? Projected<C, Sel> & LocalRels : never;

fragmentKey

FunctionDeclaration · Source: packages/client/src/query.ts:605 · Supporting declarations

export declare function fragmentKey(ref: FragmentRef<any>): string;

FragmentRef

TypeAliasDeclaration · Source: packages/client/src/query.ts:563 · Supporting declarations

The opaque token a parent passes to a component that reads {@link FragmentData} for F.

export type FragmentRef<F> = F extends Fragment<any, any, any, any> ? LocalFragmentRef<F> : never;

QueryCache

ClassDeclaration · Source: packages/react/src/index.ts:1044 · Supporting declarations

export declare class QueryCache {
    private readonly entries;
    private nextLeaseId;
    private readonly store;
    private readonly defaultReleaseDelayMs;
    constructor(store: Store<ColsMap>, opts?: QueryCacheOptions);
    /** `releaseDelayMs` overrides the cache default for THIS lease only (see
     *  {@link QueryReleaseOptions}). Ignored for a local-only store, whose views are always torn down
     *  on release. */
    retain<Q extends AnyQuery>(viewKey: string, query: Q, releaseDelayMs?: number): QueryLease;
    release(lease: QueryLease): void;
    subscribe(viewKey: string, listener: () => void): () => void;
    snapshot(viewKey: string, one: boolean): unknown;
    /** A query's current {@link ResultType} (from its view), or `unknown` before it is retained. */
    resultType(viewKey: string): ResultType;
    /** The SSR/hydration snapshot for `viewKey` — the store's preloaded/dehydrated seed, read
     *  WITHOUT retaining (the server never opens a subscription). Falls back to empty so a
     *  non-preloaded query renders like an unhydrated one. */
    serverSnapshot(viewKey: string, one: boolean): unknown;
    /** The SSR {@link ResultType}: `complete` when a seed exists (server-authoritative first paint),
     *  else `unknown`. */
    serverResultType(viewKey: string): ResultType;
    size(): number;
    private createSplitEntry;
    private createSplitLease;
    private createMaterializedEntry;
    private createMaterializedLease;
    private chooseCanonical;
    private setCanonical;
    private resolveDelay;
    /** Hand back every deferred remote lease and cancel the pending teardown, WITHOUT touching
     *  `entry.releaseDeadline`. Called when a retain revives the entry: the new lease covers the query,
     *  so the deferred ones are redundant, and the timer armed for an idle entry is stale. The deadline
     *  is not — it is an outstanding claim, and dropping it here would let a remount silently refresh a
     *  window that must only ever decay. */
    private flushPendingReleases;
    /** Arm (or re-arm) the teardown for `entry.releaseDeadline`. Because the deadline is an ABSOLUTE
     *  instant, re-arming is idempotent — a later release recomputes the same wake-up time instead of
     *  restarting the window. */
    private scheduleSplitRelease;
    private finalizeSplitRelease;
}

queryCacheKey

FunctionDeclaration · Source: packages/react/src/index.ts:1281 · Supporting declarations

export declare function queryCacheKey(query: AnyQuery): string;

QueryData

TypeAliasDeclaration · Source: packages/react/src/index.ts:49 · Supporting declarations

export type QueryData<Q extends AnyQuery> = ReturnType<Q["materialize"]>["data"];

QueryReleaseOptions

InterfaceDeclaration · Source: packages/react/src/index.ts:189 · Supporting declarations

Per-call-site override for how long a query is kept warm after its LAST subscriber unmounts.

The default (2s, or whatever <Rindle releaseDelayMs> sets) exists so a changed filter/limit can re-materialize from the still-warm local base while the replacement server lease streams its first answer — it's what keeps navigation from flashing empty. That grace window is wrong for queries you KNOW you will never come back to, the canonical case being typeahead search: every keystroke is a distinct query, so a 2s window leaves one dead view + server subscription open per character typed. Pass 0 there to tear down on unmount:

const results = useQuery(searchIssues(term), { releaseDelayMs: 0 });

Treat the value as a constant per call site — changing it re-leases the query (drops the old lease and takes a fresh one), which is wasted work if it changes every render.

The rule for a query several components share with DIFFERENT delays is a DEADLINE, not a duration: every release stamps now + that lease's delay, and the query stays warm until the latest deadline any of its leases asked for (max-wins over what REMAINS, matching the SSR preload TTL rule in @rindle/client's ssr.ts). Two consequences worth internalizing:

  • The clock starts when a subscriber LEAVES, never when it arrives — a mounted reader is never timed out, however long it stays.
  • A deadline expires on its own, so a later lease inherits at most the RESIDUE of an older window, never a fresh copy of it. Unmount a 2s reader, remount a releaseDelayMs: 0 one 1.9s later and drop it: teardown lands at the original 2s mark, not 1.9s past it.

Only meaningful against a backend that can retain remote queries. A local-only store (the SSR seed over OneShotBackend, or a store with no remote leg) always tears its views down on release, so there is no window to shorten.

export interface QueryReleaseOptions {
    /** ms to keep this query warm after the last subscriber unmounts. `0` = release immediately.
     *  Defaults to the provider's `releaseDelayMs` (2s). */
    releaseDelayMs?: number;
}

ResultType

TypeAliasDeclaration · Source: packages/client/src/types.ts:155 · Supporting declarations

A query's SERVER-CHANNEL state, surfaced on its {@link ArrayView } (FOLDED-MUTATIONS-DESIGN §7 — formerly conflated with pending-ness, OPTIMISTIC-WRITES-DESIGN.md §6):

  • unknown — not hydrated: the server has not produced a first result for this query yet;
  • complete — the server has answered. STAYS complete while a local mutation is pending (the prediction is the client's best current answer); reversion on rejection is an event (onRejected), not a downgrade of completeness;
  • error — RESERVED for a future server-side, query-level error signal (see designs/101-QUERY-ERRORS-DESIGN.md); no longer produced by a pending mutation. "Is a prediction pending here?" is now a separate reactive axis (the backend's pending(qid) / onPending), not folded into this type. A backend with no server lifecycle (the in-process engine) leaves every view complete.
export type ResultType = "unknown" | "complete" | "error";

Rindle

FunctionDeclaration · Source: packages/react/src/index.ts:221 · Supporting declarations

export declare function Rindle<S extends ColsMap>({ store, releaseDelayMs, children }: RindleProps<S>): import("react").FunctionComponentElement<import("react").ProviderProps<RindleContextValue | null>>;

RindleProps

InterfaceDeclaration · Source: packages/react/src/index.ts:77 · Supporting declarations

export interface RindleProps<S extends ColsMap = ColsMap> {
    store: Store<S>;
    /** Default grace window (ms) for every query in this tree — how long a view + its server lease are
     *  kept warm after the last subscriber unmounts. Defaults to 2s; see {@link QueryReleaseOptions}
     *  for why, and for the per-call-site override. Treat as a constant: changing it rebuilds the
     *  caches and tears down every live view. */
    releaseDelayMs?: number;
    children?: ReactNode;
}

RindleProvider

VariableDeclaration · Source: packages/react/src/index.ts:232 · Supporting declarations

export declare const RindleProvider: typeof Rindle;

RindleSSR

FunctionDeclaration · Source: packages/react/src/index.ts:268 · Supporting declarations

The SSR→SPA store handoff (SSR-DESIGN.md §6.1). Renders <Rindle> with a store that swaps from a transport-less SSR seed to the live engine WITHOUT changing a single useQuery caller — bind the app's schema + bootClient and drop it in above the tree:

  • Server render + browser HYDRATION: a seed {@link Store} over a {@link OneShotBackend}, hydrated from ssrState. useQuery reads its seed via getServerSnapshot, so the server and the client's first render produce identical markup with NO engine on either side — first paint is the server-rendered data, never a "Starting…" splash that would break hydration.
  • After hydration: boot() starts the wasm IVM engine (browser only), its views are seeded from the SAME snapshot (so the swap shows the SSR rows with no flash), and the live subscribe reconciles — the page is now a live SPA.

Framework-agnostic of the app: everything but schema/boot/ssrState is owned here (previously hand-rolled per app as src/RindleApp.tsx).

export declare function RindleSSR<S extends ColsMap>({ schema, ssrState, boot, children }: RindleSSRProps<S>): import("react").FunctionComponentElement<RindleProps<S>>;

RindleSSRProps

InterfaceDeclaration · Source: packages/react/src/index.ts:238 · Supporting declarations

export interface RindleSSRProps<S extends ColsMap = ColsMap> {
    /** The app schema — used to build the transport-less seed {@link Store} that backs the server
     *  render and the matching client hydration pass. */
    schema: Schema<S>;
    /** The dehydrated first-paint cache from the route loader (`ServerStore.dehydrate()`), embedded in
     *  the HTML. Read on BOTH the server render and the client's first (hydration) render. */
    ssrState: DehydratedState;
    /** Boots the live (wasm-backed) client in the BROWSER — the app's `bootClient`. Called once, after
     *  hydration, and must resolve to the live optimistic store. Never invoked during the server
     *  render (SSR seeds are a first-paint concern only). */
    boot: () => Promise<{
        store: Store<S>;
    }>;
    children?: ReactNode;
}

RootData

TypeAliasDeclaration · Source: packages/react/src/index.ts:50 · Supporting declarations

export type RootData<Q extends AnyQuery> = QueryLocalData<Q>;

RootDetails

InterfaceDeclaration · Source: packages/react/src/index.ts:53 · Supporting declarations

export interface RootDetails {
    readonly status: ResultType;
}

RootRefData

TypeAliasDeclaration · Source: packages/react/src/index.ts:51 · Supporting declarations

export type RootRefData<Q extends AnyQuery, F extends Fragment<any, any, any, any>> = QueryData<Q> extends readonly unknown[] ? readonly FragmentRef<F>[] : FragmentRef<F> | null;

RootRefResult

TypeAliasDeclaration · Source: packages/react/src/index.ts:57 · Supporting declarations

export type RootRefResult<Q extends AnyQuery, F extends Fragment<any, any, any, any>> = readonly [
    data: RootRefData<Q, F>,
    details: RootDetails
];

RootResult

TypeAliasDeclaration · Source: packages/react/src/index.ts:56 · Supporting declarations

export type RootResult<Q extends AnyQuery> = readonly [
    data: RootData<Q>,
    details: RootDetails
];

spliceStreamText

FunctionDeclaration · Source: packages/client/src/stream.ts:75 · Supporting declarations

Merge the durable plane with the live tail.

durable is what the IVM view shows; produced is what a subscription has accumulated (the prefix it joined at, plus every chunk). Both are prefixes of the same response, so the merge is "take the longer" — no diffing, no overlap handling, no ranges.

The length comparison is the whole algorithm, which is why a caller MUST seed its accumulator with the text it joined at: a tail carrying only the chunks it received would read as shorter than the durable text and be discarded. useStreamedText does that for you.

export declare function spliceStreamText(durable: string, produced: string): string;

StreamFrame

TypeAliasDeclaration · Source: packages/client/src/stream.ts:32 · Supporting declarations

One frame of a subscription. A subscription always begins with open and always ends with exactly one terminal frame — end, stale, or absent — after which the iterator completes.

stale and absent are the two "you are on the durable plane now" answers, and both are SAFE: the store holds everything below floorSeq and everything through durableSeq, so the reader's IVM view converges without the stream. Neither is an error.

export type StreamFrame = 
/** Join accepted. `from` is the (clamped) offset the replay starts at. */
{
    type: "open";
    streamId: string;
    from: number;
    seq: number;
    durableSeq: number;
    ended: boolean;
}
/** PRODUCED text — not a durability claim. `text.length === seq - from`, always. */
 | {
    type: "chunk";
    from: number;
    seq: number;
    text: string;
}
/** The store now holds the prefix through `seq`. */
 | {
    type: "durable";
    seq: number;
}
/** Sealed. No further frames. */
 | {
    type: "end";
    seq: number;
    status: StreamStatus;
    error?: string;
}
/** `from` is below the producer's retained buffer floor (or the subscriber fell too far behind):
 *  read the store. (A raw `EventSource` rejoins automatically on its reconnect; `useStreamedText`
 *  deliberately stays on the durable plane instead — correct, at checkpoint granularity.) */
 | {
    type: "stale";
    floorSeq: number;
    durableSeq: number;
}
/** The process serving this subscribe is not hosting the stream (wrong instance, already evicted,
 *  or it never existed): the store is the whole truth. */
 | {
    type: "absent";
};

StreamStatus

TypeAliasDeclaration · Source: packages/client/src/stream.ts:19 · Supporting declarations

How a stream ended.

  • complete — the model finished.
  • cancelled — the reader asked it to stop and the producer honoured it.
  • error — the generation threw.
  • interrupted — the host went away mid-generation. The one status that implies the store may be short of what was produced.
export type StreamStatus = "complete" | "cancelled" | "error" | "interrupted";

streamSubscribeUrl

FunctionDeclaration · Source: packages/react/src/stream.ts:74 · Supporting declarations

<endpoint>?streamId=…&from=…. from is the join offset; a reconnecting EventSource overrides it with its own Last-Event-ID header, which the server prefers.

export declare function streamSubscribeUrl(endpoint: string, streamId: string, from: number): string;

StreamTransport

InterfaceDeclaration · Source: packages/react/src/stream.ts:44 · Supporting declarations

How the live plane is reached. The default ({@link eventSourceTransport}) is SSE, which is what the api-server's streamFramesToSse serves and what gets Last-Event-ID resume for free. Supply your own for a WebSocket, a fetch-stream, or a test.

export interface StreamTransport {
    /** Attach at `url` and call `onFrame` per decoded frame. MUST return a detach function; it may be
     *  called more than once and must tolerate that. `onFrame` may be called synchronously. */
    subscribe(url: string, onFrame: (frame: StreamFrame) => void): () => void;
}

SyncQueryCache

ClassDeclaration · Source: packages/react/src/index.ts:923 · Supporting declarations

export declare class SyncQueryCache {
    private readonly entries;
    private nextLeaseId;
    private readonly store;
    private readonly defaultReleaseDelayMs;
    constructor(store: Store<ColsMap>, opts?: QueryCacheOptions);
    /** `releaseDelayMs` overrides the cache default for THIS lease only (see
     *  {@link QueryReleaseOptions}) — `0` asks for no warm window of its own, though an unexpired
     *  deadline from an earlier lease on this coverage still applies. */
    retain(coverageKey: string, query: AnyQuery, releaseDelayMs?: number): SyncLease;
    release(lease: SyncLease): void;
    subscribe(coverageKey: string, listener: () => void): () => void;
    resultType(coverageKey: string): ResultType;
    size(): number;
    private createHandle;
    private resolveDelay;
    /** Arm (or re-arm) the teardown for `entry.releaseDeadline`. Because the deadline is an ABSOLUTE
     *  instant, re-arming is idempotent — a later release recomputes the same wake-up time instead of
     *  restarting the window. */
    private scheduleRelease;
    private finalizeRelease;
}

useFragment

FunctionDeclaration · Source: packages/react/src/index.ts:591 · Supporting declarations

Read a {@link Fragment}'s local data from an opaque ref.

The query boundary calls {@link useRoot} with a fragment argument to retain the full named coverage query and receive root refs. Descendants call useFragment with those refs (or child refs returned by a parent fragment read) to open narrow local-only reads for the fields their fragment owns.

ref is an opaque token created by {@link useRoot} or returned from another local fragment read. The hook opens a narrow local-only query for this exact fragment and keeps the root coverage lease retained while mounted. Passing a legacy projected data object is unsupported.

export declare function useFragment<F extends Fragment<any, any, any, any>>(fragment: F, ref: FragmentRef<F> | null | undefined, opts?: QueryReleaseOptions): FragmentData<F> | null;

useQuery

FunctionDeclaration · Source: packages/react/src/index.ts:302 · Supporting declarations

export declare function useQuery<Q extends AnyQuery>(query: Q, opts?: QueryReleaseOptions): QueryData<Q>;

useQueryStatus

FunctionDeclaration · Source: packages/react/src/index.ts:343 · Supporting declarations

The SERVER-CHANNEL state of a query's view (@rindle/client {@link ResultType}): unknown while it loads (not yet server-authoritative), complete once the server has answered. A pending optimistic mutation no longer moves this — that is a separate axis now (FOLDED-MUTATIONS-DESIGN §7); the error variant is reserved and currently unproduced. Shares the same cached/leased view as {@link useQuery} (so reading both for one query is one subscription), and re-renders only when the status changes.

export declare function useQueryStatus(query: AnyQuery, opts?: QueryReleaseOptions): ResultType;

useRindleStore

FunctionDeclaration · Source: packages/react/src/index.ts:234 · Supporting declarations

export declare function useRindleStore<S extends ColsMap = ColsMap>(): Store<S>;

useRoot

FunctionDeclaration · Source: packages/react/src/index.ts:411 · Supporting declarations

Run a named root query and expose its local React-facing data. Fragment child relationships are refs, so child components can keep owning their own local reads. Passing a root fragment as the final argument switches the result to opaque root refs for that fragment.

export declare function useRoot<Q extends AnyQuery>(query: Q): RootResult<Q>;
export declare function useRoot<Q extends AnyQuery, F extends AnyFragment>(query: Q, fragment: F): RootRefResult<Q, F>;
export declare function useRoot<Q extends AnyQuery>(query: NamedQuery<void, [
], Q>): RootResult<Q>;
export declare function useRoot<Q extends AnyQuery, F extends AnyFragment>(query: NamedQuery<void, [
], Q>, fragment: F): RootRefResult<Q, F>;
export declare function useRoot<Args, Ctx extends readonly unknown[], Q extends AnyQuery>(query: NamedQuery<Args, Ctx, Q>, args: Args, ...ctx: Ctx): RootResult<Q>;
export declare function useRoot<Args, Ctx extends readonly unknown[], Q extends AnyQuery, F extends AnyFragment>(query: NamedQuery<Args, Ctx, Q>, args: Args, ...ctxAndFragment: [
    ...ctx: Ctx,
    fragment: F
]): RootRefResult<Q, F>;

useStreamedText

FunctionDeclaration · Source: packages/react/src/stream.ts:131 · Supporting declarations

The response text as it should be rendered right now: the durable prefix spliced with the live tail.

const data = useFragment(MessageFragment, message);
const text = useStreamedText({
  streamId: data.id,
  durable: assembleDurableText(data, data.chunks),
  live: data.status === "streaming" || data.status === "pending",
});

The value is monotone in practice — it only grows while a stream runs — and when the closing checkpoint compacts the chunks into the body it returns the identical string, so there is no flicker at the handoff.

export declare function useStreamedText({ streamId, durable, live }: UseStreamedTextInput, options?: UseStreamedTextOptions): string;

UseStreamedTextInput

InterfaceDeclaration · Source: packages/react/src/stream.ts:50 · Supporting declarations

export interface UseStreamedTextInput {
    /** The stream's id — the message row's key. Changing it drops the old tail and rejoins. */
    streamId: string;
    /** What the IVM view shows: `assembleDurableText(message, message.chunks)`. Read from a ref
     *  internally (TRAP 1), so it may change every checkpoint without disturbing the connection. */
    durable: string;
    /** Whether a producer is still running — the app's own read of its status column (typically
     *  `status === "streaming" || status === "pending"`). The live leg attaches only while true. */
    live: boolean;
}

UseStreamedTextOptions

InterfaceDeclaration · Source: packages/react/src/stream.ts:61 · Supporting declarations

export interface UseStreamedTextOptions {
    /** Default {@link DEFAULT_STREAM_ENDPOINT}. `streamId` and `from` are appended as query params. */
    endpoint?: string;
    /** Default {@link eventSourceTransport}. Read at subscribe time, NOT a dependency — an inline
     *  literal would otherwise reconnect on every render. */
    transport?: StreamTransport;
    /** A frame that could not be decoded, or a transport-level error. The durable plane is unaffected,
     *  so this is a diagnostic, not a failure. */
    onError?: (err: unknown) => void;
}

useSyncQuery

FunctionDeclaration · Source: packages/react/src/index.ts:376 · Supporting declarations

Retain a named server query for normalized/local-first sync coverage without subscribing React to that query's broad result tree. The returned value is lifecycle state only; it is unknown until the backend reports that the retained coverage has hydrated.

export declare function useSyncQuery(query: AnyQuery, opts?: QueryReleaseOptions): ResultType;