@delali/sirannon-db 0.1.8 → 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.
Files changed (71) hide show
  1. package/README.md +117 -989
  2. package/dist/backup-scheduler/index.d.ts +1 -1
  3. package/dist/backup-scheduler/index.mjs +2 -2
  4. package/dist/baseline-Br77Fnhb.d.ts +6 -0
  5. package/dist/{change-tracker-CbmaMO-N.d.ts → change-tracker-DKRVUC3l.d.ts} +5 -8
  6. package/dist/chunk-5NOIGN5Y.mjs +18 -0
  7. package/dist/chunk-67M7KAH6.mjs +90 -0
  8. package/dist/{chunk-GEZUUIKV.mjs → chunk-D7LAYTKN.mjs} +5 -2
  9. package/dist/{chunk-UXLAO6ZH.mjs → chunk-EBJXPQQO.mjs} +8 -3
  10. package/dist/{chunk-BNUTBHHH.mjs → chunk-FHWTZFI4.mjs} +1 -1
  11. package/dist/chunk-GVCNMPOS.mjs +130 -0
  12. package/dist/chunk-H237TXZW.mjs +1407 -0
  13. package/dist/{chunk-HHRMRFFR.mjs → chunk-H6PIVVDN.mjs} +353 -142
  14. package/dist/chunk-HR5CWTLC.mjs +661 -0
  15. package/dist/chunk-JU64Y7HM.mjs +299 -0
  16. package/dist/{chunk-CW6S3WL5.mjs → chunk-JZGINXTN.mjs} +7 -5
  17. package/dist/{chunk-CJLYFDP5.mjs → chunk-LFZ37BSX.mjs} +1 -1
  18. package/dist/chunk-LNY2VVHE.mjs +18 -0
  19. package/dist/chunk-NVQS53NT.mjs +665 -0
  20. package/dist/{chunk-H5AB6NIR.mjs → chunk-O7SLN3GI.mjs} +2 -15
  21. package/dist/chunk-OQVZBEBY.mjs +283 -0
  22. package/dist/{chunk-VLTICJOD.mjs → chunk-PIKHN33N.mjs} +1 -1
  23. package/dist/chunk-TJF5GZSV.mjs +187 -0
  24. package/dist/chunk-TUD5CJ76.mjs +126 -0
  25. package/dist/{chunk-YPYVQJ4C.mjs → chunk-UC3SCMIN.mjs} +9 -1
  26. package/dist/client/index.d.ts +216 -321
  27. package/dist/client/index.mjs +1192 -1342
  28. package/dist/client/topology.d.ts +61 -0
  29. package/dist/client/topology.mjs +682 -0
  30. package/dist/client-base-CLWmH5Ln.d.ts +234 -0
  31. package/dist/codegen/cli.d.ts +2 -0
  32. package/dist/codegen/cli.mjs +11 -0
  33. package/dist/codegen/index.d.ts +30 -0
  34. package/dist/codegen/index.mjs +5 -0
  35. package/dist/core/index.d.ts +37 -16
  36. package/dist/core/index.mjs +2322 -298
  37. package/dist/core/writer-worker.mjs +3 -3
  38. package/dist/{database-DuGp0Rtr.d.ts → database-BY0L5Q2n.d.ts} +59 -14
  39. package/dist/driver/better-sqlite3.d.ts +1 -1
  40. package/dist/driver/better-sqlite3.mjs +5 -5
  41. package/dist/driver/node.d.ts +1 -1
  42. package/dist/driver/node.mjs +5 -5
  43. package/dist/driver/wa-sqlite.d.ts +1 -136
  44. package/dist/{errors-5Nf5ZAEC.d.ts → errors-Bw5MdNCu.d.ts} +5 -1
  45. package/dist/file-migrations/index.d.ts +7 -5
  46. package/dist/file-migrations/index.mjs +42 -37
  47. package/dist/operation-registry-9DcvxcE5.d.ts +45 -0
  48. package/dist/primary-wins-DPAm2AKG.d.ts +61 -0
  49. package/dist/protocol-6KrSq2Hy.d.ts +66 -0
  50. package/dist/react/index.d.ts +20 -0
  51. package/dist/react/index.mjs +137 -0
  52. package/dist/replication/coordinator/etcd.d.ts +5 -3
  53. package/dist/replication/coordinator/etcd.mjs +554 -462
  54. package/dist/replication/index.d.ts +51 -105
  55. package/dist/replication/index.mjs +1415 -2061
  56. package/dist/server/index.d.ts +111 -87
  57. package/dist/server/index.mjs +1917 -489
  58. package/dist/server-options-1JHu8pid.d.ts +191 -0
  59. package/dist/{sirannon-4SspRvP5.d.ts → sirannon-DaQSyhbJ.d.ts} +7 -2
  60. package/dist/transport/grpc.d.ts +7 -4
  61. package/dist/transport/grpc.mjs +191 -97
  62. package/dist/transport/memory.d.ts +5 -134
  63. package/dist/transport/memory.mjs +3 -1
  64. package/dist/{types-BsjobKbl.d.ts → types-B7gmEsZW.d.ts} +8 -61
  65. package/dist/{types-BEu1I_9_.d.ts → types-BsVabqSI.d.ts} +1 -1
  66. package/dist/types-CL6piSnD.d.ts +43 -0
  67. package/dist/types-C_D8IhpO.d.ts +60 -0
  68. package/dist/{types-D_hQW1hr.d.ts → types-zhnRXrsb.d.ts} +55 -165
  69. package/package.json +44 -6
  70. package/dist/chunk-TKGHYWQ6.mjs +0 -35
  71. package/dist/types-D4p4UyDK.d.ts +0 -26
@@ -0,0 +1,1407 @@
1
+ // src/client/types.ts
2
+ var RemoteError = class extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.name = "RemoteError";
7
+ this.code = code;
8
+ }
9
+ };
10
+
11
+ // src/client/remote-live-query.ts
12
+ var RemoteLiveQuery = class _RemoteLiveQuery {
13
+ state = { status: "pending" };
14
+ rows = [];
15
+ listeners = /* @__PURE__ */ new Set();
16
+ subscription = null;
17
+ closed = false;
18
+ static async open(subscribe) {
19
+ const query = new _RemoteLiveQuery();
20
+ try {
21
+ query.subscription = await subscribe(query.handlers());
22
+ } catch (err) {
23
+ query.closed = true;
24
+ throw err;
25
+ }
26
+ if (query.state.status === "error") {
27
+ const failure = query.state.error;
28
+ await query.close();
29
+ throw failure;
30
+ }
31
+ return query;
32
+ }
33
+ getState() {
34
+ return this.state;
35
+ }
36
+ subscribe(listener) {
37
+ this.listeners.add(listener);
38
+ return () => {
39
+ this.listeners.delete(listener);
40
+ };
41
+ }
42
+ async close() {
43
+ if (this.closed) return;
44
+ this.closed = true;
45
+ this.listeners.clear();
46
+ this.subscription?.unsubscribe();
47
+ this.subscription = null;
48
+ }
49
+ handlers() {
50
+ return {
51
+ onRows: (rows) => this.replace(rows),
52
+ onOps: (ops) => this.apply(ops),
53
+ onRevalidating: () => this.markRevalidating(),
54
+ onError: (error) => this.fail(error)
55
+ };
56
+ }
57
+ replace(rows) {
58
+ if (this.closed) return;
59
+ this.rows = rows;
60
+ this.publish({ status: "ready", rows: this.rows, revalidating: false }, { kind: "rows" });
61
+ }
62
+ apply(ops) {
63
+ if (this.closed) return;
64
+ if (this.state.status !== "ready") {
65
+ this.fail(new RemoteError("INVALID_RESPONSE", "A live query received row operations before its first result"));
66
+ return;
67
+ }
68
+ const next = this.rows.slice();
69
+ for (const op of ops) {
70
+ const highest = op.op === "insert" ? next.length : next.length - 1;
71
+ if (!Number.isInteger(op.index) || op.index < 0 || op.index > highest) {
72
+ this.fail(new RemoteError("INVALID_RESPONSE", "A live query received an operation outside its rows"));
73
+ return;
74
+ }
75
+ if (op.op === "insert") next.splice(op.index, 0, op.row);
76
+ else if (op.op === "update") next[op.index] = op.row;
77
+ else next.splice(op.index, 1);
78
+ }
79
+ this.rows = next;
80
+ this.publish({ status: "ready", rows: this.rows, revalidating: false }, { kind: "ops", ops });
81
+ }
82
+ markRevalidating() {
83
+ if (this.closed || this.state.status !== "ready" || this.state.revalidating) return;
84
+ this.publish({ status: "ready", rows: this.rows, revalidating: true }, { kind: "revalidating" });
85
+ }
86
+ fail(error) {
87
+ if (this.closed) return;
88
+ this.publish({ status: "error", error }, { kind: "error" });
89
+ }
90
+ publish(next, update) {
91
+ this.state = next;
92
+ for (const listener of [...this.listeners]) {
93
+ try {
94
+ listener(update);
95
+ } catch {
96
+ }
97
+ }
98
+ }
99
+ };
100
+
101
+ // src/client/subscription.ts
102
+ var RemoteSubscriptionBuilderImpl = class {
103
+ constructor(table, transport) {
104
+ this.table = table;
105
+ this.transport = transport;
106
+ }
107
+ conditions = {};
108
+ filter(conditions) {
109
+ this.conditions = { ...this.conditions, ...conditions };
110
+ return this;
111
+ }
112
+ subscribe(callback, options) {
113
+ const filter = Object.keys(this.conditions).length > 0 ? this.conditions : void 0;
114
+ return this.transport.subscribe(this.table, filter, callback, options);
115
+ }
116
+ };
117
+
118
+ // src/client/database-proxy.ts
119
+ var DEFAULT_LOAD_BATCH_SIZE = 1e3;
120
+ var READ_CONCERN_UNSUPPORTED_MESSAGE = "This transport does not carry a per-read concern to the server. Use the HTTP transport for a per-call readConcern, or set the client-wide readConcern that topology routing applies when choosing a node.";
121
+ function isAsyncIterable(value) {
122
+ return typeof value[Symbol.asyncIterator] === "function";
123
+ }
124
+ var RemoteDatabase = class {
125
+ constructor(id, transport, capabilities, onDispose) {
126
+ this.id = id;
127
+ this.transport = transport;
128
+ this.capabilities = capabilities;
129
+ this.onDispose = onDispose;
130
+ }
131
+ async query(operation, params, options) {
132
+ this.assertReadConcernReaches(options?.readConcern);
133
+ if (typeof operation !== "string") {
134
+ const named = await this.transport.queryNamed(
135
+ operation.name,
136
+ params,
137
+ options?.readConcern
138
+ );
139
+ return named.rows;
140
+ }
141
+ await this.capabilities.assertSqlAccepted();
142
+ const response = await this.transport.query(operation, params, options?.readConcern);
143
+ return response.rows;
144
+ }
145
+ assertReadConcernReaches(readConcern) {
146
+ if (readConcern === void 0 || this.transport.carriesReadConcern === true) return;
147
+ throw new RemoteError("INVALID_ARGUMENT", READ_CONCERN_UNSUPPORTED_MESSAGE);
148
+ }
149
+ async execute(operation, params, writeConcern) {
150
+ if (typeof operation !== "string") {
151
+ const named = await this.transport.executeNamed(
152
+ operation.name,
153
+ params,
154
+ writeConcern
155
+ );
156
+ return named.results;
157
+ }
158
+ await this.capabilities.assertSqlAccepted();
159
+ return this.transport.execute(operation, params);
160
+ }
161
+ async live(operation, args) {
162
+ const name = typeof operation === "string" ? operation : operation.name;
163
+ return RemoteLiveQuery.open(
164
+ (handlers) => this.transport.liveSubscribe(name, args, handlers, (refresh) => this.capabilities.registryDigest(refresh))
165
+ );
166
+ }
167
+ /**
168
+ * Execute multiple statements as a single atomic transaction.
169
+ * Returns an array of results, one per statement.
170
+ *
171
+ * The whole list travels in one request and commits or rolls back as a
172
+ * unit, so the client is never in the loop between statements.
173
+ */
174
+ async transaction(statements) {
175
+ await this.capabilities.assertSqlAccepted();
176
+ const response = await this.transport.transaction(statements);
177
+ return response.results;
178
+ }
179
+ /**
180
+ * Run the same statement once per parameter set as a single atomic
181
+ * transaction that commits with one fsync. Returns one result per
182
+ * parameter set, in order. Use this for a burst of same-shape writes
183
+ * (an import, a bulk insert) that must all commit or all roll back.
184
+ */
185
+ async batch(sql, paramsBatch, writeConcern) {
186
+ await this.capabilities.assertSqlAccepted();
187
+ const response = await this.transport.batch(sql, paramsBatch, writeConcern);
188
+ return response.results;
189
+ }
190
+ /**
191
+ * Load a whole dataset through the same statement, batching it into requests
192
+ * for you and paying the one fsyncing WAL checkpoint once, after the final
193
+ * batch. The configured durability is restored after every batch, so an
194
+ * import that stops partway never leaves the writer at the relaxed level.
195
+ * Prefer this over {@link load} for anything larger than a single request:
196
+ * it runs the finalize itself, so there is no checkpoint flag to forget.
197
+ *
198
+ * Accepts a synchronous or asynchronous iterable of parameter sets, so rows
199
+ * can stream from a file or the network without being held in memory at
200
+ * once. Returns the total rows loaded and changes applied.
201
+ *
202
+ * ```ts
203
+ * const summary = await db.loadAll(
204
+ * 'INSERT INTO events (id, payload) VALUES (?, ?)',
205
+ * rowStream,
206
+ * { batchSize: 5000, durability: 'off' },
207
+ * )
208
+ * ```
209
+ */
210
+ async loadAll(sql, rows, options) {
211
+ const batchSize = options?.batchSize ?? DEFAULT_LOAD_BATCH_SIZE;
212
+ if (!Number.isInteger(batchSize) || batchSize <= 0) {
213
+ throw new RemoteError("INVALID_ARGUMENT", "loadAll batchSize must be a positive integer");
214
+ }
215
+ await this.capabilities.assertSqlAccepted();
216
+ const durability = options?.durability;
217
+ const total = { rowsLoaded: 0, changes: 0 };
218
+ const send = async (paramsBatch, checkpoint) => {
219
+ const summary = await this.transport.load(sql, paramsBatch, durability, checkpoint);
220
+ total.rowsLoaded += summary.rowsLoaded;
221
+ total.changes += summary.changes;
222
+ };
223
+ let batch = [];
224
+ let previous = null;
225
+ const rotate = async () => {
226
+ if (previous !== null) await send(previous, false);
227
+ previous = batch;
228
+ batch = [];
229
+ };
230
+ if (isAsyncIterable(rows)) {
231
+ for await (const row of rows) {
232
+ batch.push(row);
233
+ if (batch.length >= batchSize) await rotate();
234
+ }
235
+ } else {
236
+ for (const row of rows) {
237
+ batch.push(row);
238
+ if (batch.length >= batchSize) await rotate();
239
+ }
240
+ }
241
+ if (batch.length > 0) {
242
+ if (previous !== null) await send(previous, false);
243
+ await send(batch, true);
244
+ } else if (previous !== null) {
245
+ await send(previous, true);
246
+ }
247
+ return total;
248
+ }
249
+ /**
250
+ * Load one batch of rows through the same statement with writer durability
251
+ * relaxed for the duration, then restored before this resolves. This is the
252
+ * low-level primitive; prefer {@link loadAll} for a dataset that spans more
253
+ * than one request, since it runs the finalize itself rather than relying on
254
+ * a `checkpoint` flag.
255
+ *
256
+ * Returns the total rows loaded and changes applied. When splitting a dataset
257
+ * across many `load` calls by hand, pass `checkpoint: false` on every call
258
+ * but the last so the one fsyncing WAL checkpoint runs once at the end; the
259
+ * configured durability is restored after each call regardless.
260
+ */
261
+ async load(sql, paramsBatch, durability, checkpoint) {
262
+ await this.capabilities.assertSqlAccepted();
263
+ return this.transport.load(sql, paramsBatch, durability, checkpoint);
264
+ }
265
+ /**
266
+ * Start building a CDC subscription for the given table.
267
+ * Chain `.filter()` to narrow the events, then call `.subscribe()`
268
+ * with a callback to begin receiving real-time change events.
269
+ *
270
+ * ```ts
271
+ * const sub = await db
272
+ * .on('orders')
273
+ * .filter({ status: 'pending' })
274
+ * .subscribe(event => console.log(event))
275
+ *
276
+ * // Later:
277
+ * sub.unsubscribe()
278
+ * ```
279
+ */
280
+ on(table) {
281
+ return new RemoteSubscriptionBuilderImpl(table, this.transport);
282
+ }
283
+ /**
284
+ * Close the transport for this database. After calling `close()`,
285
+ * all pending requests are rejected and new calls will throw.
286
+ */
287
+ close() {
288
+ this.transport.close();
289
+ this.onDispose?.();
290
+ }
291
+ };
292
+
293
+ // src/client/endpoint-urls.ts
294
+ function normaliseEndpointUrl(rawUrl) {
295
+ let parsed;
296
+ try {
297
+ parsed = new URL(rawUrl);
298
+ } catch {
299
+ throw new TypeError(`Endpoint URL '${rawUrl}' is invalid`);
300
+ }
301
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
302
+ throw new TypeError(`Endpoint URL '${rawUrl}' must use http or https`);
303
+ }
304
+ if (parsed.username || parsed.password) {
305
+ throw new TypeError(`Endpoint URL '${rawUrl}' must not contain credentials`);
306
+ }
307
+ if (parsed.hash) {
308
+ throw new TypeError(`Endpoint URL '${rawUrl}' must not contain a fragment`);
309
+ }
310
+ if (parsed.search) {
311
+ throw new TypeError(`Endpoint URL '${rawUrl}' must not contain a query string`);
312
+ }
313
+ parsed.pathname = parsed.pathname.replace(/\/+$/, "");
314
+ return parsed.toString().replace(/\/$/, "");
315
+ }
316
+ function toBaseUrl(url) {
317
+ return normaliseEndpointUrl(url);
318
+ }
319
+ function escapeRegExp(value) {
320
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
321
+ }
322
+ function toServerBaseUrl(url, databaseId) {
323
+ const base = toBaseUrl(url);
324
+ if (!databaseId) return base.replace(/\/db\/[^/]+$/i, "");
325
+ return base.replace(new RegExp(`/db/${escapeRegExp(encodeURIComponent(databaseId))}$`, "i"), "");
326
+ }
327
+ function toWsUrl(baseUrl) {
328
+ return baseUrl.replace(/^http:\/\//i, "ws://").replace(/^https:\/\//i, "wss://");
329
+ }
330
+
331
+ // src/client/http-json.ts
332
+ var DEFAULT_HTTP_REQUEST_TIMEOUT_MS = 3e4;
333
+ function unrefTimer(timer) {
334
+ const unrefable = timer;
335
+ unrefable.unref?.();
336
+ }
337
+ async function requestJson(url, init, timeoutMs) {
338
+ const controller = new AbortController();
339
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
340
+ unrefTimer(timer);
341
+ let response;
342
+ try {
343
+ response = await fetch(url, { ...init, signal: controller.signal });
344
+ } catch (err) {
345
+ throw new RemoteError(
346
+ "CONNECTION_ERROR",
347
+ `Failed to connect to ${url}: ${err instanceof Error ? err.message : String(err)}`
348
+ );
349
+ } finally {
350
+ clearTimeout(timer);
351
+ }
352
+ let data;
353
+ try {
354
+ data = await response.json();
355
+ } catch {
356
+ throw new RemoteError("INVALID_RESPONSE", `Server returned non-JSON response (HTTP ${response.status})`);
357
+ }
358
+ if (!response.ok) {
359
+ const errorData = data;
360
+ throw new RemoteError(
361
+ errorData.error?.code ?? "UNKNOWN_ERROR",
362
+ errorData.error?.message ?? `HTTP ${response.status}`
363
+ );
364
+ }
365
+ return data;
366
+ }
367
+ async function postJson(url, body, headers, timeoutMs = DEFAULT_HTTP_REQUEST_TIMEOUT_MS) {
368
+ return requestJson(
369
+ url,
370
+ {
371
+ method: "POST",
372
+ headers: { "content-type": "application/json", ...headers },
373
+ body: JSON.stringify(body)
374
+ },
375
+ timeoutMs
376
+ );
377
+ }
378
+ async function getJson(url, headers, timeoutMs = DEFAULT_HTTP_REQUEST_TIMEOUT_MS) {
379
+ return requestJson(url, { method: "GET", headers: { ...headers } }, timeoutMs);
380
+ }
381
+
382
+ // src/core/cdc/encoding.ts
383
+ var BLOB_TAG = "__sirannon_blob";
384
+ var INT_TAG = "__sirannon_int";
385
+ var SAFE_INT_BOUND = 9007199254740991n;
386
+ var HEX_BYTES_RE = /^[0-9a-fA-F]*$/;
387
+ var INT_PAYLOAD_RE = /^-?\d{1,19}$/;
388
+ function encodeTaggedValues(value) {
389
+ if (typeof value === "bigint" || isBinaryValue(value)) {
390
+ return encodeTaggedLeaf(value);
391
+ }
392
+ if (value === null || typeof value !== "object") return value;
393
+ if (Array.isArray(value)) {
394
+ return value.map(encodeTaggedValues);
395
+ }
396
+ if (typeof value.toJSON === "function") {
397
+ return value;
398
+ }
399
+ const obj = value;
400
+ const out = {};
401
+ for (const k of Object.keys(obj)) {
402
+ out[k] = encodeTaggedValues(obj[k]);
403
+ }
404
+ return out;
405
+ }
406
+ function encodeTaggedLeaf(value) {
407
+ if (typeof value === "bigint") {
408
+ if (value >= -SAFE_INT_BOUND && value <= SAFE_INT_BOUND) {
409
+ return Number(value);
410
+ }
411
+ return { [INT_TAG]: value.toString() };
412
+ }
413
+ if (isBinaryValue(value)) {
414
+ return { [BLOB_TAG]: encodeHexBytes(value) };
415
+ }
416
+ return value;
417
+ }
418
+ function decodeTaggedValues(value) {
419
+ if (value === null || typeof value !== "object") return value;
420
+ if (isBinaryValue(value)) return value;
421
+ if (Array.isArray(value)) {
422
+ return value.map(decodeTaggedValues);
423
+ }
424
+ const obj = value;
425
+ const keys = Object.keys(obj);
426
+ if (keys.length === 1) {
427
+ const tag = keys[0];
428
+ const tagValue = obj[tag];
429
+ if (tag === BLOB_TAG && typeof tagValue === "string") {
430
+ return decodeHexBytes(tagValue);
431
+ }
432
+ if (tag === INT_TAG && typeof tagValue === "string") {
433
+ if (!INT_PAYLOAD_RE.test(tagValue)) {
434
+ throw new Error("Invalid tagged integer payload");
435
+ }
436
+ return BigInt(tagValue);
437
+ }
438
+ }
439
+ const out = {};
440
+ for (const k of keys) {
441
+ out[k] = decodeTaggedValues(obj[k]);
442
+ }
443
+ return out;
444
+ }
445
+ function isBinaryValue(value) {
446
+ return value instanceof Uint8Array || typeof Buffer !== "undefined" && Buffer.isBuffer(value);
447
+ }
448
+ function encodeHexBytes(bytes) {
449
+ if (typeof Buffer !== "undefined") {
450
+ return Buffer.from(bytes).toString("hex").toUpperCase();
451
+ }
452
+ let hex = "";
453
+ for (const byte of bytes) {
454
+ hex += byte.toString(16).padStart(2, "0");
455
+ }
456
+ return hex.toUpperCase();
457
+ }
458
+ function decodeHexBytes(hex) {
459
+ if (hex.length % 2 !== 0 || !HEX_BYTES_RE.test(hex)) {
460
+ throw new Error("Invalid CDC BLOB hex payload");
461
+ }
462
+ if (typeof Buffer !== "undefined") {
463
+ return Buffer.from(hex, "hex");
464
+ }
465
+ const bytes = new Uint8Array(hex.length / 2);
466
+ for (let i = 0; i < hex.length; i += 2) {
467
+ const byte = Number.parseInt(hex.slice(i, i + 2), 16);
468
+ if (Number.isNaN(byte)) {
469
+ throw new Error("Invalid CDC BLOB hex payload");
470
+ }
471
+ bytes[i / 2] = byte;
472
+ }
473
+ return bytes;
474
+ }
475
+
476
+ // src/server/capabilities.ts
477
+ var REQUIRED_DEVICE_SYNC_CAPABILITIES = [
478
+ "sync.push",
479
+ "sync.echo-suppression",
480
+ "sync.ack",
481
+ "sync.resume",
482
+ "sync.snapshot",
483
+ "sync.migrations",
484
+ "sync.schema-gate",
485
+ "sync.stream-apply"
486
+ ];
487
+ var STAGED_STREAM_CAPABILITY = "sync.staged-stream";
488
+ var SQL_QUERY_CAPABILITY = "query.sql";
489
+
490
+ // src/client/sync-capabilities.ts
491
+ function validateCapabilities(raw) {
492
+ const record = raw;
493
+ if (record === null || typeof record !== "object" || !Array.isArray(record.capabilities) || record.capabilities.some((name) => typeof name !== "string")) {
494
+ throw new RemoteError("INVALID_RESPONSE", "Capabilities response is malformed");
495
+ }
496
+ const digest = record.registry?.digest;
497
+ if (digest !== void 0 && typeof digest !== "string") {
498
+ throw new RemoteError("INVALID_RESPONSE", "Capabilities response carries a non-string registry digest");
499
+ }
500
+ return { capabilities: record.capabilities, registryDigest: digest };
501
+ }
502
+ async function fetchCapabilityReport(options) {
503
+ return validateCapabilities(await getJson(`${options.url}/capabilities`, options.headers, options.requestTimeoutMs));
504
+ }
505
+ async function fetchServerCapabilities(options) {
506
+ return (await fetchCapabilityReport(options)).capabilities;
507
+ }
508
+ async function verifyDeviceSyncCapabilities(options) {
509
+ let capabilities;
510
+ try {
511
+ capabilities = await fetchServerCapabilities(options);
512
+ } catch (err) {
513
+ if (err instanceof RemoteError && err.code === "NOT_FOUND") {
514
+ throw new RemoteError(
515
+ "SYNC_UNSUPPORTED",
516
+ "The server does not announce capabilities; it predates device sync and cannot sync safely. Upgrade the server."
517
+ );
518
+ }
519
+ throw err;
520
+ }
521
+ const announced = new Set(capabilities);
522
+ const missing = REQUIRED_DEVICE_SYNC_CAPABILITIES.filter((name) => !announced.has(name));
523
+ if (missing.length > 0) {
524
+ throw new RemoteError(
525
+ "SYNC_UNSUPPORTED",
526
+ `The server does not support required device sync capabilities: ${missing.join(", ")}. Upgrade the server.`
527
+ );
528
+ }
529
+ return capabilities;
530
+ }
531
+
532
+ // src/client/server-capabilities.ts
533
+ var SQL_REFUSED_MESSAGE = "This server does not accept SQL over the network. Call a registered operation by name, or start the server with acceptSql: true.";
534
+ var SQL_UNCONFIRMED_MESSAGE = "GET /capabilities returned 404, so this client cannot confirm that the server accepts SQL and refuses to send it. Check that the URL reaches a sirannon-db server and that any proxy in front of it forwards /capabilities.";
535
+ var ServerCapabilities = class {
536
+ constructor(resolveUrl, headers, requestTimeoutMs = DEFAULT_HTTP_REQUEST_TIMEOUT_MS) {
537
+ this.resolveUrl = resolveUrl;
538
+ this.headers = headers;
539
+ this.requestTimeoutMs = requestTimeoutMs;
540
+ }
541
+ pending = null;
542
+ async read(refresh = false) {
543
+ if (refresh) this.pending = null;
544
+ if (this.pending === null) {
545
+ const request = this.fetch();
546
+ this.pending = request;
547
+ request.catch(() => {
548
+ if (this.pending === request) this.pending = null;
549
+ });
550
+ }
551
+ return this.pending;
552
+ }
553
+ async registryDigest(refresh = false) {
554
+ try {
555
+ return (await this.read(refresh)).registryDigest;
556
+ } catch (err) {
557
+ if (isMissingEndpoint(err)) return void 0;
558
+ throw err;
559
+ }
560
+ }
561
+ async assertSqlAccepted() {
562
+ let report;
563
+ try {
564
+ report = await this.read();
565
+ } catch (err) {
566
+ if (isMissingEndpoint(err)) throw new RemoteError("SQL_NOT_ACCEPTED", SQL_UNCONFIRMED_MESSAGE);
567
+ throw err;
568
+ }
569
+ if (!report.capabilities.includes(SQL_QUERY_CAPABILITY)) {
570
+ throw new RemoteError("SQL_NOT_ACCEPTED", SQL_REFUSED_MESSAGE);
571
+ }
572
+ }
573
+ async fetch() {
574
+ const url = await this.resolveUrl();
575
+ return fetchCapabilityReport({ url, headers: this.headers, requestTimeoutMs: this.requestTimeoutMs });
576
+ }
577
+ };
578
+ function isMissingEndpoint(err) {
579
+ return err instanceof RemoteError && err.code === "NOT_FOUND";
580
+ }
581
+
582
+ // src/client/transport/http.ts
583
+ var HttpTransport = class {
584
+ carriesReadConcern = true;
585
+ baseUrl;
586
+ headers;
587
+ closed = false;
588
+ constructor(baseUrl, headers) {
589
+ this.baseUrl = baseUrl.replace(/\/$/, "");
590
+ this.headers = {
591
+ "content-type": "application/json",
592
+ ...headers
593
+ };
594
+ }
595
+ async query(sql, params, readConcern) {
596
+ const response = await this.post("/query", {
597
+ sql,
598
+ params: encodeTaggedValues(params),
599
+ readConcern
600
+ });
601
+ return { rows: decodeTaggedValues(response.rows ?? []) };
602
+ }
603
+ async execute(sql, params) {
604
+ return this.post("/execute", { sql, params: encodeTaggedValues(params) });
605
+ }
606
+ async transaction(statements) {
607
+ return this.post("/transaction", {
608
+ statements: statements.map((stmt) => ({ sql: stmt.sql, params: encodeTaggedValues(stmt.params) }))
609
+ });
610
+ }
611
+ async batch(sql, paramsBatch, writeConcern) {
612
+ return this.post("/batch", {
613
+ sql,
614
+ paramsBatch: paramsBatch.map((entry) => encodeTaggedValues(entry)),
615
+ writeConcern
616
+ });
617
+ }
618
+ async load(sql, paramsBatch, durability, checkpoint) {
619
+ return this.post("/load", {
620
+ sql,
621
+ paramsBatch: paramsBatch.map((entry) => encodeTaggedValues(entry)),
622
+ durability,
623
+ checkpoint
624
+ });
625
+ }
626
+ async queryNamed(name, args, readConcern) {
627
+ const response = await this.post(`/query/${encodeURIComponent(name)}`, {
628
+ args: encodeTaggedValues(args),
629
+ readConcern
630
+ });
631
+ return { rows: decodeTaggedValues(response.rows ?? []) };
632
+ }
633
+ async executeNamed(name, args, writeConcern) {
634
+ return this.post(`/execute/${encodeURIComponent(name)}`, {
635
+ args: encodeTaggedValues(args),
636
+ writeConcern
637
+ });
638
+ }
639
+ async liveSubscribe() {
640
+ throw new RemoteError(
641
+ "TRANSPORT_ERROR",
642
+ 'Live queries require WebSocket transport. Create the client with { transport: "websocket" } to use them.'
643
+ );
644
+ }
645
+ async subscribe(_table, _filter, _callback) {
646
+ throw new RemoteError(
647
+ "TRANSPORT_ERROR",
648
+ 'Subscriptions require WebSocket transport. Create the client with { transport: "websocket" } to use real-time subscriptions.'
649
+ );
650
+ }
651
+ close() {
652
+ this.closed = true;
653
+ }
654
+ async post(path, body) {
655
+ if (this.closed) {
656
+ throw new RemoteError("TRANSPORT_ERROR", "Transport is closed");
657
+ }
658
+ const url = `${this.baseUrl}${path}`;
659
+ let response;
660
+ try {
661
+ response = await fetch(url, {
662
+ method: "POST",
663
+ headers: this.headers,
664
+ body: JSON.stringify(body)
665
+ });
666
+ } catch (err) {
667
+ throw new RemoteError(
668
+ "CONNECTION_ERROR",
669
+ `Failed to connect to ${url}: ${err instanceof Error ? err.message : String(err)}`
670
+ );
671
+ }
672
+ let data;
673
+ try {
674
+ data = await response.json();
675
+ } catch {
676
+ throw new RemoteError("INVALID_RESPONSE", `Server returned non-JSON response (HTTP ${response.status})`);
677
+ }
678
+ if (!response.ok) {
679
+ const errorData = data;
680
+ throw new RemoteError(
681
+ errorData.error?.code ?? "UNKNOWN_ERROR",
682
+ errorData.error?.message ?? `HTTP ${response.status}`
683
+ );
684
+ }
685
+ return data;
686
+ }
687
+ };
688
+
689
+ // src/core/ws-handshake.ts
690
+ var SIRANNON_WS_SUBPROTOCOL = "sirannon.v1";
691
+ var WS_CLOSE_UNAUTHENTICATED = 4401;
692
+ var WS_CLOSE_FORBIDDEN = 4403;
693
+ var REFUSAL_CLOSE_CODE_MIN = 4e3;
694
+ var REFUSAL_CLOSE_CODE_MAX = 4099;
695
+ function withSirannonSubprotocol(protocols) {
696
+ if (protocols === void 0) return void 0;
697
+ const configured = typeof protocols === "string" ? [protocols] : [...protocols];
698
+ return [SIRANNON_WS_SUBPROTOCOL, ...configured.filter((value) => value !== SIRANNON_WS_SUBPROTOCOL)];
699
+ }
700
+ function isRefusalCloseCode(code) {
701
+ if (code === WS_CLOSE_UNAUTHENTICATED || code === WS_CLOSE_FORBIDDEN) return true;
702
+ return code >= REFUSAL_CLOSE_CODE_MIN && code <= REFUSAL_CLOSE_CODE_MAX;
703
+ }
704
+ function refusalErrorCode(code) {
705
+ return code === WS_CLOSE_FORBIDDEN ? "FORBIDDEN" : "UNAUTHORIZED";
706
+ }
707
+
708
+ // src/client/transport/ws-headers.ts
709
+ var HEADERS_UNSUPPORTED_MESSAGE = "This runtime builds a WebSocket from the global constructor, which carries no handshake header, so 'headers' reaches the server on HTTP requests but never on the WebSocket upgrade. Carry the credential in 'webSocketProtocols' as well, which a browser handshake does carry, or create the client with { transport: 'http' }.";
710
+ function currentRuntime() {
711
+ return globalThis;
712
+ }
713
+ function runtimeSupportsHandshakeHeaders(runtime = currentRuntime()) {
714
+ if (runtime.Deno !== void 0 || runtime.window !== void 0) return false;
715
+ return typeof runtime.process?.versions?.node === "string";
716
+ }
717
+ function carriesSubprotocolCredential(protocols) {
718
+ return protocols !== void 0 && protocols.length > 0;
719
+ }
720
+ function assertHandshakeHeadersSupported(headers, webSocketProtocols, runtime = currentRuntime()) {
721
+ if (headers === void 0 || Object.keys(headers).length === 0) return;
722
+ if (carriesSubprotocolCredential(webSocketProtocols)) return;
723
+ if (runtimeSupportsHandshakeHeaders(runtime)) return;
724
+ throw new RemoteError("INVALID_ARGUMENT", HEADERS_UNSUPPORTED_MESSAGE);
725
+ }
726
+
727
+ // src/client/transport/ws-connect.ts
728
+ function createSocket(url, options) {
729
+ const { protocols, headers } = options;
730
+ if (headers !== void 0 && Object.keys(headers).length > 0 && runtimeSupportsHandshakeHeaders()) {
731
+ const construct = WebSocket;
732
+ return new construct(url, protocols === void 0 ? { headers } : { headers, protocols });
733
+ }
734
+ return protocols === void 0 ? new WebSocket(url) : new WebSocket(url, protocols);
735
+ }
736
+ function readCloseInfo(event) {
737
+ const raw = event;
738
+ return {
739
+ code: typeof raw.code === "number" ? raw.code : 0,
740
+ reason: typeof raw.reason === "string" ? raw.reason : ""
741
+ };
742
+ }
743
+ function refusalError(close) {
744
+ const detail = close.reason.length > 0 ? close.reason : "no reason given";
745
+ return new RemoteError(refusalErrorCode(close.code), `The server refused the WebSocket connection: ${detail}`);
746
+ }
747
+ function openWebSocket(url, options, callbacks) {
748
+ return new Promise((resolve, reject) => {
749
+ let settled = false;
750
+ const ws = createSocket(url, options);
751
+ const onOpen = () => {
752
+ settled = true;
753
+ callbacks.onConnected(ws);
754
+ resolve();
755
+ };
756
+ const onError = () => {
757
+ if (!settled) {
758
+ settled = true;
759
+ reject(new RemoteError("CONNECTION_ERROR", `Failed to connect to ${url}`));
760
+ }
761
+ };
762
+ const onClose = (event) => {
763
+ ws.removeEventListener("open", onOpen);
764
+ ws.removeEventListener("error", onError);
765
+ const close = readCloseInfo(event);
766
+ const refusal = isRefusalCloseCode(close.code) ? refusalError(close) : null;
767
+ if (refusal) {
768
+ callbacks.onRefused(refusal);
769
+ }
770
+ if (!settled) {
771
+ settled = true;
772
+ reject(
773
+ refusal ?? new RemoteError("CONNECTION_ERROR", `Connection closed during handshake: ${close.code} ${close.reason}`)
774
+ );
775
+ return;
776
+ }
777
+ callbacks.onDisconnected();
778
+ };
779
+ const onMessage = (event) => {
780
+ callbacks.onMessage(String(event.data));
781
+ };
782
+ ws.addEventListener("open", onOpen);
783
+ ws.addEventListener("error", onError);
784
+ ws.addEventListener("close", onClose);
785
+ ws.addEventListener("message", onMessage);
786
+ });
787
+ }
788
+
789
+ // src/client/transport/ws-subscription-state.ts
790
+ function applySubscribedMessage(sub, msg) {
791
+ if (msg.epoch !== void 0) {
792
+ sub.epoch = msg.epoch;
793
+ }
794
+ let baseline;
795
+ if (msg.seq !== void 0) {
796
+ try {
797
+ baseline = BigInt(msg.seq);
798
+ } catch {
799
+ baseline = void 0;
800
+ }
801
+ }
802
+ if (msg.resync) {
803
+ sub.lastSeq = baseline;
804
+ try {
805
+ sub.onReset?.();
806
+ } catch {
807
+ }
808
+ } else if (sub.lastSeq === void 0 && baseline !== void 0) {
809
+ sub.lastSeq = baseline;
810
+ }
811
+ try {
812
+ sub.onSubscribed?.({
813
+ seq: baseline,
814
+ epoch: sub.epoch,
815
+ resync: msg.resync === true,
816
+ maxUnacknowledgedChanges: msg.maxUnacknowledgedChanges
817
+ });
818
+ } catch {
819
+ }
820
+ }
821
+ function deliverChangeMessage(sub, msg) {
822
+ deliverWireEvent(sub, msg.event);
823
+ }
824
+ function deliverChangesMessage(sub, msg) {
825
+ if (!Array.isArray(msg.events)) return;
826
+ for (const event of msg.events) {
827
+ deliverWireEvent(sub, event);
828
+ }
829
+ }
830
+ function deliverWireEvent(sub, wire) {
831
+ try {
832
+ const event = {
833
+ type: wire.type,
834
+ table: wire.table,
835
+ row: decodeTaggedValues(wire.row),
836
+ oldRow: wire.oldRow === void 0 ? void 0 : decodeTaggedValues(wire.oldRow),
837
+ seq: BigInt(wire.seq),
838
+ timestamp: wire.timestamp,
839
+ ...wire.hlc !== void 0 ? { hlc: wire.hlc } : {},
840
+ ...wire.origin !== void 0 ? { origin: wire.origin } : {},
841
+ ...wire.rowId !== void 0 ? { rowId: wire.rowId } : {},
842
+ ...wire.txId !== void 0 ? { txId: wire.txId } : {},
843
+ ...wire.txEnd === true ? { txEnd: true } : {}
844
+ };
845
+ if (sub.lastSeq === void 0 || event.seq > sub.lastSeq) {
846
+ sub.lastSeq = event.seq;
847
+ }
848
+ sub.callback(event);
849
+ } catch {
850
+ }
851
+ }
852
+ function buildResubscribeMessage(id, sub) {
853
+ const resumeFrom = sub.resumeSeq === void 0 ? sub.lastSeq : sub.resumeSeq();
854
+ return {
855
+ type: "subscribe",
856
+ id,
857
+ table: sub.table,
858
+ ...sub.tables !== void 0 ? { tables: [...sub.tables] } : {},
859
+ ...sub.filter ? { filter: encodeTaggedValues(sub.filter) } : {},
860
+ ...resumeFrom !== void 0 ? { sinceSeq: resumeFrom.toString() } : {},
861
+ ...sub.epoch !== void 0 ? { epoch: sub.epoch } : {},
862
+ ...sub.deviceId !== void 0 ? { deviceId: sub.deviceId } : {},
863
+ ...sub.schemaVersion !== void 0 ? { schemaVersion: sub.schemaVersion } : {},
864
+ ...sub.stagedStream === true ? { stagedStream: true } : {}
865
+ };
866
+ }
867
+
868
+ // src/client/transport/ws-inbound.ts
869
+ function routeServerMessage(raw, sinks) {
870
+ let msg;
871
+ try {
872
+ msg = JSON.parse(raw);
873
+ } catch {
874
+ return;
875
+ }
876
+ switch (msg.type) {
877
+ case "result":
878
+ sinks.pending.resolve(msg.id, msg.data);
879
+ return;
880
+ case "subscribed": {
881
+ const sub = sinks.subscriptions.get(msg.id);
882
+ if (sub) applySubscribedMessage(sub, msg);
883
+ sinks.live.deliverSubscribed(msg.id, msg.rows ?? []);
884
+ sinks.pending.resolve(msg.id, void 0);
885
+ return;
886
+ }
887
+ case "error": {
888
+ const failure = new RemoteError(msg.error.code, msg.error.message);
889
+ if (!sinks.pending.reject(msg.id, failure)) sinks.live.fail(msg.id, failure);
890
+ return;
891
+ }
892
+ case "change": {
893
+ const sub = sinks.subscriptions.get(msg.id);
894
+ if (sub) deliverChangeMessage(sub, msg);
895
+ return;
896
+ }
897
+ case "changes": {
898
+ const sub = sinks.subscriptions.get(msg.id);
899
+ if (sub) deliverChangesMessage(sub, msg);
900
+ return;
901
+ }
902
+ case "live":
903
+ sinks.live.deliver(msg.id, msg);
904
+ return;
905
+ case "unsubscribed":
906
+ return;
907
+ }
908
+ }
909
+
910
+ // src/client/transport/ws-live-state.ts
911
+ var LiveQueryRegistry = class {
912
+ constructor(port) {
913
+ this.port = port;
914
+ }
915
+ queries = /* @__PURE__ */ new Map();
916
+ get size() {
917
+ return this.queries.size;
918
+ }
919
+ async open(id, name, args, handlers, digest) {
920
+ const live = { name, args, handlers, digest, established: false };
921
+ this.queries.set(id, live);
922
+ try {
923
+ await this.send(id, live);
924
+ } catch (err) {
925
+ this.queries.delete(id);
926
+ throw err;
927
+ }
928
+ return {
929
+ unsubscribe: () => {
930
+ this.queries.delete(id);
931
+ this.port.sendUnsubscribe(id);
932
+ }
933
+ };
934
+ }
935
+ deliverSubscribed(id, rows) {
936
+ this.queries.get(id)?.handlers.onRows(decodeLiveRows(rows));
937
+ }
938
+ deliver(id, message) {
939
+ const live = this.queries.get(id);
940
+ if (live !== void 0) deliverLiveMessage(live, message);
941
+ }
942
+ fail(id, error) {
943
+ this.queries.get(id)?.handlers.onError(error);
944
+ }
945
+ markDisconnected() {
946
+ for (const live of this.queries.values()) {
947
+ live.established = false;
948
+ live.handlers.onRevalidating();
949
+ }
950
+ }
951
+ clear() {
952
+ this.queries.clear();
953
+ }
954
+ async restart() {
955
+ let unreachable = null;
956
+ for (const [id, live] of [...this.queries.entries()]) {
957
+ if (this.port.isClosed() || live.established) continue;
958
+ try {
959
+ await this.send(id, live);
960
+ } catch (err) {
961
+ const failure = err instanceof RemoteError ? err : new RemoteError("CONNECTION_ERROR", "The live query could not restart");
962
+ if (failure.code === "CONNECTION_ERROR" || failure.code === "TIMEOUT") {
963
+ unreachable = failure;
964
+ continue;
965
+ }
966
+ this.queries.delete(id);
967
+ live.handlers.onError(failure);
968
+ }
969
+ }
970
+ if (unreachable !== null && !this.port.isClosed()) throw unreachable;
971
+ }
972
+ async send(id, live) {
973
+ const digest = await live.digest?.(false);
974
+ try {
975
+ await this.port.request(liveSubscribeMessage(id, live, digest));
976
+ } catch (err) {
977
+ if (!(err instanceof RemoteError) || err.code !== "REGISTRY_MISMATCH" || live.digest === void 0) {
978
+ throw err;
979
+ }
980
+ await this.port.request(liveSubscribeMessage(id, live, await live.digest(true)));
981
+ }
982
+ live.established = true;
983
+ }
984
+ };
985
+ function liveSubscribeMessage(id, live, registryDigest) {
986
+ return {
987
+ type: "subscribe",
988
+ id,
989
+ name: live.name,
990
+ ...live.args === void 0 ? {} : { args: encodeTaggedValues(live.args) },
991
+ ...registryDigest === void 0 ? {} : { registryDigest }
992
+ };
993
+ }
994
+ function decodeLiveRows(rows) {
995
+ return decodeTaggedValues(rows);
996
+ }
997
+ function deliverLiveMessage(live, msg) {
998
+ if (msg.rows !== void 0) {
999
+ live.handlers.onRows(decodeLiveRows(msg.rows));
1000
+ return;
1001
+ }
1002
+ if (msg.ops !== void 0) {
1003
+ live.handlers.onOps(msg.ops.map(decodeLiveOp));
1004
+ return;
1005
+ }
1006
+ if (msg.revalidating === true) {
1007
+ live.handlers.onRevalidating();
1008
+ return;
1009
+ }
1010
+ live.handlers.onError(new RemoteError("INVALID_RESPONSE", "A live message carried no rows, operations, or status"));
1011
+ }
1012
+ function decodeLiveOp(op) {
1013
+ if (op.op === "delete") return { op: "delete", index: op.index };
1014
+ return { op: op.op, index: op.index, row: decodeTaggedValues(op.row) };
1015
+ }
1016
+
1017
+ // src/client/transport/ws-pending.ts
1018
+ var PendingRequests = class {
1019
+ constructor(timeoutMs) {
1020
+ this.timeoutMs = timeoutMs;
1021
+ }
1022
+ requests = /* @__PURE__ */ new Map();
1023
+ start(id, send) {
1024
+ return new Promise((resolve, reject) => {
1025
+ const timer = this.timeoutMs > 0 ? setTimeout(() => {
1026
+ this.requests.delete(id);
1027
+ reject(new RemoteError("TIMEOUT", `Request timed out after ${this.timeoutMs}ms`));
1028
+ }, this.timeoutMs) : void 0;
1029
+ this.requests.set(id, { resolve, reject, timer });
1030
+ send();
1031
+ });
1032
+ }
1033
+ resolve(id, value) {
1034
+ const pending = this.take(id);
1035
+ if (pending === void 0) return false;
1036
+ pending.resolve(value);
1037
+ return true;
1038
+ }
1039
+ reject(id, error) {
1040
+ const pending = this.take(id);
1041
+ if (pending === void 0) return false;
1042
+ pending.reject(error);
1043
+ return true;
1044
+ }
1045
+ rejectAll(error) {
1046
+ for (const [, pending] of this.requests) {
1047
+ clearTimeout(pending.timer);
1048
+ pending.reject(error);
1049
+ }
1050
+ this.requests.clear();
1051
+ }
1052
+ take(id) {
1053
+ const pending = this.requests.get(id);
1054
+ if (pending === void 0) return void 0;
1055
+ clearTimeout(pending.timer);
1056
+ this.requests.delete(id);
1057
+ return pending;
1058
+ }
1059
+ };
1060
+
1061
+ // src/client/transport/ws.ts
1062
+ var DEFAULT_REQUEST_TIMEOUT = 3e4;
1063
+ var WebSocketTransport = class {
1064
+ carriesReadConcern = true;
1065
+ ws = null;
1066
+ url;
1067
+ autoReconnect;
1068
+ reconnectInterval;
1069
+ requestTimeout;
1070
+ protocols;
1071
+ headers;
1072
+ refusal = null;
1073
+ pending;
1074
+ activeSubscriptions = /* @__PURE__ */ new Map();
1075
+ liveQueries = new LiveQueryRegistry({
1076
+ request: (message) => this.request(message),
1077
+ sendUnsubscribe: (id) => this.sendUnsubscribe(id),
1078
+ isClosed: () => this.closed
1079
+ });
1080
+ idCounter = 0;
1081
+ closed = false;
1082
+ connectPromise = null;
1083
+ reconnectTimer = null;
1084
+ constructor(url, options) {
1085
+ this.url = url;
1086
+ this.autoReconnect = options?.autoReconnect ?? true;
1087
+ this.reconnectInterval = options?.reconnectInterval ?? 1e3;
1088
+ this.requestTimeout = options?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
1089
+ this.protocols = withSirannonSubprotocol(options?.protocols);
1090
+ this.headers = options?.headers;
1091
+ this.pending = new PendingRequests(this.requestTimeout);
1092
+ }
1093
+ async query(sql, params, readConcern) {
1094
+ await this.ensureConnected();
1095
+ const id = this.nextId();
1096
+ const response = await this.request({
1097
+ type: "query",
1098
+ id,
1099
+ sql,
1100
+ params: encodeTaggedValues(params),
1101
+ ...readConcern ? { readConcern } : {}
1102
+ });
1103
+ return { rows: decodeTaggedValues(response.rows ?? []) };
1104
+ }
1105
+ async execute(sql, params) {
1106
+ await this.ensureConnected();
1107
+ const id = this.nextId();
1108
+ return this.request({
1109
+ type: "execute",
1110
+ id,
1111
+ sql,
1112
+ params: encodeTaggedValues(params)
1113
+ });
1114
+ }
1115
+ async transaction(statements) {
1116
+ await this.ensureConnected();
1117
+ const id = this.nextId();
1118
+ return this.request({
1119
+ type: "transaction",
1120
+ id,
1121
+ statements: statements.map((stmt) => ({
1122
+ sql: stmt.sql,
1123
+ params: encodeTaggedValues(stmt.params)
1124
+ }))
1125
+ });
1126
+ }
1127
+ async batch(sql, paramsBatch, writeConcern) {
1128
+ await this.ensureConnected();
1129
+ const id = this.nextId();
1130
+ return this.request({
1131
+ type: "batch",
1132
+ id,
1133
+ sql,
1134
+ paramsBatch: paramsBatch.map((entry) => encodeTaggedValues(entry)),
1135
+ ...writeConcern ? { writeConcern } : {}
1136
+ });
1137
+ }
1138
+ async load(sql, paramsBatch, durability, checkpoint) {
1139
+ await this.ensureConnected();
1140
+ const id = this.nextId();
1141
+ return this.request({
1142
+ type: "load",
1143
+ id,
1144
+ sql,
1145
+ paramsBatch: paramsBatch.map((entry) => encodeTaggedValues(entry)),
1146
+ ...durability ? { durability } : {},
1147
+ ...checkpoint !== void 0 ? { checkpoint } : {}
1148
+ });
1149
+ }
1150
+ async queryNamed(name, args, readConcern) {
1151
+ await this.ensureConnected();
1152
+ const id = this.nextId();
1153
+ const response = await this.request({
1154
+ type: "query",
1155
+ id,
1156
+ name,
1157
+ ...args === void 0 ? {} : { args: encodeTaggedValues(args) },
1158
+ ...readConcern ? { readConcern } : {}
1159
+ });
1160
+ return { rows: decodeTaggedValues(response.rows ?? []) };
1161
+ }
1162
+ async executeNamed(name, args, writeConcern) {
1163
+ await this.ensureConnected();
1164
+ const id = this.nextId();
1165
+ return this.request({
1166
+ type: "execute",
1167
+ id,
1168
+ name,
1169
+ ...args === void 0 ? {} : { args: encodeTaggedValues(args) },
1170
+ ...writeConcern ? { writeConcern } : {}
1171
+ });
1172
+ }
1173
+ async liveSubscribe(name, args, handlers, registryDigest) {
1174
+ await this.ensureConnected();
1175
+ return this.liveQueries.open(this.nextId(), name, args, handlers, registryDigest);
1176
+ }
1177
+ async subscribe(table, filter, callback, options) {
1178
+ await this.ensureConnected();
1179
+ const id = this.nextId();
1180
+ this.activeSubscriptions.set(id, {
1181
+ table,
1182
+ filter,
1183
+ callback,
1184
+ onReset: options?.onReset,
1185
+ onSubscribed: options?.onSubscribed,
1186
+ deviceId: options?.deviceId,
1187
+ tables: options?.tables,
1188
+ schemaVersion: options?.schemaVersion,
1189
+ lastSeq: options?.sinceSeq,
1190
+ resumeSeq: options?.getResumeSeq,
1191
+ epoch: options?.epoch,
1192
+ stagedStream: options?.stagedStream
1193
+ });
1194
+ try {
1195
+ const msg = {
1196
+ type: "subscribe",
1197
+ id,
1198
+ table,
1199
+ ...filter ? { filter: encodeTaggedValues(filter) } : {},
1200
+ ...options?.tables !== void 0 ? { tables: [...options.tables] } : {},
1201
+ ...options?.sinceSeq !== void 0 ? { sinceSeq: options.sinceSeq.toString() } : {},
1202
+ ...options?.epoch !== void 0 ? { epoch: options.epoch } : {},
1203
+ ...options?.deviceId !== void 0 ? { deviceId: options.deviceId } : {},
1204
+ ...options?.schemaVersion !== void 0 ? { schemaVersion: options.schemaVersion } : {},
1205
+ ...options?.stagedStream === true ? { stagedStream: true } : {}
1206
+ };
1207
+ await this.request(msg);
1208
+ } catch (err) {
1209
+ this.activeSubscriptions.delete(id);
1210
+ throw err;
1211
+ }
1212
+ return {
1213
+ unsubscribe: () => {
1214
+ this.activeSubscriptions.delete(id);
1215
+ this.sendUnsubscribe(id);
1216
+ }
1217
+ };
1218
+ }
1219
+ sendUnsubscribe(id) {
1220
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
1221
+ this.ws.send(JSON.stringify({ type: "unsubscribe", id }));
1222
+ }
1223
+ }
1224
+ async ack(deviceId, seq) {
1225
+ await this.ensureConnected();
1226
+ const id = this.nextId();
1227
+ return this.request({ type: "ack", id, deviceId, seq: seq.toString() });
1228
+ }
1229
+ close() {
1230
+ this.closed = true;
1231
+ this.cancelReconnect();
1232
+ this.pending.rejectAll(new RemoteError("TRANSPORT_ERROR", "Transport closed"));
1233
+ this.activeSubscriptions.clear();
1234
+ this.liveQueries.clear();
1235
+ if (this.ws) {
1236
+ this.ws.close(1e3, "Client closed");
1237
+ this.ws = null;
1238
+ }
1239
+ this.connectPromise = null;
1240
+ }
1241
+ nextId() {
1242
+ return `c_${++this.idCounter}_${Date.now()}`;
1243
+ }
1244
+ async ensureConnected() {
1245
+ if (this.closed) {
1246
+ throw new RemoteError("TRANSPORT_ERROR", "Transport is closed");
1247
+ }
1248
+ if (this.refusal) {
1249
+ throw this.refusal;
1250
+ }
1251
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
1252
+ return;
1253
+ }
1254
+ if (!this.connectPromise) {
1255
+ this.connectPromise = this.connect().finally(() => {
1256
+ this.connectPromise = null;
1257
+ });
1258
+ }
1259
+ return this.connectPromise;
1260
+ }
1261
+ connect() {
1262
+ return openWebSocket(
1263
+ this.url,
1264
+ { protocols: this.protocols, headers: this.headers },
1265
+ {
1266
+ onConnected: (ws) => {
1267
+ this.ws = ws;
1268
+ },
1269
+ onRefused: (error) => {
1270
+ this.refusal = error;
1271
+ },
1272
+ onDisconnected: () => {
1273
+ this.ws = null;
1274
+ this.handleDisconnect();
1275
+ },
1276
+ onMessage: (raw) => routeServerMessage(raw, {
1277
+ pending: this.pending,
1278
+ subscriptions: this.activeSubscriptions,
1279
+ live: this.liveQueries
1280
+ })
1281
+ }
1282
+ );
1283
+ }
1284
+ handleDisconnect() {
1285
+ this.pending.rejectAll(this.refusal ?? new RemoteError("CONNECTION_ERROR", "WebSocket disconnected"));
1286
+ this.liveQueries.markDisconnected();
1287
+ const restorable = this.activeSubscriptions.size + this.liveQueries.size;
1288
+ if (this.autoReconnect && !this.closed && this.refusal === null && restorable > 0) {
1289
+ this.scheduleReconnect();
1290
+ }
1291
+ }
1292
+ scheduleReconnect() {
1293
+ if (this.reconnectTimer !== null || this.closed) {
1294
+ return;
1295
+ }
1296
+ this.reconnectTimer = setTimeout(async () => {
1297
+ this.reconnectTimer = null;
1298
+ if (this.closed) return;
1299
+ try {
1300
+ await this.ensureConnected();
1301
+ await this.resubscribeAll();
1302
+ } catch {
1303
+ if (!this.closed && this.refusal === null && this.activeSubscriptions.size + this.liveQueries.size > 0) {
1304
+ this.scheduleReconnect();
1305
+ }
1306
+ }
1307
+ }, this.reconnectInterval);
1308
+ }
1309
+ async resubscribeAll() {
1310
+ const entries = [...this.activeSubscriptions.entries()];
1311
+ for (const [id, sub] of entries) {
1312
+ if (this.closed) break;
1313
+ try {
1314
+ await this.request(buildResubscribeMessage(id, sub));
1315
+ } catch {
1316
+ this.activeSubscriptions.delete(id);
1317
+ }
1318
+ }
1319
+ await this.liveQueries.restart();
1320
+ }
1321
+ request(msg) {
1322
+ if (this.refusal) {
1323
+ return Promise.reject(this.refusal);
1324
+ }
1325
+ const socket = this.ws;
1326
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
1327
+ return Promise.reject(new RemoteError("CONNECTION_ERROR", "WebSocket is not connected"));
1328
+ }
1329
+ return this.pending.start(msg.id, () => socket.send(JSON.stringify(msg)));
1330
+ }
1331
+ cancelReconnect() {
1332
+ if (this.reconnectTimer !== null) {
1333
+ clearTimeout(this.reconnectTimer);
1334
+ this.reconnectTimer = null;
1335
+ }
1336
+ }
1337
+ };
1338
+
1339
+ // src/client/client-base.ts
1340
+ function resolveTransportSettings(options) {
1341
+ const transport = options?.transport ?? "websocket";
1342
+ if (transport === "websocket") {
1343
+ assertHandshakeHeadersSupported(options?.headers, options?.webSocketProtocols);
1344
+ }
1345
+ return {
1346
+ transport,
1347
+ headers: options?.headers,
1348
+ webSocketProtocols: options?.webSocketProtocols,
1349
+ autoReconnect: options?.autoReconnect ?? true,
1350
+ reconnectInterval: options?.reconnectInterval ?? 1e3,
1351
+ requestTimeout: options?.requestTimeout
1352
+ };
1353
+ }
1354
+ function createEndpointTransport(settings, baseUrl, databaseId) {
1355
+ const base = toBaseUrl(baseUrl);
1356
+ const encodedId = encodeURIComponent(databaseId);
1357
+ if (settings.transport === "http") {
1358
+ return new HttpTransport(`${base}/db/${encodedId}`, settings.headers);
1359
+ }
1360
+ return new WebSocketTransport(`${toWsUrl(base)}/db/${encodedId}`, {
1361
+ autoReconnect: settings.autoReconnect,
1362
+ reconnectInterval: settings.reconnectInterval,
1363
+ protocols: settings.webSocketProtocols,
1364
+ headers: settings.headers,
1365
+ requestTimeout: settings.requestTimeout
1366
+ });
1367
+ }
1368
+ var DatabaseClient = class {
1369
+ settings;
1370
+ databases = /* @__PURE__ */ new Map();
1371
+ closed = false;
1372
+ constructor(options) {
1373
+ this.settings = resolveTransportSettings(options);
1374
+ }
1375
+ database(id) {
1376
+ if (this.closed) {
1377
+ throw new Error("Client is closed");
1378
+ }
1379
+ const existing = this.databases.get(id);
1380
+ if (existing) {
1381
+ return existing;
1382
+ }
1383
+ const db = new RemoteDatabase(id, this.createTransport(id), this.createCapabilities(id), () => {
1384
+ this.databases.delete(id);
1385
+ });
1386
+ this.databases.set(id, db);
1387
+ return db;
1388
+ }
1389
+ createCapabilities(databaseId) {
1390
+ const timeout = this.settings.requestTimeout;
1391
+ return new ServerCapabilities(
1392
+ () => this.resolveServerUrl(databaseId),
1393
+ this.settings.headers,
1394
+ timeout !== void 0 && timeout > 0 ? timeout : DEFAULT_HTTP_REQUEST_TIMEOUT_MS
1395
+ );
1396
+ }
1397
+ close() {
1398
+ this.closed = true;
1399
+ const openDatabases = [...this.databases.values()];
1400
+ this.databases.clear();
1401
+ for (const db of openDatabases) {
1402
+ db.close();
1403
+ }
1404
+ }
1405
+ };
1406
+
1407
+ export { DEFAULT_HTTP_REQUEST_TIMEOUT_MS, DatabaseClient, HttpTransport, RemoteDatabase, RemoteError, RemoteLiveQuery, RemoteSubscriptionBuilderImpl, SQL_REFUSED_MESSAGE, STAGED_STREAM_CAPABILITY, ServerCapabilities, WebSocketTransport, createEndpointTransport, decodeTaggedValues, encodeTaggedValues, postJson, toBaseUrl, toServerBaseUrl, toWsUrl, unrefTimer, verifyDeviceSyncCapabilities };