API index and search · Build metadata
@rindle/normalized
0.0.0 · Public export map; development manifest version (0.0.0).
aggTableName
FunctionDeclaration · Source: packages/normalized/src/agg-table.ts:71 · Supporting declarations
The synthetic base-table name for a relationship count aggregate — the byte-exact twin
of Rust agg_table_name (normalize.rs §3.1). A content hash of the aggregate definition
(child table, kind, group key = correlation child fields, child where); the parent
correlation field is excluded. Same definition ⇒ same name (cross-query sharing); a
different filter ⇒ a different name (no (table, pk) collision).
export declare function aggTableName(csq: CorrelatedSubquery): string;aggTableSchemas
FunctionDeclaration · Source: packages/normalized/src/agg-table.ts:218 · Supporting declarations
Every synthetic aggregate table ast surfaces (recursively, a nested aggregate under a
materialized relationship included), as flat table schemas — the twin of Rust
agg_table_schemas. Columns [childField…, "count"]; PK = the leading group columns.
export declare function aggTableSchemas(ast: Ast, isLocal?: (table: string) => boolean): NormalizedTableSchema[];ColCounts
TypeAliasDeclaration · Source: packages/normalized/src/sync.ts:47 · Supporting declarations
Per-table FULL column count — the width of a base/union row. Needed to scatter a projected (narrower) wire row into the shared positional layout (PROJECTION-SUPPORT-DESIGN.md §5.3).
export type ColCounts = Record<string, number>;createNormalizedStore
FunctionDeclaration · Source: packages/normalized/src/backend.ts:439 · Supporting declarations
A local-first {@link Store} whose base tables are fed by a server's normalized stream.
The returned Store is the ordinary @rindle/client Store — store.query…materialize() and
store.write(…) work as always; only the backend composition differs.
export declare function createNormalizedStore<S extends ColsMap>(schema: Schema<S>, source: NormalizedSource): Store<S>;NormalizedBackend
ClassDeclaration · Source: packages/normalized/src/backend.ts:72 · Supporting declarations
export declare class NormalizedBackend<S extends ColsMap> implements Backend {
private readonly local;
private readonly sync;
private readonly source;
private handler;
/** The Store's commit-boundary handler ({@link Backend.onCommitBoundary}), forwarded from the
* local engine's `dispatch` brackets so the Store folds every affected view before notifying any
* subscriber (cross-view-atomic notification). A normalized batch becomes net base mutations
* applied to the local engine in one commit, which fans to every view reading those rows — this
* carries that commit's boundary up so all those views notify together. */
private boundaryHandler;
private readonly devObservers;
private readonly remoteSubs;
private readonly localToRemote;
private readonly remoteRetainToLocal;
private readonly sourceToRemote;
private readonly resultTypes;
private readonly hydrated;
/** Local qids that are JUST hydrating on the in-flight `onNormalized` snapshot — set only for the
* duration of its `local.mutate`, so the local-event forwarder stamps their fold `catchUp`: the
* whole first result set arrives as a `batch` (we hydrate the view by mutating the embedded engine,
* which speaks in batches — there is no second `snapshot`), and `catchUp` is the flag that tells the
* Store this batch IS hydration (retire the SSR seed, phase it as a snapshot for narration). The
* optimistic backend does the identical thing across its reconcile cycle. `null` outside a hydrate. */
private catchUpQids;
private resultTypeHandler;
/** The client's OWN typed per-table schemas (for CRIT#4 validation), fixed at construction. */
private readonly clientTables;
/** Synthetic aggregate tables (`__agg_*`) registered so far, by name (§3.3). Per aggregate
* DEFINITION (not per query), so two queries over the same count share one table. */
private readonly synthetic;
/** table → (column name → base ColId), for mapping a projected hello to base positions. */
private readonly colIndex;
/** table → full column count, to detect whether a hello's table is projected. */
private readonly colCounts;
/** Synthetic table name → how many registered queries reference it. Materialized on `0→1`,
* reclaimed (engine source + refcount layer) on `1→0` — so aggregate state is not permanent (§4). */
private readonly syntheticRefs;
/** queryId → the synthetic tables it referenced at registration, to decrement on teardown. */
private readonly queryAggTables;
constructor(schema: Schema<S>, source: NormalizedSource);
registerQuery(qid: QueryId, ast: Ast, remote?: RemoteQuery): void;
/** Register every synthetic aggregate table `ast` needs that we haven't seen yet: on the
* local engine (so it can join to it), on `NormalizedSync` (so its rows refcount/GC by
* group key), and into the source's expected-schema set (so the server's `hello` — which
* advertises the same table — passes CRIT#4 validation, the client deriving the schema
* identically). Idempotent across queries that share an aggregate definition. */
private ensureSyntheticTables;
/** Decrement each synthetic table query `qid` referenced; remove the ones that reach 0 (no
* reader left) from the engine + refcount layer — aggregate state reclaimed, not permanent
* (§4). Runs AFTER `local.unregisterQuery(qid)` so the source has no live connection when
* `unregisterTable` frees it. */
private releaseSyntheticTables;
unregisterQuery(qid: QueryId): void;
retainRemoteQuery(qid: QueryId, remote: RemoteQuery, localQueryId?: QueryId, ast?: Ast): void;
releaseRemoteQuery(qid: QueryId): void;
/** Writes are authoritative-only here: send to the server, the stream reconciles locally.
* (Optimistic local apply + rebase is Slice 6.) */
mutate(mutations: Mutation[]): Promise<void>;
onEvent(handler: (qid: QueryId, ev: ChangeEvent) => void): void;
onCommitBoundary(handler: (phase: "begin" | "end") => void): void;
onResultType(handler: (qid: QueryId, rt: ResultType) => void): void;
__attachDevtoolsServerDeltas(observer: BackendDevObserver): () => void;
private onNormalized;
private emitServerDelta;
private localQidsForSource;
private retainRemote;
private releaseRemote;
private markSubHydrated;
private hasRemoteDependency;
private setResultType;
private recomputeResultType;
}NormalizedEvent
TypeAliasDeclaration · Source: packages/client/src/types.ts:123 · Supporting declarations
A per-query NORMALIZED stream event — the path-free twin of {@link ChangeEvent}. The
hello carries the flat per-table schemas (no nested view schema, §3); snapshot/batch
carry table-tagged {@link NormalizedOp}s. The NormalizedSync layer folds these into the
local engine's base tables.
cv (the global commit version the frame's data reflects — OPTIMISTIC-WRITES-DESIGN.md
§8.3/§8.6) is present on sources that speak the optimistic protocol; the plain normalized
path may omit it.
export type NormalizedEvent = {
type: "hello";
tables: NormalizedTableSchema[];
comparatorVersion: number;
normalizedFp: string;
} | {
type: "snapshot";
ops: NormalizedOp[];
cv?: number;
} | {
type: "batch";
ops: NormalizedOp[];
cv?: number;
};NormalizedOp
TypeAliasDeclaration · Source: packages/client/src/types.ts:100 · Supporting declarations
A table-tagged, path-free row delta — the normalized wire payload (the path-free twin
of {@link FlatChange}; NORMALIZED-CHANGES-DESIGN.md §3). Rows are positional (bare cells; a
json column is its raw JSON string). op is the discriminant. Lives here (not in
@rindle/normalized) so both the protocol (@rindle/remote) and the sync layer share one type.
export type NormalizedOp = {
table: string;
op: "add";
row: WireValue[];
} | {
table: string;
op: "remove";
row: WireValue[];
} | {
table: string;
op: "edit";
old: WireValue[];
new: WireValue[];
};NormalizedSource
InterfaceDeclaration · Source: packages/client/src/types.ts:221 · Supporting declarations
The server side of the normalized local-first path (NORMALIZED-CHANGES-DESIGN.md §5/§7):
registers queries and pushes each one's normalized footprint stream ({@link NormalizedEvent}s).
Both the in-process native (@rindle/replica) source and the ws (@rindle/remote
RemoteNormalizedSource) implement it; @rindle/normalized's NormalizedBackend consumes one,
never knowing which — a sibling seam of {@link Backend} for the normalized composition.
export interface NormalizedSource {
registerQuery(queryId: QueryId, remote: RemoteQuery): void;
unregisterQuery(queryId: QueryId): void;
/** Send base-table writes to the server; the authoritative stream flows back. */
mutate(mutations: Mutation[]): Promise<void>;
onNormalized(handler: (queryId: QueryId, event: NormalizedEvent) => void): void;
/** Optional: the backend hands its OWN typed per-table schemas so the source can validate
* each server `hello` against them (column order / PK by name) and reject a schema skew
* rather than silently transposing positional cells (CRIT#4 / §3 "drift ⇒ re-subscribe").
* Sources that can't skew (the in-process native source) may omit it. */
expectClientSchema?(tables: NormalizedTableSchema[]): void;
}NormalizedSync
ClassDeclaration · Source: packages/normalized/src/sync.ts:78 · Supporting declarations
export declare class NormalizedSync {
private readonly pkCols;
/** Per-table full width, for scattering projected rows. Empty ⇒ projection unsupported
* (every query must be `'*'`), which is the pre-projection behavior. */
private readonly colCounts;
/** `(table, pk)` key → the one shared base entry. */
private readonly base;
/** queryId → the set of `(table, pk)` keys that query currently footprints. */
private readonly qfoot;
/** queryId → (table → the base ColIds it contributes for that table, in the order its wire
* rows are positional against — its `required_cols` for the table, §4.1). A query spans
* multiple base tables (root + related/EXISTS children), each projected independently; a
* table absent from the inner map ⇒ that query syncs it `'*'` (all columns, full width). */
private readonly qcols;
constructor(pkCols: PkCols, colCounts?: ColCounts);
/**
* Register a query's projection for one base table (PROJECTION-SUPPORT-DESIGN.md §4.1): the
* base ColIds it contributes for `table`, in the order that table's wire rows are positional
* against. Omit a table (or never call) for a `'*'` table — it then contributes every column
* and its rows are full width. Must be called before the query's first batch.
*
* A `cols[i] < 0` entry is a DROP sentinel: wire column `i` is an EXPANDED server column the
* client doesn't have (the server side of an `expand-then-contract` migration), so its cell is
* discarded rather than scattered. The query still contributes only its real (`>= 0`) ColIds.
*/
registerProjection(queryId: QueryId, table: string, cols: number[]): void;
/**
* Drop a query's projection for one table, reverting it to `'*'` (full presence, rows scattered
* verbatim). The inverse of {@link registerProjection}; a no-op if none was registered. Needed
* because `qcols` persists across re-hydrate epochs: if a live subscription's hello narrows from
* an EXPANDED layout (a `-1`-bearing map) back to an exact full-width one, the stale map must be
* cleared or it would mis-scatter the now-exact rows. A no-op for an unprojected (`'*'`) table.
*/
unregisterProjection(queryId: QueryId, table: string): void;
/**
* Register a table's primary-key columns after construction — for a **synthetic aggregate
* table** (`AGGREGATE-SYNC-DESIGN.md` §3.3): a relationship `count` is synced as a
* server-authoritative `__agg_*` base table that is not in the client's typed schema, so
* the backend registers it here (and on the local engine) as queries that use it arrive.
* Idempotent — re-registering the same table (a second query over the same aggregate)
* is a no-op. Once registered, its rows refcount/GC exactly like any base table.
*/
registerTable(table: string, primaryKey: number[]): void;
/**
* The inverse of {@link registerTable} for a synthetic aggregate table whose last
* referencing query is gone (`AGGREGATE-SYNC-DESIGN.md` §4): drop its primary-key
* registration so the table is unknown again. A balanced stream has already GC'd its rows
* at the `1→0` transition (via {@link dropQuery}); defensively this also sweeps any residual
* base rows + per-query footprint entries for the table, so a later re-registration of the
* same name starts clean. A no-op for an unregistered table.
*/
unregisterTable(table: string): void;
/**
* Apply one query's normalized batch (its hydrate snapshot or one transaction's ops) and
* return the NET base-table mutations to commit to the wasm `Db` in a single transaction.
* Cross-query refcount + per-query dedup + column union (§4, §5):
* - `add`: counted into this query's footprint once; on the base `0→1` transition the row
* enters at this query's projection; on a `1→N` transition the query's columns are merged
* into the shared union (widen) and an `edit` is forwarded if the union changed.
* - `remove`: on the base `N→0` transition the row leaves; on `N→M>0` the union is recomputed
* from the remaining queries and an `edit` narrows the shared row.
* - `edit`: column-merge this query's cells into the shared union; forward once (idempotent
* across queries — same source row, same values).
*/
applyBatch(queryId: QueryId, ops: NormalizedOp[]): Mutation[];
/**
* Re-hydrate one query under a new epoch (§5.3): the server re-sent `queryId`'s whole
* footprint as seq-0 `add`s. Diff the new footprint against the query's current one —
* rows only in the old set leave (refcount out, GC/narrow), rows only in the new set enter
* (refcount in / widen), and an intersecting row whose value changed during the gap is an
* edit. Only `queryId`'s references move; other queries' counts are untouched. Returns the
* net mutations to commit.
*/
rehydrate(queryId: QueryId, snapshot: NormalizedOp[]): Mutation[];
/**
* Drop a query (§5.1): decrement its footprint's refcounts, GC each row at the last
* reference (or narrow the union if others remain), and forget the query. `O(footprint)`,
* no per-row scan. Returns the net mutations. (The caller also tells the server to
* deregister the stream.)
*/
dropQuery(queryId: QueryId): Mutation[];
/** The number of distinct base rows currently synced into the local store. */
baseSize(): number;
/** How many queries currently reference `(table, row)` (0 if absent). The lookup keys by PK,
* so a projected `row` need only carry its PK columns at the base positions. */
refCount(table: string, row: UnionCell[]): number;
/**
* The synced (server-authoritative) row currently held for `(table, pkCells)`, or
* `undefined` if no query references it. `pkCells` are the primary-key cells in
* `primaryKey` order (NOT a full row) — the same key the cross-query refcount uses.
*
* The optimistic aggregate overlay (`AGGREGATE-SYNC-DESIGN.md` §4) reads the server's
* `__agg` count cell through this so it can compute `displayed = server_base ⊕ delta`
* from the authoritative base rather than re-deriving it from the local engine's head
* (which already carries the optimistic layer — a torn read).
*/
baseRow(table: string, pkCells: WireValue[]): (WireValue | undefined)[] | undefined;
private footOf;
/** Scatter a query's (possibly narrower) wire row for `table` into the shared full-width
* positional layout. No registered projection for `(queryId, table)` ⇒ a `'*'` table whose
* row is already full width (returned as-is). Otherwise allocate a full-width row of
* `ABSENT` and place `row[i]` at `cols[i]`. */
private scatter;
/** Column-merge `incoming`'s present cells over `e.row` (a widen / pure-value change) and
* forward a single `edit` if the union changed. Both rows are full width. */
private mergeInto;
private add;
private remove;
private removeKey;
private edit;
/** The columns present on a `table` row footprinted by `queries`: the union of their
* projections for that table. `null` ⇒ all columns present (some referencing query syncs
* the table `'*'`), so the row is full. */
private presentCols;
private key;
}NormalizedTableSchema
InterfaceDeclaration · Source: packages/client/src/types.ts:107 · Supporting declarations
One base table's flat schema on the normalized hello (§3): column names (in order) +
primary-key column indices. Wire rows are positional against columns.
export interface NormalizedTableSchema {
name: string;
columns: string[];
primaryKey: number[];
}PkCols
TypeAliasDeclaration · Source: packages/normalized/src/sync.ts:43 · Supporting declarations
Per-table primary-key column indices (into a positional row) — how (table, pk) is keyed.
export type PkCols = Record<string, number[]>;rewriteAggregates
FunctionDeclaration · Source: packages/normalized/src/agg-table.ts:259 · Supporting declarations
Rewrite an AST for the LOCAL engine: each relationship count becomes a precomputed,
source-backed singular relationship over its synthetic table — read the server's count
with a plain join + the same scalar projection (aggregatePrecomputed), never a reduce
(which would recount the already-aggregated rows). Non-aggregate relationships recurse
(a nested aggregate is rewritten too); the parent's frame is otherwise untouched, so the
view-schema slot order is preserved. Returns a new AST; the input is not mutated.
The where tree is rewritten the SAME way (PARENT-AGGREGATE-FILTER-DESIGN.md §3): a
having_count parent gate lowers to an EXISTS whose subquery CLONES the display count_as
and adds a post-aggregation HAVING. That is a relationship count like any other, so §3's
premise — the client never recomputes a count, it lacks the child rows — applies to it too;
left un-rewritten the gate would reduce over child rows the server (rightly) does not sync
and every parent would fail it. agg_table_name hashes neither alias nor having, so the
gate resolves to the SAME __agg_* table the display count_as already registers and syncs:
the rewrite costs no extra table, no extra rows, and reads a count the server already sent.
Its counterpart is the server pruning the gate's witnesses from the footprint (Rust
table_tree) — that prune is only sound BECAUSE of this rewrite, so ship this side first.
export declare function rewriteAggregates(ast: Ast, isLocal?: (table: string) => boolean): Ast;