@catalyst-cloud/sdk 0.1.0 → 0.2.0

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.
@@ -0,0 +1,153 @@
1
+ import { type SqlExecutor, type IssueView, type IssueDetailView, type PullView, type ProjectView, type ProjectDetailView, type InitiativeView, type InitiativeDetailView } from "@catalyst-cloud/read-model";
2
+ import { type AuthStrategy, type LiveSyncStatus, type LogLevel, type WebSocketFactory } from "../live-sync-client.js";
3
+ import { type EngineFactory, type ReplicaEngine } from "./engine.js";
4
+ import { type WriterGuardOptions } from "./writer-lock.js";
5
+ export interface CatalystReplicaOptions {
6
+ /** http(s) origin incl. any path prefix (…/api/v1); the scheme is swapped to ws(s) for /connect. */
7
+ baseUrl: string;
8
+ /** Tenant id = mirror name → `?account=` on every feed request. */
9
+ account: string;
10
+ /** How to authorize: {kind:'token',token} (host bearer rides /connect as ?token= and /snapshot as
11
+ * Authorization) | {kind:'cookie'} (same-origin session cookie). */
12
+ auth: AuthStrategy;
13
+ /** File path or ':memory:'. */
14
+ dbPath: string;
15
+ /** INJECTED sqlite engine, or a factory over dbPath. Default = auto-detect (bun:sqlite, else node:sqlite). */
16
+ engine?: ReplicaEngine | EngineFactory;
17
+ /** The connect route. Default '/connect'. */
18
+ connectPath?: string;
19
+ /** Snapshot/changes fetch. Default global fetch. */
20
+ fetchImpl?: typeof fetch;
21
+ /** Fired after each applied delta (live frame OR a completed seed) — a refetch hook. */
22
+ onChange?: () => void;
23
+ /** Connection lifecycle, for UI/logging. */
24
+ onStatus?: (status: LiveSyncStatus) => void;
25
+ /** Base reconnect backoff in ms. Default 1000. */
26
+ backoffMs?: number;
27
+ /** Reconnect backoff ceiling in ms. Default 30_000. */
28
+ maxBackoffMs?: number;
29
+ /** Injectable WebSocket factory (tests). Defaults to the runtime global WebSocket. */
30
+ wsFactory?: WebSocketFactory;
31
+ /** Optional structured logger; defaults to console. */
32
+ log?: (level: LogLevel, msg: string, extra?: unknown) => void;
33
+ /**
34
+ * Single-writer guard (a sidecar `dbPath + '.writer.lock'`). On `start()` the writer best-effort
35
+ * claims sole ownership of the file; a second LIVE writer on the same path makes `start()` throw a
36
+ * clear error (single-writer/many-reader, ADR-0008). Advisory, not a hard OS lock; a no-op for
37
+ * `:memory:`. Default: enabled. Set `{ disabled: true }` to skip, `{ override: true }` to steal.
38
+ */
39
+ writerGuard?: WriterGuardOptions;
40
+ }
41
+ /**
42
+ * Options for a READ-ONLY reader ({@link CatalystReplica.openReadOnly}). A reader needs only the file
43
+ * path (+ optional injected engine / logger): no `baseUrl`, `account`, or `auth`, because it opens NO
44
+ * socket and pulls NO snapshot.
45
+ */
46
+ export interface CatalystReplicaReadOnlyOptions {
47
+ /** File path to an EXISTING writer-seeded replica. `:memory:` makes no sense for a reader (a fresh,
48
+ * empty, per-connection DB) and is rejected. */
49
+ dbPath: string;
50
+ /** INJECTED read-only sqlite engine or a factory over dbPath. Default = auto-detect READ-ONLY
51
+ * (bun:sqlite/node:sqlite opened `{ readonly: true }`). Pass a writable engine at your own risk. */
52
+ engine?: ReplicaEngine | EngineFactory;
53
+ /** Optional structured logger; defaults to console. */
54
+ log?: (level: LogLevel, msg: string, extra?: unknown) => void;
55
+ }
56
+ export declare class CatalystReplica {
57
+ private readonly opts;
58
+ private readonly baseUrl;
59
+ private readonly fetchImpl;
60
+ private readonly log;
61
+ private engine;
62
+ private writeDb;
63
+ private sqlExecutor;
64
+ private client;
65
+ /** In-memory high-water of the last seq persisted, so each frame avoids re-reading the cursor row. */
66
+ private highWater;
67
+ private _status;
68
+ private started;
69
+ private closed;
70
+ /** READ-ONLY mode (opened via {@link CatalystReplica.openReadOnly}): no migrations, no seed, no
71
+ * socket; `start()` is a no-op and the write path is unreachable. */
72
+ private readonlyMode;
73
+ /** The claimed single-writer lock (writers only; null for readers / `:memory:` / disabled). */
74
+ private writerLock;
75
+ /** Resolved on the first 'live' status (start() = caught-up + ready to read); rejected on close /
76
+ * an initial seed failure. */
77
+ private liveResolve;
78
+ private liveReject;
79
+ constructor(opts: CatalystReplicaOptions);
80
+ /**
81
+ * Open a CatalystReplica as a READ-ONLY READER over a file another process owns as the WRITER
82
+ * (single-writer/many-reader, ADR-0008). The sqlite handle is opened read-only (`{ readonly: true }`
83
+ * + `busy_timeout`), so the reader runs NO migrations, pulls NO /snapshot, opens NO LiveSyncClient,
84
+ * and the write path is unreachable — it only serves the synchronous `build*View` reads, `.sql`,
85
+ * `.handle`, and `.cursor` off whatever the writer has already persisted. There is no live tailing:
86
+ * the reader sees the file's committed state at read time (a fresh `issues()` re-queries, so it
87
+ * reflects the writer's latest committed rows).
88
+ *
89
+ * Async because the default engine dynamic-imports its sqlite driver; once it resolves the replica
90
+ * is already open, so `start()` on the returned reader is a no-op.
91
+ */
92
+ static openReadOnly(opts: CatalystReplicaReadOnlyOptions): Promise<CatalystReplica>;
93
+ /**
94
+ * Open + migrate the replica, then open the live socket. Resolves when the replica is caught-up and
95
+ * ready to read (first 'live'); background sync continues until close(). A cold tenant stream-seeds
96
+ * /snapshot first (via the injected reseed inside LiveSyncClient.start), so 'live' implies
97
+ * seed-complete. NOTE: there is no built-in timeout — a stalled /snapshot or unreachable host can
98
+ * delay 'live'; drive progress via onStatus.
99
+ */
100
+ start(): Promise<void>;
101
+ /** Stop the socket, release the writer lock, close the DB. Idempotent. Rejects a still-pending
102
+ * start(). On a reader: no socket/lock to release — just closes the read-only handle. */
103
+ close(): Promise<void>;
104
+ /** The read-model SqlExecutor over the replica. `buildIssuesView(replica.sql, …)` is unchanged. */
105
+ get sql(): SqlExecutor;
106
+ issues(opts?: {
107
+ limit?: number;
108
+ offset?: number;
109
+ }): IssueView[];
110
+ issue(identifier: string): IssueDetailView | null;
111
+ pulls(opts?: {
112
+ limit?: number;
113
+ offset?: number;
114
+ }): PullView[];
115
+ projects(opts?: {
116
+ limit?: number;
117
+ offset?: number;
118
+ }): ProjectView[];
119
+ project(id: string): ProjectDetailView | null;
120
+ initiatives(opts?: {
121
+ limit?: number;
122
+ offset?: number;
123
+ }): InitiativeView[];
124
+ initiative(id: string): InitiativeDetailView | null;
125
+ /** The durable change-feed cursor (sync_meta), or null before the first seed. */
126
+ get cursor(): number | null;
127
+ /** The connection lifecycle status. */
128
+ get status(): LiveSyncStatus;
129
+ /** The raw driver Database the SDK owns — for `drizzle(replica.handle, { schema: mirrorSchema })`. */
130
+ get handle(): unknown;
131
+ private resolveEngine;
132
+ /**
133
+ * Open the read-only engine and wire ONLY the read ports — no migrations, no `sync_meta` DDL, no
134
+ * snapshot, no LiveSyncClient. The write-facing `writeDb` is a read-capable shim whose `run` throws,
135
+ * so reads (`sql`/`build*View`/`cursor`) work but any accidental write path is unreachable AND the
136
+ * underlying sqlite handle is itself read-only.
137
+ */
138
+ private openReadOnlyInternal;
139
+ private handleStatus;
140
+ private clearLiveDeferred;
141
+ /** Land one delta + advance the durable cursor atomically (a crash can't skip a seq), then signal. */
142
+ private applyFrame;
143
+ /**
144
+ * Stream-seed the replica from /snapshot and return the fresh cursor. Streams `response.body` as
145
+ * NDJSON (chunked, batched transactions) so a large tenant never materializes the whole snapshot in
146
+ * memory — the OOM fix vs host-sync's buffered seed. The cursor row is DELETED up front so an
147
+ * interrupted seed self-heals (getCursor → null → re-seed on the next start), preserving host-sync's
148
+ * atomic truncate+apply+setCursor safety without holding one giant transaction.
149
+ */
150
+ private seedFromSnapshot;
151
+ private feedHeaders;
152
+ }
153
+ //# sourceMappingURL=catalyst-replica.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalyst-replica.d.ts","sourceRoot":"","sources":["../../src/replica/catalyst-replica.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,409 @@
1
+ // @catalyst-cloud/sdk/node — CatalystReplica: host-sync's writer + read seam behind ONE import.
2
+ //
3
+ // Composed from the published @catalyst-cloud/{schema,read-model,replicate} packages + the SDK's own
4
+ // LiveSyncClient. It is the drop-in replacement for the hand-rolled writer loop in apps/host-sync
5
+ // (apply.ts + sync-client.ts + live-client.ts + read-adapter.ts), now engine-generic:
6
+ //
7
+ // start() = open DB (injected engine, default bun:sqlite/node:sqlite auto-detect)
8
+ // → applyMigrations(MIRROR_MIGRATIONS) + sync_meta DDL
9
+ // → open LiveSyncClient: if cursor==null it STREAM-seeds /snapshot (the injected reseed),
10
+ // then replays {type:"sync", after:cursor}, applyDelta + cursor per frame; {type:"resync"}
11
+ // re-seeds. RESOLVES WHEN FIRST 'live' (caught-up + ready to read); background sync
12
+ // continues until close(). This is the deliberate fork from the raw transport's
13
+ // resolve-on-stop.
14
+ // reads = SYNCHRONOUS over the read-model SqlExecutor (node/bun sqlite is sync). The
15
+ // `buildIssuesView(replica.sql, …)` calls are LITERALLY unchanged.
16
+ //
17
+ // Reads, writes, migrations, cursor and wire semantics are byte-identical to host-sync because this
18
+ // runs the SAME @catalyst-cloud/read-model builders over the SAME @catalyst-cloud/replicate write path
19
+ // and the SAME @catalyst-cloud/schema MIRROR_MIGRATIONS.
20
+ import { applyMigrations, MIRROR_MIGRATIONS } from "@catalyst-cloud/schema";
21
+ import { applyDelta, truncateReplica, getCursor, setCursor, } from "@catalyst-cloud/replicate";
22
+ import { buildIssuesView, buildIssueDetail, buildPullsView, buildProjectsView, buildProjectDetail, buildInitiativesView, buildInitiativeDetail, } from "@catalyst-cloud/read-model";
23
+ import { LiveSyncClient, stripTrailingSlashes, } from "../live-sync-client.js";
24
+ import { autoDetectEngine, autoDetectReadonlyEngine, } from "./engine.js";
25
+ import { claimWriterLock, } from "./writer-lock.js";
26
+ /** Host-sync bookkeeping table (NOT part of the DO mirror schema, so not in MIRROR_MIGRATIONS): the
27
+ * change-feed cursor, so a restart resumes from the live `{type:"sync", after}` replay (or /changes)
28
+ * instead of a full /snapshot every boot. */
29
+ const SYNC_META_DDL = `CREATE TABLE IF NOT EXISTS sync_meta (
30
+ key TEXT PRIMARY KEY, value TEXT
31
+ );`;
32
+ /** Rows per streamed seed transaction — bounds memory + fsync so a large tenant never OOMs the node/bun
33
+ * process the way host-sync's buffered `await res.text()` + `split("\n")` snapshot would. */
34
+ const SEED_BATCH_ROWS = 1000;
35
+ export class CatalystReplica {
36
+ opts;
37
+ baseUrl;
38
+ fetchImpl;
39
+ log;
40
+ engine = null;
41
+ writeDb = null;
42
+ sqlExecutor = null;
43
+ client = null;
44
+ /** In-memory high-water of the last seq persisted, so each frame avoids re-reading the cursor row. */
45
+ highWater = 0;
46
+ _status = "connecting";
47
+ started = false;
48
+ closed = false;
49
+ /** READ-ONLY mode (opened via {@link CatalystReplica.openReadOnly}): no migrations, no seed, no
50
+ * socket; `start()` is a no-op and the write path is unreachable. */
51
+ readonlyMode = false;
52
+ /** The claimed single-writer lock (writers only; null for readers / `:memory:` / disabled). */
53
+ writerLock = null;
54
+ /** Resolved on the first 'live' status (start() = caught-up + ready to read); rejected on close /
55
+ * an initial seed failure. */
56
+ liveResolve = null;
57
+ liveReject = null;
58
+ constructor(opts) {
59
+ this.opts = opts;
60
+ this.baseUrl = stripTrailingSlashes(opts.baseUrl);
61
+ this.fetchImpl = opts.fetchImpl ?? fetch;
62
+ this.log =
63
+ opts.log ??
64
+ ((lvl, msg, extra) => console[lvl === "error" ? "error" : "log"](`[catalyst-replica] ${msg}`, extra ?? ""));
65
+ }
66
+ /**
67
+ * Open a CatalystReplica as a READ-ONLY READER over a file another process owns as the WRITER
68
+ * (single-writer/many-reader, ADR-0008). The sqlite handle is opened read-only (`{ readonly: true }`
69
+ * + `busy_timeout`), so the reader runs NO migrations, pulls NO /snapshot, opens NO LiveSyncClient,
70
+ * and the write path is unreachable — it only serves the synchronous `build*View` reads, `.sql`,
71
+ * `.handle`, and `.cursor` off whatever the writer has already persisted. There is no live tailing:
72
+ * the reader sees the file's committed state at read time (a fresh `issues()` re-queries, so it
73
+ * reflects the writer's latest committed rows).
74
+ *
75
+ * Async because the default engine dynamic-imports its sqlite driver; once it resolves the replica
76
+ * is already open, so `start()` on the returned reader is a no-op.
77
+ */
78
+ static async openReadOnly(opts) {
79
+ if (!opts.dbPath || opts.dbPath === ":memory:" || opts.dbPath.startsWith("file::memory:")) {
80
+ throw new Error("CatalystReplica.openReadOnly: a reader needs a FILE path to a writer-seeded replica; " +
81
+ "':memory:' is a fresh per-connection DB with nothing to read.");
82
+ }
83
+ // Reuse the writer constructor with placeholders for the connect-only fields (never touched in
84
+ // read-only mode), then open the read-only engine eagerly.
85
+ const replica = new CatalystReplica({
86
+ baseUrl: "readonly://local",
87
+ account: "",
88
+ auth: { kind: "cookie" },
89
+ dbPath: opts.dbPath,
90
+ engine: opts.engine,
91
+ log: opts.log,
92
+ });
93
+ await replica.openReadOnlyInternal(opts);
94
+ return replica;
95
+ }
96
+ /**
97
+ * Open + migrate the replica, then open the live socket. Resolves when the replica is caught-up and
98
+ * ready to read (first 'live'); background sync continues until close(). A cold tenant stream-seeds
99
+ * /snapshot first (via the injected reseed inside LiveSyncClient.start), so 'live' implies
100
+ * seed-complete. NOTE: there is no built-in timeout — a stalled /snapshot or unreachable host can
101
+ * delay 'live'; drive progress via onStatus.
102
+ */
103
+ async start() {
104
+ // A reader is already open (openReadOnly opened the engine); start() is an inert no-op.
105
+ if (this.readonlyMode)
106
+ return;
107
+ if (this.closed)
108
+ throw new Error("CatalystReplica: start() after close()");
109
+ if (this.started)
110
+ throw new Error("CatalystReplica: start() already called");
111
+ this.started = true;
112
+ // Single-writer guard: claim sole ownership of the file BEFORE opening it, so a second concurrent
113
+ // writer rejects here instead of racing the cursor/seed. A no-op for ':memory:' or when disabled.
114
+ this.writerLock = claimWriterLock(this.opts.dbPath, this.opts.writerGuard ?? {}, this.log);
115
+ const engine = await this.resolveEngine();
116
+ this.engine = engine;
117
+ this.writeDb = {
118
+ run: (sql, ...bindings) => engine.run(sql, ...bindings),
119
+ get: (sql, ...bindings) => engine.get(sql, ...bindings),
120
+ };
121
+ this.sqlExecutor = {
122
+ exec: (query, ...bindings) => ({
123
+ toArray: () => engine.all(query, ...bindings.map(engine.toBindable)),
124
+ }),
125
+ };
126
+ // Migrate via the ~3-line MigrationDb adapter, then add the host-only cursor table.
127
+ const migrationDb = {
128
+ exec: (sql) => engine.exec(sql),
129
+ query: (sql) => engine.all(sql),
130
+ };
131
+ applyMigrations(migrationDb, MIRROR_MIGRATIONS);
132
+ engine.exec(SYNC_META_DDL);
133
+ this.highWater = getCursor(this.writeDb) ?? 0;
134
+ this.client = new LiveSyncClient({
135
+ baseUrl: this.baseUrl,
136
+ accountId: this.opts.account,
137
+ connectPath: this.opts.connectPath,
138
+ auth: this.opts.auth,
139
+ reseed: () => this.seedFromSnapshot(),
140
+ getCursor: () => getCursor(this.writeDb),
141
+ onChange: (frame) => this.applyFrame(frame),
142
+ onStatus: (status) => this.handleStatus(status),
143
+ backoffMs: this.opts.backoffMs,
144
+ maxBackoffMs: this.opts.maxBackoffMs,
145
+ wsFactory: this.opts.wsFactory,
146
+ log: this.log,
147
+ });
148
+ return new Promise((resolve, reject) => {
149
+ this.liveResolve = resolve;
150
+ this.liveReject = reject;
151
+ // The transport's start() resolves only on stop() ("runs forever"); we fork on first 'live'.
152
+ // Run it in the background and surface an initial seed/connect failure as a start() rejection.
153
+ void this.client.start().catch((err) => {
154
+ const rej = this.liveReject;
155
+ this.clearLiveDeferred();
156
+ rej?.(err);
157
+ });
158
+ });
159
+ }
160
+ /** Stop the socket, release the writer lock, close the DB. Idempotent. Rejects a still-pending
161
+ * start(). On a reader: no socket/lock to release — just closes the read-only handle. */
162
+ async close() {
163
+ if (this.closed)
164
+ return;
165
+ this.closed = true;
166
+ this.client?.stop();
167
+ const rej = this.liveReject;
168
+ this.clearLiveDeferred();
169
+ rej?.(new Error("CatalystReplica: closed before first 'live'"));
170
+ try {
171
+ this.writerLock?.release();
172
+ }
173
+ catch (err) {
174
+ this.log("warn", "writer-lock release threw", err);
175
+ }
176
+ this.writerLock = null;
177
+ try {
178
+ this.engine?.close();
179
+ }
180
+ catch (err) {
181
+ this.log("warn", "engine close threw", err);
182
+ }
183
+ }
184
+ // ── Reads ───────────────────────────────────────────────────────────────────────────────────────
185
+ /** The read-model SqlExecutor over the replica. `buildIssuesView(replica.sql, …)` is unchanged. */
186
+ get sql() {
187
+ if (!this.sqlExecutor)
188
+ throw new Error("CatalystReplica: call start() before reading");
189
+ return this.sqlExecutor;
190
+ }
191
+ issues(opts) {
192
+ return buildIssuesView(this.sql, opts?.limit, opts?.offset);
193
+ }
194
+ issue(identifier) {
195
+ return buildIssueDetail(this.sql, identifier);
196
+ }
197
+ pulls(opts) {
198
+ return buildPullsView(this.sql, opts?.limit, opts?.offset);
199
+ }
200
+ projects(opts) {
201
+ return buildProjectsView(this.sql, opts?.limit, opts?.offset);
202
+ }
203
+ project(id) {
204
+ return buildProjectDetail(this.sql, id);
205
+ }
206
+ initiatives(opts) {
207
+ return buildInitiativesView(this.sql, opts?.limit, opts?.offset);
208
+ }
209
+ initiative(id) {
210
+ return buildInitiativeDetail(this.sql, id);
211
+ }
212
+ /** The durable change-feed cursor (sync_meta), or null before the first seed. */
213
+ get cursor() {
214
+ return this.writeDb ? getCursor(this.writeDb) : null;
215
+ }
216
+ /** The connection lifecycle status. */
217
+ get status() {
218
+ return this._status;
219
+ }
220
+ /** The raw driver Database the SDK owns — for `drizzle(replica.handle, { schema: mirrorSchema })`. */
221
+ get handle() {
222
+ return this.engine?.handle;
223
+ }
224
+ // ── Internals ─────────────────────────────────────────────────────────────────────────────────
225
+ async resolveEngine() {
226
+ const e = this.opts.engine;
227
+ if (e === undefined)
228
+ return autoDetectEngine(this.opts.dbPath);
229
+ if (typeof e === "function")
230
+ return e(this.opts.dbPath);
231
+ return e;
232
+ }
233
+ /**
234
+ * Open the read-only engine and wire ONLY the read ports — no migrations, no `sync_meta` DDL, no
235
+ * snapshot, no LiveSyncClient. The write-facing `writeDb` is a read-capable shim whose `run` throws,
236
+ * so reads (`sql`/`build*View`/`cursor`) work but any accidental write path is unreachable AND the
237
+ * underlying sqlite handle is itself read-only.
238
+ */
239
+ async openReadOnlyInternal(opts) {
240
+ this.readonlyMode = true;
241
+ this.started = true; // already open → start() is a no-op
242
+ const e = opts.engine;
243
+ const engine = e === undefined
244
+ ? await autoDetectReadonlyEngine(opts.dbPath)
245
+ : typeof e === "function"
246
+ ? await e(opts.dbPath)
247
+ : e;
248
+ this.engine = engine;
249
+ this.sqlExecutor = {
250
+ exec: (query, ...bindings) => ({
251
+ toArray: () => engine.all(query, ...bindings.map(engine.toBindable)),
252
+ }),
253
+ };
254
+ // Read-capable cursor access; `run` is poisoned so the write path can never fire on a reader.
255
+ this.writeDb = {
256
+ get: (sql, ...bindings) => engine.get(sql, ...bindings),
257
+ run: () => {
258
+ throw new Error("CatalystReplica: this replica is READ-ONLY (opened via openReadOnly)");
259
+ },
260
+ };
261
+ this._status = "live"; // a reader is "live" the moment it is open (it has no socket)
262
+ }
263
+ handleStatus(status) {
264
+ this._status = status;
265
+ if (status === "live" && this.liveResolve) {
266
+ const res = this.liveResolve;
267
+ this.clearLiveDeferred();
268
+ res();
269
+ }
270
+ try {
271
+ this.opts.onStatus?.(status);
272
+ }
273
+ catch (err) {
274
+ this.log("warn", "onStatus handler threw", err);
275
+ }
276
+ }
277
+ clearLiveDeferred() {
278
+ this.liveResolve = null;
279
+ this.liveReject = null;
280
+ }
281
+ /** Land one delta + advance the durable cursor atomically (a crash can't skip a seq), then signal. */
282
+ applyFrame(frame) {
283
+ const engine = this.engine;
284
+ const writeDb = this.writeDb;
285
+ if (!engine || !writeDb)
286
+ return;
287
+ try {
288
+ engine.transaction(() => {
289
+ applyDelta(writeDb, { entity: frame.entity, op: frame.op, row: frame.row ?? {}, entityId: frame.entityId }, engine.toBindable);
290
+ // Advance to the seq we SAW (not just applied), so a stale-but-newer-seq delta still moves the
291
+ // cursor forward and a reconnect doesn't re-request it.
292
+ if (frame.seq > this.highWater)
293
+ setCursor(writeDb, frame.seq, engine.toBindable);
294
+ });
295
+ if (frame.seq > this.highWater)
296
+ this.highWater = frame.seq;
297
+ try {
298
+ this.opts.onChange?.();
299
+ }
300
+ catch (err) {
301
+ this.log("warn", "onChange handler threw", err);
302
+ }
303
+ }
304
+ catch (err) {
305
+ this.log("error", `apply failed for ${frame.entity} seq=${frame.seq}`, err);
306
+ }
307
+ }
308
+ /**
309
+ * Stream-seed the replica from /snapshot and return the fresh cursor. Streams `response.body` as
310
+ * NDJSON (chunked, batched transactions) so a large tenant never materializes the whole snapshot in
311
+ * memory — the OOM fix vs host-sync's buffered seed. The cursor row is DELETED up front so an
312
+ * interrupted seed self-heals (getCursor → null → re-seed on the next start), preserving host-sync's
313
+ * atomic truncate+apply+setCursor safety without holding one giant transaction.
314
+ */
315
+ async seedFromSnapshot() {
316
+ const engine = this.engine;
317
+ const writeDb = this.writeDb;
318
+ const url = `${this.baseUrl}/snapshot?account=${encodeURIComponent(this.opts.account)}`;
319
+ const res = await this.fetchImpl(url, { headers: this.feedHeaders() });
320
+ if (!res.ok)
321
+ throw new Error(`/snapshot ${res.status}`);
322
+ // Invalidate the cursor BEFORE truncating so a crash mid-seed re-seeds rather than going live over
323
+ // an empty replica from a stale cursor.
324
+ engine.run("DELETE FROM sync_meta WHERE key = 'cursor'");
325
+ engine.transaction(() => truncateReplica(writeDb));
326
+ let cursor = 0;
327
+ let batch = [];
328
+ const flush = () => {
329
+ if (batch.length === 0)
330
+ return;
331
+ const rows = batch;
332
+ batch = [];
333
+ engine.transaction(() => {
334
+ for (const rec of rows) {
335
+ if (rec.entity === undefined)
336
+ continue;
337
+ applyDelta(writeDb, { entity: rec.entity, op: rec.op ?? "upsert", row: rec.row ?? {} }, engine.toBindable);
338
+ }
339
+ });
340
+ };
341
+ let rowCount = 0;
342
+ for await (const line of iterateNdjson(res)) {
343
+ const rec = JSON.parse(line);
344
+ if (typeof rec.cursor === "number") {
345
+ cursor = rec.cursor; // the FINAL line carries the cursor
346
+ continue;
347
+ }
348
+ batch.push(rec);
349
+ rowCount++;
350
+ if (batch.length >= SEED_BATCH_ROWS)
351
+ flush();
352
+ }
353
+ flush();
354
+ engine.transaction(() => setCursor(writeDb, cursor, engine.toBindable));
355
+ this.highWater = cursor;
356
+ this.log("info", `snapshot seeded (${rowCount} rows), cursor=${cursor}`);
357
+ try {
358
+ this.opts.onChange?.();
359
+ }
360
+ catch (err) {
361
+ this.log("warn", "onChange handler threw", err);
362
+ }
363
+ return cursor;
364
+ }
365
+ feedHeaders() {
366
+ const h = { accept: "application/x-ndjson" };
367
+ if (this.opts.auth.kind === "token")
368
+ h["authorization"] = `Bearer ${this.opts.auth.token}`;
369
+ return h;
370
+ }
371
+ }
372
+ /**
373
+ * Iterate an NDJSON Response as non-empty lines. Streams `response.body` (chunked + partial-line
374
+ * buffered) when present — the production path that never buffers the whole snapshot; falls back to a
375
+ * buffered `await res.text()` when the body is absent (e.g. a test fetch stand-in).
376
+ */
377
+ async function* iterateNdjson(res) {
378
+ const body = res.body;
379
+ if (body && typeof body.getReader === "function") {
380
+ const reader = body.getReader();
381
+ const decoder = new TextDecoder();
382
+ let buf = "";
383
+ for (;;) {
384
+ const { done, value } = await reader.read();
385
+ if (done)
386
+ break;
387
+ buf += decoder.decode(value, { stream: true });
388
+ let nl;
389
+ while ((nl = buf.indexOf("\n")) >= 0) {
390
+ const line = buf.slice(0, nl);
391
+ buf = buf.slice(nl + 1);
392
+ if (line.length > 0)
393
+ yield line;
394
+ }
395
+ }
396
+ buf += decoder.decode();
397
+ if (buf.length > 0) {
398
+ for (const line of buf.split("\n"))
399
+ if (line.length > 0)
400
+ yield line;
401
+ }
402
+ return;
403
+ }
404
+ const text = await res.text();
405
+ for (const line of text.split("\n"))
406
+ if (line.length > 0)
407
+ yield line;
408
+ }
409
+ //# sourceMappingURL=catalyst-replica.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalyst-replica.js","sourceRoot":"","sources":["../../src/replica/catalyst-replica.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /** A bindable SQLite scalar on the node/bun side (booleans stored as 0/1 INTEGER; blobs as Uint8Array). */
2
+ export type EngineBindable = string | number | bigint | null | Uint8Array;
3
+ /**
4
+ * The portable sqlite engine the SDK owns and adapts. Generic over the bindable type `B` so a driver
5
+ * that binds binary/bigint differently can declare its own; the node/bun builtins use `EngineBindable`.
6
+ */
7
+ export interface ReplicaEngine<B = unknown> {
8
+ /** Execute DDL / migration statement(s) for side effect (no rows). */
9
+ exec(sql: string): void;
10
+ /** Run a parameterized SELECT → rows as plain objects (reads + the migration ledger). */
11
+ all(sql: string, ...bindings: B[]): Record<string, B>[];
12
+ /** Run a parameterized mutation → sqlite3_changes() (rows written). */
13
+ run(sql: string, ...bindings: B[]): number;
14
+ /** Run a single-row query → the first row, or undefined. */
15
+ get(sql: string, ...bindings: B[]): Record<string, B> | undefined;
16
+ /** Run `fn` inside one atomic transaction (snapshot-seed batch / delta apply). */
17
+ transaction<T>(fn: () => T): T;
18
+ /** Coerce ONE wire JSON value to an engine-bindable scalar (bool → 0/1, blob/bigint per engine). */
19
+ toBindable: (value: unknown) => B;
20
+ /** Close the underlying database handle. */
21
+ close(): void;
22
+ }
23
+ /** A factory that opens a `ReplicaEngine` over `dbPath`. May be async (drivers are dynamic-imported). */
24
+ export type EngineFactory = (dbPath: string) => ReplicaEngine | Promise<ReplicaEngine>;
25
+ /** A `ReplicaEngine` plus the raw driver `handle` (exposed via CatalystReplica.handle for drizzle). */
26
+ export type ReplicaEngineWithHandle = ReplicaEngine<EngineBindable> & {
27
+ readonly handle: unknown;
28
+ };
29
+ export declare function bunSqliteEngine(dbPath: string): Promise<ReplicaEngineWithHandle>;
30
+ /** Read-only `bun:sqlite` engine (`{ readonly: true }` + `busy_timeout`). See {@link openBun}. */
31
+ export declare function bunSqliteReadonlyEngine(dbPath: string): Promise<ReplicaEngineWithHandle>;
32
+ export declare function nodeSqliteEngine(dbPath: string): Promise<ReplicaEngineWithHandle>;
33
+ /** Read-only `node:sqlite` engine (`{ readOnly: true }` + `busy_timeout`). See {@link openNode}. */
34
+ export declare function nodeSqliteReadonlyEngine(dbPath: string): Promise<ReplicaEngineWithHandle>;
35
+ /** A statement handle the `better-sqlite3` default export hands back (typed structurally — no import). */
36
+ interface BetterSqlite3Statement {
37
+ all(...bindings: unknown[]): unknown[];
38
+ get(...bindings: unknown[]): unknown;
39
+ run(...bindings: unknown[]): {
40
+ changes: number | bigint;
41
+ lastInsertRowid: number | bigint;
42
+ };
43
+ }
44
+ /** The `better-sqlite3` Database surface this engine uses (typed structurally — no import / no peer pin). */
45
+ interface BetterSqlite3Database {
46
+ prepare(sql: string): BetterSqlite3Statement;
47
+ exec(sql: string): void;
48
+ pragma(source: string): unknown;
49
+ transaction<Args extends unknown[], R>(fn: (...args: Args) => R): (...args: Args) => R;
50
+ close(): void;
51
+ }
52
+ /** `import Database from "better-sqlite3"` is structurally assignable to this constructor type. */
53
+ export type BetterSqlite3Driver = new (path?: string, options?: unknown) => BetterSqlite3Database;
54
+ export declare function betterSqlite3Engine(driver: BetterSqlite3Driver, dbPath: string): ReplicaEngineWithHandle;
55
+ /** Read-only `better-sqlite3` engine (`{ readonly: true }` + `busy_timeout`). See {@link openBetter}. */
56
+ export declare function betterSqlite3ReadonlyEngine(driver: BetterSqlite3Driver, dbPath: string): ReplicaEngineWithHandle;
57
+ /**
58
+ * Auto-detect the default engine when `opts.engine` is omitted: Bun → `bunSqliteEngine`; else
59
+ * `node:sqlite` if available → `nodeSqliteEngine`; else throw a clear "pass opts.engine" error rather
60
+ * than pick an engine that resolves but breaks on first write.
61
+ */
62
+ export declare function autoDetectEngine(dbPath: string): Promise<ReplicaEngineWithHandle>;
63
+ /** READ-ONLY counterpart of {@link autoDetectEngine}: Bun → `bunSqliteReadonlyEngine`; else
64
+ * `nodeSqliteReadonlyEngine`. Used by `CatalystReplica.openReadOnly` when no engine is injected. */
65
+ export declare function autoDetectReadonlyEngine(dbPath: string): Promise<ReplicaEngineWithHandle>;
66
+ export {};
67
+ //# sourceMappingURL=engine.d.ts.map