@rindle/wasm 0.1.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -0,0 +1,36 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ export const memory: WebAssembly.Memory;
4
+ export const __rindle_wasm_start: () => void;
5
+ export const __wbg_db_free: (a: number, b: number) => void;
6
+ export const __wbg_rindleview_free: (a: number, b: number) => void;
7
+ export const __wbg_writetxn_free: (a: number, b: number) => void;
8
+ export const db_destroyQuery: (a: number, b: number) => void;
9
+ export const db_new: () => number;
10
+ export const db_query: (a: number, b: number, c: any) => [number, number, number];
11
+ export const db_registerTable: (a: number, b: number, c: number, d: any) => [number, number];
12
+ export const db_serverBatchBegin: (a: number, b: any) => [number, number];
13
+ export const db_serverBatchEnd: (a: number) => [number, number, number];
14
+ export const db_unregisterTable: (a: number, b: number, c: number) => [number, number];
15
+ export const db_write: (a: number) => number;
16
+ export const rindleview_build: (a: any, b: any, c: any) => [number, number, number];
17
+ export const rindleview_data: (a: number) => any;
18
+ export const rindleview_flush: (a: number) => void;
19
+ export const rindleview_push: (a: number, b: number, c: number, d: any) => [number, number];
20
+ export const rindleview_resultType: (a: number) => any;
21
+ export const rindleview_setResultType: (a: number, b: number, c: number) => void;
22
+ export const rindleview_subscribe: (a: number, b: any) => number;
23
+ export const writetxn_add: (a: number, b: number, c: number, d: any) => [number, number];
24
+ export const writetxn_commit: (a: number) => [number, number, number];
25
+ export const writetxn_edit: (a: number, b: number, c: number, d: any, e: any) => [number, number];
26
+ export const writetxn_get: (a: number, b: number, c: number, d: any) => [number, number, number];
27
+ export const writetxn_remove: (a: number, b: number, c: number, d: any) => [number, number];
28
+ export const writetxn_rollback: (a: number) => void;
29
+ export const __wbindgen_malloc: (a: number, b: number) => number;
30
+ export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
31
+ export const __wbindgen_exn_store: (a: number) => void;
32
+ export const __externref_table_alloc: () => number;
33
+ export const __wbindgen_externrefs: WebAssembly.Table;
34
+ export const __wbindgen_free: (a: number, b: number, c: number) => void;
35
+ export const __externref_table_dealloc: (a: number) => void;
36
+ export const __wbindgen_start: () => void;
package/src/index.ts ADDED
@@ -0,0 +1,205 @@
1
+ // @rindle/wasm — the WASM backend: the in-process IVM engine (src/wasm/db.rs) behind the
2
+ // @rindle/client `Backend` seam (WASM-CLIENT-DESIGN.md §2.1). Also re-exports @rindle/client so a
3
+ // local app can import everything from here.
4
+ //
5
+ // The wasm is a `--target web` ESM artifact (packages/wasm/pkg, built by ./build.sh): it
6
+ // needs an explicit, one-time init — `await initWasm()` — before constructing a backend.
7
+ // In Node the bytes are read from the package; in a browser/bundler the wasm is fetched.
8
+
9
+ import init, { Db } from "../pkg/rindle.js";
10
+ import { Store, tableSpec } from "@rindle/client";
11
+ import type {
12
+ Ast,
13
+ Backend,
14
+ ChangeEvent,
15
+ ColsMap,
16
+ Mutation,
17
+ QueryId,
18
+ Schema,
19
+ } from "@rindle/client";
20
+
21
+ export * from "@rindle/client";
22
+
23
+ // The wasm Db surface this backend uses (kept local so it doesn't depend on the generated .d.ts).
24
+ interface WasmDb {
25
+ registerTable(table: string, spec: { columns: string[]; primaryKey: number[] }): void;
26
+ unregisterTable(table: string): void;
27
+ query(queryId: number, ast: Ast): { comparatorVersion: number; schema: unknown; snapshot: unknown[] };
28
+ destroyQuery(queryId: number): void;
29
+ write(): WasmWriteTxn;
30
+ serverBatchBegin(deltas: ServerDeltaOp[]): void;
31
+ serverBatchEnd(): Array<{ queryId: number; events: unknown[] }>;
32
+ }
33
+
34
+ /** A raw staged write transaction over the wasm engine — the surface an optimistic client
35
+ * mutator runs against (`get` reads the live state under this txn's staged overlay, so a
36
+ * read-dependent mutator sees its own writes; OPTIMISTIC-WRITES-DESIGN.md §4.1). */
37
+ export interface WasmWriteTxn {
38
+ add(table: string, row: unknown[]): void;
39
+ remove(table: string, row: unknown[]): void;
40
+ edit(table: string, oldRow: unknown[], newRow: unknown[]): void;
41
+ get(table: string, pk: unknown[]): unknown[] | undefined;
42
+ commit(): Array<{ queryId: number; events: unknown[] }>;
43
+ rollback(): void;
44
+ }
45
+
46
+ /** One base-table row op of a coherent server delta (the §1.3 `D`), bare cells. */
47
+ export type ServerDeltaOp =
48
+ | { table: string; type: "add"; row: unknown[] }
49
+ | { table: string; type: "remove"; row: unknown[] }
50
+ | { table: string; type: "edit"; row: unknown[]; old: unknown[] };
51
+
52
+ let initialized: Promise<void> | null = null;
53
+
54
+ /** Initialize the wasm module (idempotent). Call once at startup before `new WasmBackend`
55
+ * / `createWasmStore`. Browser/bundler: no args (the wasm is fetched). Node: the bytes are
56
+ * read from the package. Pass `moduleOrPath` to override (a `WebAssembly.Module`, URL, or bytes). */
57
+ export function initWasm(moduleOrPath?: unknown): Promise<void> {
58
+ if (!initialized) {
59
+ initialized = (async () => {
60
+ if (moduleOrPath !== undefined) {
61
+ await init({ module_or_path: moduleOrPath });
62
+ } else if ((globalThis as { process?: { versions?: { node?: string } } }).process?.versions?.node) {
63
+ const { readFile } = await import("node:fs/promises");
64
+ const bytes = await readFile(new URL("../pkg/rindle_bg.wasm", import.meta.url));
65
+ await init({ module_or_path: bytes });
66
+ } else {
67
+ await init();
68
+ }
69
+ })();
70
+ }
71
+ return initialized;
72
+ }
73
+
74
+ /** The in-process WASM backend. Requires {@link initWasm} to have resolved first. */
75
+ /** One engine commit's per-query batches (the shape `WriteTxn.commit` / `serverBatchEnd` return). */
76
+ type BatchSet = Array<{ queryId: number; events: unknown[] }>;
77
+
78
+ export class WasmBackend<S extends ColsMap> implements Backend {
79
+ private readonly db: WasmDb;
80
+ private handler: (qid: QueryId, ev: ChangeEvent) => void = () => {};
81
+
82
+ // Non-reentrant delivery (#15): commits dispatch through a FIFO queue. A subscriber that
83
+ // synchronously triggers another write enqueues that commit's batches; they drain only after
84
+ // the in-flight commit's batches finish, preserving per-query commit order regardless of what
85
+ // subscribers do.
86
+ private readonly dispatchQueue: BatchSet[] = [];
87
+ private draining = false;
88
+
89
+ constructor(schema: Schema<S>) {
90
+ this.db = new Db() as unknown as WasmDb;
91
+ for (const name of Object.keys(schema.tables)) {
92
+ this.db.registerTable(name, tableSpec(schema.tables[name]));
93
+ }
94
+ }
95
+
96
+ /** Register an additional base table after construction — for a SYNTHETIC aggregate table
97
+ * (`AGGREGATE-SYNC-DESIGN.md` §3.3) that is not in the typed schema. The
98
+ * `NormalizedBackend` registers each such `__agg_*` table once, before a query that reads
99
+ * it, so the local engine can join to it (the relationship `count` it backs is shipped by
100
+ * the server, never recomputed). */
101
+ registerTable(name: string, spec: { columns: string[]; primaryKey: number[] }): void {
102
+ this.db.registerTable(name, spec);
103
+ }
104
+
105
+ /** Remove a synthetic aggregate table registered by {@link registerTable}, once the last
106
+ * query reading it has been unregistered (`AGGREGATE-SYNC-DESIGN.md` §4): frees the engine
107
+ * source + its optimistic baseline so aggregate state is reclaimed, not permanent. Throws
108
+ * if a query still reads it (the backend refcounts readers, so it calls this only at 0). */
109
+ unregisterTable(name: string): void {
110
+ this.db.unregisterTable(name);
111
+ }
112
+
113
+ registerQuery(qid: QueryId, ast: Ast): void {
114
+ const r = this.db.query(qid, ast);
115
+ this.handler(qid, { type: "hello", schema: r.schema as never, comparatorVersion: r.comparatorVersion });
116
+ this.handler(qid, { type: "snapshot", adds: r.snapshot as never, last: true });
117
+ }
118
+
119
+ unregisterQuery(qid: QueryId): void {
120
+ this.db.destroyQuery(qid);
121
+ }
122
+
123
+ mutate(mutations: Mutation[]): Promise<void> {
124
+ const tx = this.db.write();
125
+ for (const m of mutations) {
126
+ if (m.op === "add") tx.add(m.table, m.row);
127
+ else if (m.op === "remove") tx.remove(m.table, m.row);
128
+ else tx.edit(m.table, m.old, m.new);
129
+ }
130
+ this.dispatch(tx.commit() as BatchSet);
131
+ return Promise.resolve();
132
+ }
133
+
134
+ onEvent(handler: (qid: QueryId, ev: ChangeEvent) => void): void {
135
+ this.handler = handler;
136
+ }
137
+
138
+ /** Deliver one commit's per-query batches. Non-reentrant (#15): if a delivery is already in
139
+ * progress (a subscriber re-entered via write()), enqueue and let the active drain pick it
140
+ * up FIFO, so each query folds commits in order. Per-query isolated (#11): a view's fold or
141
+ * subscriber throwing does NOT drop sibling queries' batches — the first error is re-raised
142
+ * only after every query has been delivered. */
143
+ private dispatch(batches: BatchSet): void {
144
+ this.dispatchQueue.push(batches);
145
+ if (this.draining) return; // an outer drain is running; it will deliver this set
146
+ this.draining = true;
147
+ let firstError: unknown;
148
+ let hasError = false;
149
+ try {
150
+ while (this.dispatchQueue.length) {
151
+ const next = this.dispatchQueue.shift()!;
152
+ for (const b of next) {
153
+ try {
154
+ this.handler(b.queryId, { type: "batch", events: b.events as never });
155
+ } catch (err) {
156
+ if (!hasError) {
157
+ hasError = true;
158
+ firstError = err;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ } finally {
164
+ this.draining = false;
165
+ }
166
+ if (hasError) throw firstError;
167
+ }
168
+
169
+ // --- the optimistic-cycle surface (OPTIMISTIC-WRITES-DESIGN.md §1/§3/§9) ----------
170
+ //
171
+ // `@rindle/optimistic` drives the engine's fork/rebase loop through these three; the
172
+ // plain local path never calls them.
173
+
174
+ /** Run `f` against a raw staged write txn (with the §4.1 `get` read path), commit, and
175
+ * dispatch the resulting batches on the ordinary event stream. Inside an open server
176
+ * batch the engine buffers the events into the cycle instead (commit returns `[]`),
177
+ * so re-invocations dispatch nothing here — delivery is `serverBatchEnd`'s. */
178
+ writeWith(f: (tx: WasmWriteTxn) => void): void {
179
+ const tx = this.db.write();
180
+ f(tx);
181
+ this.dispatch(tx.commit() as BatchSet);
182
+ }
183
+
184
+ /** Open a §1.3 reconcile cycle against the coherent server delta: the engine rewinds
185
+ * (optimistic layer un-applied, delta folded in, `sync` re-forked) and starts
186
+ * buffering. Re-invoke the still-pending mutators via {@link writeWith}, then call
187
+ * {@link serverBatchEnd}. On error the optimistic state is poisoned — discard the
188
+ * backend and re-hydrate. */
189
+ serverBatchBegin(deltas: ServerDeltaOp[]): void {
190
+ this.db.serverBatchBegin(deltas);
191
+ }
192
+
193
+ /** Close the cycle: the whole buffered stream (rewind + re-invocations) coalesces to
194
+ * the minimal net per query (§3 — a confirmed-correct prediction delivers nothing)
195
+ * and dispatches as ONE batch per affected query on the ordinary event stream. */
196
+ serverBatchEnd(): void {
197
+ this.dispatch(this.db.serverBatchEnd() as BatchSet);
198
+ }
199
+ }
200
+
201
+ /** Convenience: init the wasm + return a ready local {@link Store}. */
202
+ export async function createWasmStore<S extends ColsMap>(schema: Schema<S>): Promise<Store<S>> {
203
+ await initWasm();
204
+ return new Store(schema, new WasmBackend(schema));
205
+ }