@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 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/replica/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,216 @@
1
+ // @catalyst-cloud/sdk/node — the portable sqlite ENGINE seam + the injected driver factories.
2
+ //
3
+ // CatalystReplica never statically imports a sqlite driver: that is how it avoids hard-depping one.
4
+ // It defines one tiny portable `ReplicaEngine` — the union of replicate's `ReplicaWriteDb` (run/get),
5
+ // read-model's `SqlExecutor` (all), and schema's `MigrationDb` (exec/query) primitives, plus a
6
+ // per-engine `toBindable` (wire JSON → engine-bindable) and `transaction`/`close`. CatalystReplica
7
+ // adapts THIS one object into the three ports it needs.
8
+ //
9
+ // Drivers are INJECTED, not imported. The SDK ships three factory adapters, each dynamic-import()ing
10
+ // its driver only when called:
11
+ // • bunSqliteEngine(dbPath) — `await import("bun:sqlite")` (runtime builtin → never in package.json)
12
+ // • nodeSqliteEngine(dbPath) — `await import("node:sqlite")` { DatabaseSync } (Node >=22.5, builtin)
13
+ // • betterSqlite3Engine(driver, …) — the consumer passes `import Database from "better-sqlite3"`
14
+ // (an OPTIONAL peer, never statically imported here).
15
+ // Any other driver (D1, sql.js, …) is supported by passing a custom `ReplicaEngine`.
16
+ // Load RUNTIME-BUILTIN drivers (`bun:sqlite` / `node:sqlite`) via `createRequire`, NOT a literal
17
+ // `import("…")`. Vite/vitest and esbuild rewrite (and break) a literal dynamic import of a builtin —
18
+ // they strip the `node:`/`bun:` prefix and fail to resolve a bare `sqlite`; and a `new Function`
19
+ // import throws "dynamic import callback was not specified" under node ESM. `require` of a builtin is
20
+ // untouched by the bundler and resolves natively on both node and Bun. This keeps the drivers OUT of
21
+ // package.json (they are platform builtins) and loaded only when a factory is called.
22
+ import { createRequire } from "node:module";
23
+ const requireBuiltin = createRequire(import.meta.url);
24
+ /** Require a RUNTIME-BUILTIN driver module; the type is supplied by the caller via `typeof import(...)`. */
25
+ function importBuiltin(specifier) {
26
+ return Promise.resolve(requireBuiltin(specifier));
27
+ }
28
+ /** Coerce a wire JSON value to a node/bun-bindable scalar. Booleans → 0/1, blobs → Uint8Array, nested
29
+ * object/array → JSON text (DO rows are flat scalars; this just keeps one odd field from wedging sync). */
30
+ function toBindable(value) {
31
+ if (value === null || value === undefined)
32
+ return null;
33
+ if (typeof value === "boolean")
34
+ return value ? 1 : 0;
35
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint")
36
+ return value;
37
+ if (value instanceof Uint8Array)
38
+ return value;
39
+ if (value instanceof ArrayBuffer)
40
+ return new Uint8Array(value);
41
+ return JSON.stringify(value);
42
+ }
43
+ /** Fold a driver into the portable engine. `all`/`run`/`get` re-prepare per call (bounded read path,
44
+ * and applyDelta's SQL varies per row anyway, so statement caching would not help). */
45
+ function makeEngine(driver) {
46
+ return {
47
+ handle: driver.handle,
48
+ exec: (sql) => driver.exec(sql),
49
+ all: (sql, ...bindings) => driver.prepare(sql).all(...bindings),
50
+ run: (sql, ...bindings) => driver.prepare(sql).run(...bindings),
51
+ get: (sql, ...bindings) => driver.prepare(sql).get(...bindings),
52
+ transaction: (fn) => driver.transaction(fn),
53
+ toBindable,
54
+ close: () => driver.close(),
55
+ };
56
+ }
57
+ /**
58
+ * `bun:sqlite` engine. `db.query(sql)` prepares; `.run().changes` is the write count; WAL keeps
59
+ * concurrent readers non-blocking. `bun:sqlite` is a Bun runtime builtin → dynamic-imported, never a
60
+ * package.json dependency.
61
+ *
62
+ * The WRITER opens read-write + WAL. The READER (`readonly`) opens `{ readonly: true }` and sets
63
+ * `busy_timeout = 250` instead of WAL — exactly how CTL's replica-read.mjs opens the same file: under
64
+ * the single-writer/many-reader topology (ADR-0008) a checkpoint can briefly hold the lock, so a
65
+ * reader waits a beat rather than failing. A readonly handle runs NO migrations and rejects writes at
66
+ * the sqlite layer.
67
+ */
68
+ async function openBun(dbPath, readonly) {
69
+ const { Database } = await importBuiltin("bun:sqlite");
70
+ const db = readonly ? new Database(dbPath, { readonly: true }) : new Database(dbPath);
71
+ try {
72
+ db.run(readonly ? "PRAGMA busy_timeout = 250" : "PRAGMA journal_mode = WAL");
73
+ }
74
+ catch {
75
+ // :memory: rejects WAL on some builds; busy_timeout is harmless either way.
76
+ }
77
+ return makeEngine({
78
+ handle: db,
79
+ exec: (sql) => db.exec(sql),
80
+ prepare: (sql) => {
81
+ const st = db.query(sql);
82
+ return {
83
+ all: (...b) => st.all(...b),
84
+ get: (...b) => (st.get(...b) ?? undefined),
85
+ run: (...b) => st.run(...b).changes,
86
+ };
87
+ },
88
+ transaction: (fn) => db.transaction(fn)(),
89
+ close: () => db.close(),
90
+ });
91
+ }
92
+ export function bunSqliteEngine(dbPath) {
93
+ return openBun(dbPath, false);
94
+ }
95
+ /** Read-only `bun:sqlite` engine (`{ readonly: true }` + `busy_timeout`). See {@link openBun}. */
96
+ export function bunSqliteReadonlyEngine(dbPath) {
97
+ return openBun(dbPath, true);
98
+ }
99
+ /**
100
+ * `node:sqlite` engine (Node >=22.5; may need `--experimental-sqlite` on some 22.x). `DatabaseSync`
101
+ * has no `.transaction()` helper, so we wrap BEGIN/COMMIT/ROLLBACK. Also a runtime builtin → never a
102
+ * package.json dependency. The `readonly` variant opens `{ readOnly: true }` (node:sqlite's spelling)
103
+ * + `busy_timeout`; see {@link openBun} for the read-only rationale.
104
+ */
105
+ async function openNode(dbPath, readonly) {
106
+ const { DatabaseSync } = await importBuiltin("node:sqlite");
107
+ const db = readonly ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath);
108
+ try {
109
+ db.exec(readonly ? "PRAGMA busy_timeout = 250" : "PRAGMA journal_mode = WAL");
110
+ }
111
+ catch {
112
+ // :memory: cannot WAL — harmless; busy_timeout is allowed on a readonly handle.
113
+ }
114
+ return makeEngine({
115
+ handle: db,
116
+ exec: (sql) => db.exec(sql),
117
+ prepare: (sql) => {
118
+ const st = db.prepare(sql);
119
+ return {
120
+ all: (...b) => st.all(...b),
121
+ get: (...b) => (st.get(...b) ?? undefined),
122
+ run: (...b) => Number(st.run(...b).changes),
123
+ };
124
+ },
125
+ transaction: (fn) => {
126
+ db.exec("BEGIN");
127
+ try {
128
+ const out = fn();
129
+ db.exec("COMMIT");
130
+ return out;
131
+ }
132
+ catch (err) {
133
+ db.exec("ROLLBACK");
134
+ throw err;
135
+ }
136
+ },
137
+ close: () => db.close(),
138
+ });
139
+ }
140
+ export function nodeSqliteEngine(dbPath) {
141
+ return openNode(dbPath, false);
142
+ }
143
+ /** Read-only `node:sqlite` engine (`{ readOnly: true }` + `busy_timeout`). See {@link openNode}. */
144
+ export function nodeSqliteReadonlyEngine(dbPath) {
145
+ return openNode(dbPath, true);
146
+ }
147
+ /**
148
+ * `better-sqlite3` engine. The consumer passes the driver constructor (`import Database from
149
+ * "better-sqlite3"`) so the SDK never statically imports it — it stays an OPTIONAL peer the consumer's
150
+ * bundler resolves. Synchronous (the driver is already loaded), so no dynamic import here. The
151
+ * `readonly` variant opens `{ readonly: true }` + `busy_timeout`; see {@link openBun}.
152
+ */
153
+ function openBetter(driver, dbPath, readonly) {
154
+ const db = readonly ? new driver(dbPath, { readonly: true }) : new driver(dbPath);
155
+ try {
156
+ db.pragma(readonly ? "busy_timeout = 250" : "journal_mode = WAL");
157
+ }
158
+ catch {
159
+ // :memory: cannot WAL — harmless; busy_timeout is allowed on a readonly handle.
160
+ }
161
+ return makeEngine({
162
+ handle: db,
163
+ exec: (sql) => db.exec(sql),
164
+ prepare: (sql) => {
165
+ const st = db.prepare(sql);
166
+ return {
167
+ all: (...b) => st.all(...b),
168
+ get: (...b) => (st.get(...b) ?? undefined),
169
+ run: (...b) => Number(st.run(...b).changes),
170
+ };
171
+ },
172
+ transaction: (fn) => db.transaction(fn)(),
173
+ close: () => db.close(),
174
+ });
175
+ }
176
+ export function betterSqlite3Engine(driver, dbPath) {
177
+ return openBetter(driver, dbPath, false);
178
+ }
179
+ /** Read-only `better-sqlite3` engine (`{ readonly: true }` + `busy_timeout`). See {@link openBetter}. */
180
+ export function betterSqlite3ReadonlyEngine(driver, dbPath) {
181
+ return openBetter(driver, dbPath, true);
182
+ }
183
+ /**
184
+ * Auto-detect the default engine when `opts.engine` is omitted: Bun → `bunSqliteEngine`; else
185
+ * `node:sqlite` if available → `nodeSqliteEngine`; else throw a clear "pass opts.engine" error rather
186
+ * than pick an engine that resolves but breaks on first write.
187
+ */
188
+ export async function autoDetectEngine(dbPath) {
189
+ if (typeof globalThis.Bun !== "undefined") {
190
+ return bunSqliteEngine(dbPath);
191
+ }
192
+ try {
193
+ return await nodeSqliteEngine(dbPath);
194
+ }
195
+ catch (err) {
196
+ throw new Error("CatalystReplica: no sqlite engine available — pass opts.engine. " +
197
+ "Auto-detect needs Bun (bun:sqlite) or Node >=22.5 (node:sqlite). " +
198
+ `Underlying error: ${err instanceof Error ? err.message : String(err)}`);
199
+ }
200
+ }
201
+ /** READ-ONLY counterpart of {@link autoDetectEngine}: Bun → `bunSqliteReadonlyEngine`; else
202
+ * `nodeSqliteReadonlyEngine`. Used by `CatalystReplica.openReadOnly` when no engine is injected. */
203
+ export async function autoDetectReadonlyEngine(dbPath) {
204
+ if (typeof globalThis.Bun !== "undefined") {
205
+ return bunSqliteReadonlyEngine(dbPath);
206
+ }
207
+ try {
208
+ return await nodeSqliteReadonlyEngine(dbPath);
209
+ }
210
+ catch (err) {
211
+ throw new Error("CatalystReplica: no read-only sqlite engine available — pass opts.engine. " +
212
+ "Auto-detect needs Bun (bun:sqlite) or Node >=22.5 (node:sqlite). " +
213
+ `Underlying error: ${err instanceof Error ? err.message : String(err)}`);
214
+ }
215
+ }
216
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/replica/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { LogLevel } from "../live-sync-client.js";
2
+ /** Config for the single-writer guard. Passed as `CatalystReplicaOptions.writerGuard`. */
3
+ export interface WriterGuardOptions {
4
+ /** Disable the guard entirely (claim nothing). Default false — the guard is ON for file replicas. */
5
+ disabled?: boolean;
6
+ /** Steal an existing LIVE lock instead of throwing (the configurable override). Default false. */
7
+ override?: boolean;
8
+ /** A lock with no heartbeat newer than this many ms is treated as abandoned + reclaimed. Default 15000. */
9
+ staleMs?: number;
10
+ /** How often the owning writer rewrites its heartbeat. Default max(1000, staleMs/3). */
11
+ heartbeatMs?: number;
12
+ }
13
+ /** A claimed writer lock. `release()` stops the heartbeat and removes the file if still ours. Idempotent. */
14
+ export interface WriterLockHandle {
15
+ /** The sidecar lock file path (`dbPath + '.writer.lock'`). */
16
+ readonly path: string;
17
+ release(): void;
18
+ }
19
+ type Logger = (level: LogLevel, msg: string, extra?: unknown) => void;
20
+ /**
21
+ * Best-effort claim sole ownership of `dbPath` for a writer. Returns a {@link WriterLockHandle} (or
22
+ * `null` when the guard is disabled or `dbPath` is in-memory). Throws a clear error when another LIVE
23
+ * writer already holds the lock and `override` is not set.
24
+ */
25
+ export declare function claimWriterLock(dbPath: string, opts: WriterGuardOptions, log?: Logger): WriterLockHandle | null;
26
+ export {};
27
+ //# sourceMappingURL=writer-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer-lock.d.ts","sourceRoot":"","sources":["../../src/replica/writer-lock.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAEvD,0FAA0F;AAC1F,MAAM,WAAW,kBAAkB;IACjC,qGAAqG;IACrG,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kGAAkG;IAClG,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2GAA2G;IAC3G,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wFAAwF;IACxF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,6GAA6G;AAC7G,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,IAAI,IAAI,CAAC;CACjB;AAWD,KAAK,MAAM,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;AA4BtE;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,kBAAkB,EACxB,GAAG,CAAC,EAAE,MAAM,GACX,gBAAgB,GAAG,IAAI,CA0EzB"}
@@ -0,0 +1,125 @@
1
+ // @catalyst-cloud/sdk/node — the single-writer guard for a CatalystReplica writer (CTC-113).
2
+ //
3
+ // The managed replica is single-writer/many-reader (ADR-0008): ONE process owns the SQLite file and
4
+ // applies the change feed; everyone else opens it READ-ONLY (`CatalystReplica.openReadOnly`). Two
5
+ // concurrent WRITERS on the same file would race the cursor + truncate-and-reseed and silently
6
+ // diverge. This guard makes that mistake LOUD: when a writer `start()`s it best-effort claims sole
7
+ // ownership of `dbPath` via a sidecar lock file `dbPath + '.writer.lock'` carrying `{pid, owner,
8
+ // heartbeat}`. A second live writer on the same path throws a clear error instead of corrupting the
9
+ // replica.
10
+ //
11
+ // LIMITS — this is deliberately ADVISORY, not a hard OS lock:
12
+ // • It is cooperative: it only stops OTHER CatalystReplica writers. A raw `new Database(path)` from
13
+ // unrelated code is not blocked (use OS file locking / WAL for that).
14
+ // • Liveness has two signals. The HEARTBEAT (a timestamp rewritten on an interval) is the portable,
15
+ // cross-host signal — a lock older than `staleMs` with no heartbeat is treated as abandoned and
16
+ // reclaimed. The PID probe (`process.kill(pid,0)`) only means anything SAME-HOST; cross-host or
17
+ // when it can't probe, it conservatively assumes the holder is alive and relies on the heartbeat.
18
+ // • A hard `kill -9` can leave a stale lock until `staleMs` elapses; the next writer reclaims it.
19
+ // • `:memory:` (and `file::memory:`) replicas are per-connection, never shared, so the guard is a
20
+ // no-op for them.
21
+ import * as fs from "node:fs";
22
+ const DEFAULT_STALE_MS = 15_000;
23
+ function readLock(lockPath) {
24
+ try {
25
+ const rec = JSON.parse(fs.readFileSync(lockPath, "utf8"));
26
+ if (rec && typeof rec.pid === "number" && typeof rec.heartbeat === "number")
27
+ return rec;
28
+ return null;
29
+ }
30
+ catch {
31
+ return null; // absent, partial write, or garbage → treat as no lock
32
+ }
33
+ }
34
+ /** SAME-HOST liveness probe. Returns true when it cannot tell (assume the holder is alive — the
35
+ * heartbeat staleness check is the portable signal that ultimately reclaims a dead lock). */
36
+ function pidAlive(pid) {
37
+ const proc = globalThis.process;
38
+ if (!proc?.kill || !pid)
39
+ return true;
40
+ try {
41
+ proc.kill(pid, 0); // signal 0 = existence check, never delivered
42
+ return true;
43
+ }
44
+ catch (err) {
45
+ if (err.code === "ESRCH")
46
+ return false; // no such process
47
+ return true; // EPERM: exists but owned by another user → alive
48
+ }
49
+ }
50
+ /**
51
+ * Best-effort claim sole ownership of `dbPath` for a writer. Returns a {@link WriterLockHandle} (or
52
+ * `null` when the guard is disabled or `dbPath` is in-memory). Throws a clear error when another LIVE
53
+ * writer already holds the lock and `override` is not set.
54
+ */
55
+ export function claimWriterLock(dbPath, opts, log) {
56
+ if (opts.disabled)
57
+ return null;
58
+ if (!dbPath || dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
59
+ return null;
60
+ const lockPath = `${dbPath}.writer.lock`;
61
+ const staleMs = opts.staleMs ?? DEFAULT_STALE_MS;
62
+ const heartbeatMs = opts.heartbeatMs ?? Math.max(1000, Math.floor(staleMs / 3));
63
+ const pid = globalThis.process?.pid ?? 0;
64
+ const owner = `${pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
65
+ const serialize = () => JSON.stringify({ pid, owner, heartbeat: Date.now() });
66
+ const writeFresh = () => fs.writeFileSync(lockPath, serialize());
67
+ // Atomic exclusive create: wins the claim outright if no file exists.
68
+ let created = false;
69
+ try {
70
+ const fd = fs.openSync(lockPath, "wx");
71
+ fs.writeSync(fd, serialize());
72
+ fs.closeSync(fd);
73
+ created = true;
74
+ }
75
+ catch (err) {
76
+ if (err.code !== "EEXIST")
77
+ throw err;
78
+ }
79
+ if (!created) {
80
+ const existing = readLock(lockPath);
81
+ const held = existing != null && Date.now() - existing.heartbeat < staleMs && pidAlive(existing.pid);
82
+ if (held && !opts.override) {
83
+ throw new Error(`CatalystReplica: another writer owns this replica at ${dbPath} ` +
84
+ `(pid=${existing.pid}, last heartbeat ${Date.now() - existing.heartbeat}ms ago). ` +
85
+ `Only ONE writer may hold a replica file (ADR-0008 single-writer/many-reader); open it ` +
86
+ `read-only with CatalystReplica.openReadOnly() instead. Pass writerGuard:{override:true} ` +
87
+ `to take it over, or writerGuard:{disabled:true} to skip the check. NOTE: this guard is ` +
88
+ `advisory (a sidecar ${lockPath}), not a hard OS lock.`);
89
+ }
90
+ log?.("warn", held
91
+ ? `writer-lock: overriding a live writer at ${lockPath} (writerGuard.override)`
92
+ : `writer-lock: reclaiming a stale/abandoned lock at ${lockPath}`);
93
+ writeFresh();
94
+ }
95
+ // Keep the lock fresh so peers see it's alive; unref so it never holds the event loop open.
96
+ const timer = setInterval(() => {
97
+ try {
98
+ writeFresh();
99
+ }
100
+ catch (err) {
101
+ log?.("warn", "writer-lock heartbeat failed", err);
102
+ }
103
+ }, heartbeatMs);
104
+ timer.unref?.();
105
+ let released = false;
106
+ return {
107
+ path: lockPath,
108
+ release() {
109
+ if (released)
110
+ return;
111
+ released = true;
112
+ clearInterval(timer);
113
+ const cur = readLock(lockPath);
114
+ if (cur && cur.owner === owner) {
115
+ try {
116
+ fs.unlinkSync(lockPath);
117
+ }
118
+ catch {
119
+ // already gone — fine
120
+ }
121
+ }
122
+ },
123
+ };
124
+ }
125
+ //# sourceMappingURL=writer-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer-lock.js","sourceRoot":"","sources":["../../src/replica/writer-lock.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -1,11 +1,10 @@
1
- /** The tenant id. Also the Mirror DO name. `"tenant-0"` is Ryan's own workspace (Phase 0). */
1
+ /** The tenant id (also the mirror's name). `"tenant-0"` is an example tenant. */
2
2
  export type AccountId = string;
3
3
  /**
4
- * Canonical mirror table names — the `entity` field in change_log + the change-feed wire.
4
+ * Canonical mirror table names — the `entity` field on the change-feed wire.
5
5
  *
6
- * Mirrors @catalyst-cloud/types `EntityName` exactly. The runtime list `ENTITY_NAMES` below is the
7
- * same set as an array so the contract test can assert membership at runtime (a bare `type` alias
8
- * erases to nothing and can't be checked).
6
+ * The runtime list `ENTITY_NAMES` below is the same set as an array so the contract test can assert
7
+ * membership at runtime (a bare `type` alias erases to nothing and can't be checked).
9
8
  */
10
9
  export type EntityName = "issues" | "labels" | "users" | "issue_labels" | "relations" | "issue_history" | "projects" | "cycles" | "initiatives" | "project_initiatives" | "comments" | "pull_requests" | "check_runs" | "commit_statuses" | "reviews";
11
10
  /**
@@ -14,28 +13,28 @@ export type EntityName = "issues" | "labels" | "users" | "issue_labels" | "relat
14
13
  * the contract test asserts the two agree.
15
14
  */
16
15
  export declare const ENTITY_NAMES: readonly ["issues", "labels", "users", "issue_labels", "relations", "issue_history", "projects", "cycles", "initiatives", "project_initiatives", "comments", "pull_requests", "check_runs", "commit_statuses", "reviews"];
17
- /** change_log.op — the change-feed wire contract. */
16
+ /** The change op — the change-feed wire contract. */
18
17
  export type ChangeOp = "upsert" | "delete";
19
18
  /** The `ChangeOp` union as a runtime array (same lockstep contract as `ENTITY_NAMES`). */
20
19
  export declare const CHANGE_OPS: readonly ["upsert", "delete"];
21
20
  /**
22
- * A live change frame off the `/connect` WebSocket — the exact shape `apps/mirror/src/do/ws.ts`
23
- * broadcasts and replays. One row of the change_log, serialized.
21
+ * A live change frame off the `/connect` WebSocket — the exact shape the service broadcasts and
22
+ * replays. One change from the feed, serialized.
24
23
  */
25
24
  export interface ChangeFrame {
26
25
  type: "change";
27
26
  accountId: AccountId;
28
- /** The change_log seq — the monotonic cursor the replica advances to. */
27
+ /** The feed sequence — the monotonic cursor the replica advances to. */
29
28
  seq: number;
30
29
  entity: EntityName;
31
- /** The change_log.entity_id the PK (composite PKs joined with ':'). */
30
+ /** The entity's primary key (composite PKs joined with ':'). */
32
31
  entityId: string;
33
32
  op: ChangeOp;
34
33
  /** The full normalized row for "upsert"; absent / partial for "delete". */
35
34
  row?: Record<string, unknown>;
36
35
  }
37
36
  /**
38
- * The underflow control frame: the consumer's cursor predates the DO's retained change_log ring
37
+ * The underflow control frame: the consumer's cursor predates the service's retained change buffer
39
38
  * the consumer must re-seed from a full /snapshot.
40
39
  */
41
40
  export interface ResyncFrame {
@@ -44,14 +43,14 @@ export interface ResyncFrame {
44
43
  }
45
44
  /**
46
45
  * The catch-up request the consumer sends on every (re)connect: "replay everything after this
47
- * cursor, in seq order". The DO answers with the missed `ChangeFrame`s, or a `ResyncFrame` if `after`
48
- * predates the retained ring (cursor underflow).
46
+ * cursor, in seq order". The service answers with the missed `ChangeFrame`s, or a `ResyncFrame` if
47
+ * `after` predates the retained buffer (cursor underflow).
49
48
  */
50
49
  export interface SyncFrame {
51
50
  type: "sync";
52
- /** The durable cursor: the last change_log.seq the consumer has applied (-1 if none). */
51
+ /** The durable cursor: the last feed seq the consumer has applied (-1 if none). */
53
52
  after: number;
54
53
  }
55
- /** Any frame the DO can push to a consumer over `/connect`. */
54
+ /** Any frame the service can push to a consumer over `/connect`. */
56
55
  export type ServerFrame = ChangeFrame | ResyncFrame;
57
56
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAcA,8FAA8F;AAC9F,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC;AAE/B;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAClB,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,cAAc,GACd,WAAW,GACX,eAAe,GACf,UAAU,GACV,QAAQ,GACR,aAAa,GACb,qBAAqB,GACrB,UAAU,GACV,eAAe,GACf,YAAY,GACZ,iBAAiB,GACjB,SAAS,CAAC;AAEd;;;;GAIG;AACH,eAAO,MAAM,YAAY,2NAgBiB,CAAC;AAE3C,qDAAqD;AACrD,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3C,0FAA0F;AAC1F,eAAO,MAAM,UAAU,+BAA8D,CAAC;AAEtF;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE,SAAS,CAAC;IACrB,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,UAAU,CAAC;IACnB,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,QAAQ,CAAC;IACb,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAC;CACf;AAED,+DAA+D;AAC/D,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAUA,iFAAiF;AACjF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC;AAE/B;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAClB,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,cAAc,GACd,WAAW,GACX,eAAe,GACf,UAAU,GACV,QAAQ,GACR,aAAa,GACb,qBAAqB,GACrB,UAAU,GACV,eAAe,GACf,YAAY,GACZ,iBAAiB,GACjB,SAAS,CAAC;AAEd;;;;GAIG;AACH,eAAO,MAAM,YAAY,2NAgBiB,CAAC;AAE3C,qDAAqD;AACrD,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3C,0FAA0F;AAC1F,eAAO,MAAM,UAAU,+BAA8D,CAAC;AAEtF;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE,SAAS,CAAC;IACrB,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,UAAU,CAAC;IACnB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,QAAQ,CAAC;IACb,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;CACf;AAED,oEAAoE;AACpE,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC"}
package/dist/types.js CHANGED
@@ -1,16 +1,12 @@
1
1
  // catalyst-cloud-sdk wire types — the change-feed contract, SELF-CONTAINED.
2
2
  //
3
- // This file is a deliberate, byte-for-byte copy of the portable wire contract from the
4
- // catalyst-cloud monorepo's @catalyst-cloud/types package (packages/types/src/index.ts): the
5
- // `EntityName` table-name union, the `ChangeOp` op union, and the change/sync/resync frame shapes
6
- // the Mirror Durable Object broadcasts over `/connect`. The SDK is published standalone, so it must
7
- // NOT depend on the internal @catalyst-cloud/types workspace package — instead it owns its own copy
8
- // of the contract, and a contract test (test/contract.test.ts) pins the literal members so any drift
9
- // from the monorepo is caught.
3
+ // The `EntityName` table-name union, the `ChangeOp` op union, and the change/sync/resync frame shapes
4
+ // the catalyst-cloud service broadcasts over `/connect`. The SDK is published standalone and owns its
5
+ // own copy of this contract; a contract test (test/contract.test.ts) pins the literal members so any
6
+ // drift is caught.
10
7
  //
11
- // Like @catalyst-cloud/types, this module is Cloudflare-Workers-free and DOM-free: it is pure type
12
- // declarations + plain JS runtime constants, so it imports cleanly into a browser bundle, a node/bun
13
- // app, and a Worker alike.
8
+ // This module is free of any runtime-specific or DOM imports — pure type declarations + plain JS
9
+ // runtime constants so it imports cleanly into a browser bundle or a node/bun app alike.
14
10
  /**
15
11
  * The `EntityName` union as a runtime array, in the SAME ORDER as the type above. Frozen so consumers
16
12
  * (and the contract test) can iterate the canonical table set. Kept in lockstep with `EntityName` —
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,uFAAuF;AACvF,6FAA6F;AAC7F,kGAAkG;AAClG,oGAAoG;AACpG,oGAAoG;AACpG,qGAAqG;AACrG,+BAA+B;AAC/B,EAAE;AACF,mGAAmG;AACnG,qGAAqG;AACrG,2BAA2B;AA6B3B;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,cAAc;IACd,WAAW;IACX,eAAe;IACf,UAAU;IACV,QAAQ;IACR,aAAa;IACb,qBAAqB;IACrB,UAAU;IACV,eAAe;IACf,YAAY;IACZ,iBAAiB;IACjB,SAAS;CAC+B,CAAC;AAK3C,0FAA0F;AAC1F,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAwC,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,sGAAsG;AACtG,sGAAsG;AACtG,qGAAqG;AACrG,mBAAmB;AACnB,EAAE;AACF,iGAAiG;AACjG,2FAA2F;AA4B3F;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,cAAc;IACd,WAAW;IACX,eAAe;IACf,UAAU;IACV,QAAQ;IACR,aAAa;IACb,qBAAqB;IACrB,UAAU;IACV,eAAe;IACf,YAAY;IACZ,iBAAiB;IACjB,SAAS;CAC+B,CAAC;AAK3C,0FAA0F;AAC1F,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAwC,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@catalyst-cloud/sdk",
3
- "version": "0.1.0",
4
- "description": "Isomorphic (browser + node/bun) read-layer-over-WebSocket client for the catalyst-cloud change-feed (ADR-0008/0009).",
3
+ "version": "0.2.0",
4
+ "description": "Keep a live local copy of your Linear and GitHub project data — pushed in real time, without polling rate limits or webhook tunnels.",
5
5
  "license": "MIT",
6
6
  "author": "Coalesce Labs",
7
7
  "homepage": "https://github.com/coalesce-labs/catalyst-cloud-sdk#readme",
@@ -24,6 +24,11 @@
24
24
  "types": "./dist/index.d.ts",
25
25
  "import": "./dist/index.js",
26
26
  "default": "./dist/index.js"
27
+ },
28
+ "./node": {
29
+ "types": "./dist/node.d.ts",
30
+ "import": "./dist/node.js",
31
+ "default": "./dist/node.js"
27
32
  }
28
33
  },
29
34
  "files": [
@@ -51,5 +56,22 @@
51
56
  "devDependencies": {
52
57
  "typescript": "^5.6.3",
53
58
  "vitest": "^2.1.8"
59
+ },
60
+ "dependencies": {
61
+ "@catalyst-cloud/read-model": "^0.1.0",
62
+ "@catalyst-cloud/replicate": "^0.1.0",
63
+ "@catalyst-cloud/schema": "^0.1.0"
64
+ },
65
+ "peerDependencies": {
66
+ "@sqlite.org/sqlite-wasm": "*",
67
+ "better-sqlite3": "*"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "better-sqlite3": {
71
+ "optional": true
72
+ },
73
+ "@sqlite.org/sqlite-wasm": {
74
+ "optional": true
75
+ }
54
76
  }
55
77
  }