@delali/sirannon-db 0.1.1
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.
- package/LICENSE +201 -0
- package/NOTICE +14 -0
- package/README.md +418 -0
- package/dist/chunk-VI4UP4RR.mjs +417 -0
- package/dist/client/index.d.ts +223 -0
- package/dist/client/index.mjs +479 -0
- package/dist/core/index.d.ts +295 -0
- package/dist/core/index.mjs +1346 -0
- package/dist/protocol-BX1H-_Mz.d.ts +104 -0
- package/dist/server/index.d.ts +103 -0
- package/dist/server/index.mjs +808 -0
- package/dist/sirannon-BJ8Yd1Uf.d.ts +148 -0
- package/dist/types-DArCObcu.d.ts +186 -0
- package/package.json +87 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var SirannonError = class extends Error {
|
|
3
|
+
constructor(message, code) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = "SirannonError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var DatabaseNotFoundError = class extends SirannonError {
|
|
10
|
+
constructor(id) {
|
|
11
|
+
super(`Database '${id}' not found`, "DATABASE_NOT_FOUND");
|
|
12
|
+
this.name = "DatabaseNotFoundError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var DatabaseAlreadyExistsError = class extends SirannonError {
|
|
16
|
+
constructor(id) {
|
|
17
|
+
super(`Database '${id}' already exists`, "DATABASE_ALREADY_EXISTS");
|
|
18
|
+
this.name = "DatabaseAlreadyExistsError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var ReadOnlyError = class extends SirannonError {
|
|
22
|
+
constructor(id) {
|
|
23
|
+
super(`Database '${id}' is read-only`, "READ_ONLY");
|
|
24
|
+
this.name = "ReadOnlyError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var QueryError = class extends SirannonError {
|
|
28
|
+
constructor(message, sql) {
|
|
29
|
+
super(message, "QUERY_ERROR");
|
|
30
|
+
this.sql = sql;
|
|
31
|
+
this.name = "QueryError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var TransactionError = class extends SirannonError {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message, "TRANSACTION_ERROR");
|
|
37
|
+
this.name = "TransactionError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var MigrationError = class extends SirannonError {
|
|
41
|
+
constructor(message, version, code = "MIGRATION_ERROR") {
|
|
42
|
+
super(message, code);
|
|
43
|
+
this.version = version;
|
|
44
|
+
this.name = "MigrationError";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var HookDeniedError = class extends SirannonError {
|
|
48
|
+
constructor(hookName, reason) {
|
|
49
|
+
super(
|
|
50
|
+
reason ? `Hook '${hookName}' denied the operation: ${reason}` : `Hook '${hookName}' denied the operation`,
|
|
51
|
+
"HOOK_DENIED"
|
|
52
|
+
);
|
|
53
|
+
this.name = "HookDeniedError";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var CDCError = class extends SirannonError {
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message, "CDC_ERROR");
|
|
59
|
+
this.name = "CDCError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var BackupError = class extends SirannonError {
|
|
63
|
+
constructor(message) {
|
|
64
|
+
super(message, "BACKUP_ERROR");
|
|
65
|
+
this.name = "BackupError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var ConnectionPoolError = class extends SirannonError {
|
|
69
|
+
constructor(message) {
|
|
70
|
+
super(message, "CONNECTION_POOL_ERROR");
|
|
71
|
+
this.name = "ConnectionPoolError";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var MaxDatabasesError = class extends SirannonError {
|
|
75
|
+
constructor(max) {
|
|
76
|
+
super(`Maximum number of open databases (${max}) reached`, "MAX_DATABASES");
|
|
77
|
+
this.name = "MaxDatabasesError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var ExtensionError = class extends SirannonError {
|
|
81
|
+
constructor(path, cause) {
|
|
82
|
+
super(
|
|
83
|
+
cause ? `Failed to load extension '${path}': ${cause}` : `Failed to load extension '${path}'`,
|
|
84
|
+
"EXTENSION_ERROR"
|
|
85
|
+
);
|
|
86
|
+
this.name = "ExtensionError";
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/core/cdc/change-tracker.ts
|
|
91
|
+
var DEFAULT_RETENTION_MS = 36e5;
|
|
92
|
+
var DEFAULT_CHANGES_TABLE = "_sirannon_changes";
|
|
93
|
+
var DEFAULT_POLL_BATCH_SIZE = 1e3;
|
|
94
|
+
var IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
95
|
+
var ChangeTracker = class {
|
|
96
|
+
watched = /* @__PURE__ */ new Map();
|
|
97
|
+
lastSeq = 0;
|
|
98
|
+
retentionMs;
|
|
99
|
+
changesTable;
|
|
100
|
+
pollBatchSize;
|
|
101
|
+
changesTableReady = false;
|
|
102
|
+
watchedTablesCache = null;
|
|
103
|
+
stmtCache = /* @__PURE__ */ new WeakMap();
|
|
104
|
+
constructor(options) {
|
|
105
|
+
this.retentionMs = options?.retention ?? DEFAULT_RETENTION_MS;
|
|
106
|
+
this.changesTable = options?.changesTable ?? DEFAULT_CHANGES_TABLE;
|
|
107
|
+
this.pollBatchSize = options?.pollBatchSize ?? DEFAULT_POLL_BATCH_SIZE;
|
|
108
|
+
this.assertIdentifier(this.changesTable, "changes table name");
|
|
109
|
+
}
|
|
110
|
+
watch(db, table) {
|
|
111
|
+
this.assertIdentifier(table, "table name");
|
|
112
|
+
this.ensureChangesTable(db);
|
|
113
|
+
const columns = this.getColumns(db, table);
|
|
114
|
+
if (columns.length === 0) {
|
|
115
|
+
throw new CDCError(`Table '${table}' does not exist or has no columns`);
|
|
116
|
+
}
|
|
117
|
+
for (const col of columns) {
|
|
118
|
+
this.assertIdentifier(col, `column name in table '${table}'`);
|
|
119
|
+
}
|
|
120
|
+
const pkColumns = this.getPkColumns(db, table);
|
|
121
|
+
const existing = this.watched.get(table);
|
|
122
|
+
if (existing) {
|
|
123
|
+
const same = existing.columns.length === columns.length && existing.columns.every((col, i) => col === columns[i]);
|
|
124
|
+
if (same) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.dropTriggers(db, table);
|
|
128
|
+
}
|
|
129
|
+
this.installTriggers(db, table, columns, pkColumns);
|
|
130
|
+
this.watched.set(table, { table, columns, pkColumns });
|
|
131
|
+
this.watchedTablesCache = null;
|
|
132
|
+
}
|
|
133
|
+
unwatch(db, table) {
|
|
134
|
+
if (!this.watched.has(table)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.dropTriggers(db, table);
|
|
138
|
+
this.watched.delete(table);
|
|
139
|
+
this.watchedTablesCache = null;
|
|
140
|
+
}
|
|
141
|
+
poll(db) {
|
|
142
|
+
if (!this.changesTableReady) {
|
|
143
|
+
this.detectChangesTable(db);
|
|
144
|
+
if (!this.changesTableReady) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const stmt = this.getStmt(
|
|
149
|
+
db,
|
|
150
|
+
"poll",
|
|
151
|
+
`SELECT seq, table_name, operation, row_id, changed_at, old_data, new_data
|
|
152
|
+
FROM "${this.changesTable}"
|
|
153
|
+
WHERE seq > ?
|
|
154
|
+
ORDER BY seq ASC
|
|
155
|
+
LIMIT ?`
|
|
156
|
+
);
|
|
157
|
+
const rows = stmt.all(this.lastSeq, this.pollBatchSize);
|
|
158
|
+
if (rows.length === 0) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
const events = [];
|
|
162
|
+
for (const row of rows) {
|
|
163
|
+
events.push({
|
|
164
|
+
type: row.operation.toLowerCase(),
|
|
165
|
+
table: row.table_name,
|
|
166
|
+
row: row.new_data ? JSON.parse(row.new_data) : {},
|
|
167
|
+
oldRow: row.old_data ? JSON.parse(row.old_data) : void 0,
|
|
168
|
+
seq: BigInt(row.seq),
|
|
169
|
+
timestamp: row.changed_at
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
this.lastSeq = rows[rows.length - 1].seq;
|
|
173
|
+
return events;
|
|
174
|
+
}
|
|
175
|
+
cleanup(db) {
|
|
176
|
+
if (!this.changesTableReady) {
|
|
177
|
+
this.detectChangesTable(db);
|
|
178
|
+
if (!this.changesTableReady) {
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const cutoff = Date.now() / 1e3 - this.retentionMs / 1e3;
|
|
183
|
+
if (this.lastSeq > 0) {
|
|
184
|
+
const stmt2 = this.getStmt(
|
|
185
|
+
db,
|
|
186
|
+
"cleanup_coordinated",
|
|
187
|
+
`DELETE FROM "${this.changesTable}" WHERE changed_at < ? AND seq <= ?`
|
|
188
|
+
);
|
|
189
|
+
return stmt2.run(cutoff, this.lastSeq).changes;
|
|
190
|
+
}
|
|
191
|
+
const stmt = this.getStmt(db, "cleanup", `DELETE FROM "${this.changesTable}" WHERE changed_at < ?`);
|
|
192
|
+
return stmt.run(cutoff).changes;
|
|
193
|
+
}
|
|
194
|
+
get watchedTables() {
|
|
195
|
+
if (!this.watchedTablesCache) {
|
|
196
|
+
this.watchedTablesCache = new Set(this.watched.keys());
|
|
197
|
+
}
|
|
198
|
+
return this.watchedTablesCache;
|
|
199
|
+
}
|
|
200
|
+
assertIdentifier(name, label) {
|
|
201
|
+
if (!IDENTIFIER_RE.test(name)) {
|
|
202
|
+
throw new CDCError(
|
|
203
|
+
`Invalid ${label} '${name}': must contain only letters, digits, and underscores, and start with a letter or underscore`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
detectChangesTable(db) {
|
|
208
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(this.changesTable);
|
|
209
|
+
if (row) {
|
|
210
|
+
this.changesTableReady = true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
ensureChangesTable(db) {
|
|
214
|
+
if (this.changesTableReady) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
db.exec(`
|
|
218
|
+
CREATE TABLE IF NOT EXISTS "${this.changesTable}" (
|
|
219
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
220
|
+
table_name TEXT NOT NULL,
|
|
221
|
+
operation TEXT NOT NULL,
|
|
222
|
+
row_id TEXT NOT NULL,
|
|
223
|
+
changed_at REAL NOT NULL DEFAULT (unixepoch('subsec')),
|
|
224
|
+
old_data TEXT,
|
|
225
|
+
new_data TEXT
|
|
226
|
+
)`);
|
|
227
|
+
db.exec(`CREATE INDEX IF NOT EXISTS "idx_${this.changesTable}_changed_at" ON "${this.changesTable}" (changed_at)`);
|
|
228
|
+
this.changesTableReady = true;
|
|
229
|
+
}
|
|
230
|
+
getColumns(db, table) {
|
|
231
|
+
const info = db.prepare(`PRAGMA table_info("${table}")`).all();
|
|
232
|
+
return info.map((col) => col.name);
|
|
233
|
+
}
|
|
234
|
+
getPkColumns(db, table) {
|
|
235
|
+
const info = db.prepare(`PRAGMA table_info("${table}")`).all();
|
|
236
|
+
return info.filter((col) => col.pk > 0).sort((a, b) => a.pk - b.pk).map((col) => col.name);
|
|
237
|
+
}
|
|
238
|
+
dropTriggers(db, table) {
|
|
239
|
+
db.exec(`DROP TRIGGER IF EXISTS "_sirannon_trg_${table}_insert"`);
|
|
240
|
+
db.exec(`DROP TRIGGER IF EXISTS "_sirannon_trg_${table}_update"`);
|
|
241
|
+
db.exec(`DROP TRIGGER IF EXISTS "_sirannon_trg_${table}_delete"`);
|
|
242
|
+
}
|
|
243
|
+
getStmt(db, key, sql) {
|
|
244
|
+
let stmts = this.stmtCache.get(db);
|
|
245
|
+
if (!stmts) {
|
|
246
|
+
stmts = /* @__PURE__ */ new Map();
|
|
247
|
+
this.stmtCache.set(db, stmts);
|
|
248
|
+
}
|
|
249
|
+
let stmt = stmts.get(key);
|
|
250
|
+
if (!stmt) {
|
|
251
|
+
stmt = db.prepare(sql);
|
|
252
|
+
stmts.set(key, stmt);
|
|
253
|
+
}
|
|
254
|
+
return stmt;
|
|
255
|
+
}
|
|
256
|
+
buildPkRef(pkColumns, ref) {
|
|
257
|
+
if (pkColumns.length === 0) {
|
|
258
|
+
return `${ref}.rowid`;
|
|
259
|
+
}
|
|
260
|
+
if (pkColumns.length === 1) {
|
|
261
|
+
return `${ref}."${this.escId(pkColumns[0])}"`;
|
|
262
|
+
}
|
|
263
|
+
return pkColumns.map((col) => `${ref}."${this.escId(col)}"`).join(" || '-' || ");
|
|
264
|
+
}
|
|
265
|
+
installTriggers(db, table, columns, pkColumns) {
|
|
266
|
+
const newJson = this.buildJsonObject(columns, "NEW");
|
|
267
|
+
const oldJson = this.buildJsonObject(columns, "OLD");
|
|
268
|
+
const newPk = this.buildPkRef(pkColumns, "NEW");
|
|
269
|
+
const oldPk = this.buildPkRef(pkColumns, "OLD");
|
|
270
|
+
db.exec(`
|
|
271
|
+
CREATE TRIGGER IF NOT EXISTS "_sirannon_trg_${table}_insert"
|
|
272
|
+
AFTER INSERT ON "${table}"
|
|
273
|
+
BEGIN
|
|
274
|
+
INSERT INTO "${this.changesTable}" (table_name, operation, row_id, new_data)
|
|
275
|
+
VALUES ('${table}', 'INSERT', ${newPk}, ${newJson});
|
|
276
|
+
END
|
|
277
|
+
`);
|
|
278
|
+
db.exec(`
|
|
279
|
+
CREATE TRIGGER IF NOT EXISTS "_sirannon_trg_${table}_update"
|
|
280
|
+
AFTER UPDATE ON "${table}"
|
|
281
|
+
BEGIN
|
|
282
|
+
INSERT INTO "${this.changesTable}" (table_name, operation, row_id, old_data, new_data)
|
|
283
|
+
VALUES ('${table}', 'UPDATE', ${newPk}, ${oldJson}, ${newJson});
|
|
284
|
+
END
|
|
285
|
+
`);
|
|
286
|
+
db.exec(`
|
|
287
|
+
CREATE TRIGGER IF NOT EXISTS "_sirannon_trg_${table}_delete"
|
|
288
|
+
AFTER DELETE ON "${table}"
|
|
289
|
+
BEGIN
|
|
290
|
+
INSERT INTO "${this.changesTable}" (table_name, operation, row_id, old_data)
|
|
291
|
+
VALUES ('${table}', 'DELETE', ${oldPk}, ${oldJson});
|
|
292
|
+
END
|
|
293
|
+
`);
|
|
294
|
+
}
|
|
295
|
+
buildJsonObject(columns, ref) {
|
|
296
|
+
const pairs = columns.map((col) => `'${this.escStr(col)}', ${ref}."${this.escId(col)}"`).join(", ");
|
|
297
|
+
return `json_object(${pairs})`;
|
|
298
|
+
}
|
|
299
|
+
escId(name) {
|
|
300
|
+
return name.replace(/"/g, '""');
|
|
301
|
+
}
|
|
302
|
+
escStr(name) {
|
|
303
|
+
return name.replace(/'/g, "''");
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// src/core/cdc/subscription.ts
|
|
308
|
+
var SubscriptionManager = class {
|
|
309
|
+
nextId = 1;
|
|
310
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
311
|
+
byTable = /* @__PURE__ */ new Map();
|
|
312
|
+
subscribe(table, filter, callback) {
|
|
313
|
+
const id = this.nextId++;
|
|
314
|
+
this.subscriptions.set(id, { id, table, filter, callback });
|
|
315
|
+
let tableSet = this.byTable.get(table);
|
|
316
|
+
if (!tableSet) {
|
|
317
|
+
tableSet = /* @__PURE__ */ new Set();
|
|
318
|
+
this.byTable.set(table, tableSet);
|
|
319
|
+
}
|
|
320
|
+
tableSet.add(id);
|
|
321
|
+
return {
|
|
322
|
+
unsubscribe: () => {
|
|
323
|
+
this.subscriptions.delete(id);
|
|
324
|
+
const set = this.byTable.get(table);
|
|
325
|
+
if (set) {
|
|
326
|
+
set.delete(id);
|
|
327
|
+
if (set.size === 0) {
|
|
328
|
+
this.byTable.delete(table);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
dispatch(events) {
|
|
335
|
+
for (const event of events) {
|
|
336
|
+
const ids = this.byTable.get(event.table);
|
|
337
|
+
if (!ids) continue;
|
|
338
|
+
for (const id of ids) {
|
|
339
|
+
const sub = this.subscriptions.get(id);
|
|
340
|
+
if (!sub) continue;
|
|
341
|
+
if (sub.filter && !matchesFilter(event, sub.filter)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
sub.callback(event);
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
get size() {
|
|
352
|
+
return this.subscriptions.size;
|
|
353
|
+
}
|
|
354
|
+
subscriberCount(table) {
|
|
355
|
+
return this.byTable.get(table)?.size ?? 0;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
var SubscriptionBuilderImpl = class {
|
|
359
|
+
constructor(table, manager) {
|
|
360
|
+
this.table = table;
|
|
361
|
+
this.manager = manager;
|
|
362
|
+
}
|
|
363
|
+
conditions;
|
|
364
|
+
filter(conditions) {
|
|
365
|
+
this.conditions = { ...this.conditions, ...conditions };
|
|
366
|
+
return this;
|
|
367
|
+
}
|
|
368
|
+
subscribe(callback) {
|
|
369
|
+
return this.manager.subscribe(this.table, this.conditions, callback);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
function startPolling(db, tracker, manager, intervalMs, onError) {
|
|
373
|
+
let consecutiveErrors = 0;
|
|
374
|
+
let tickCount = 0;
|
|
375
|
+
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
376
|
+
const CLEANUP_INTERVAL_TICKS = 100;
|
|
377
|
+
const tick = () => {
|
|
378
|
+
if (manager.size === 0) return;
|
|
379
|
+
try {
|
|
380
|
+
const events = tracker.poll(db);
|
|
381
|
+
if (events.length > 0) {
|
|
382
|
+
manager.dispatch(events);
|
|
383
|
+
}
|
|
384
|
+
consecutiveErrors = 0;
|
|
385
|
+
tickCount++;
|
|
386
|
+
if (tickCount >= CLEANUP_INTERVAL_TICKS) {
|
|
387
|
+
tickCount = 0;
|
|
388
|
+
tracker.cleanup(db);
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
consecutiveErrors++;
|
|
392
|
+
if (onError) {
|
|
393
|
+
onError(err instanceof Error ? err : new Error(String(err)));
|
|
394
|
+
}
|
|
395
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
396
|
+
stop();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
const interval = setInterval(tick, intervalMs);
|
|
401
|
+
interval.unref();
|
|
402
|
+
const stop = () => {
|
|
403
|
+
clearInterval(interval);
|
|
404
|
+
};
|
|
405
|
+
return stop;
|
|
406
|
+
}
|
|
407
|
+
function matchesFilter(event, filter) {
|
|
408
|
+
const target = event.type === "delete" ? event.oldRow ?? {} : event.row;
|
|
409
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
410
|
+
if (target[key] !== value) {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export { BackupError, CDCError, ChangeTracker, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, SirannonError, SubscriptionBuilderImpl, SubscriptionManager, TransactionError, startPolling };
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { P as Params, d as ChangeEvent, f as ClientOptions } from '../types-DArCObcu.js';
|
|
2
|
+
import { c as QueryResponse, b as ExecuteResponse, d as TransactionResponse } from '../protocol-BX1H-_Mz.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Transport layer for communicating with a sirannon-db server.
|
|
6
|
+
* Each transport instance is bound to a specific database.
|
|
7
|
+
*/
|
|
8
|
+
interface Transport {
|
|
9
|
+
query(sql: string, params?: Params): Promise<QueryResponse>;
|
|
10
|
+
execute(sql: string, params?: Params): Promise<ExecuteResponse>;
|
|
11
|
+
transaction(statements: Array<{
|
|
12
|
+
sql: string;
|
|
13
|
+
params?: Params;
|
|
14
|
+
}>): Promise<TransactionResponse>;
|
|
15
|
+
subscribe(table: string, filter: Record<string, unknown> | undefined, callback: (event: ChangeEvent) => void): Promise<RemoteSubscription>;
|
|
16
|
+
close(): void;
|
|
17
|
+
}
|
|
18
|
+
/** Handle for an active remote subscription. */
|
|
19
|
+
interface RemoteSubscription {
|
|
20
|
+
unsubscribe(): void;
|
|
21
|
+
}
|
|
22
|
+
/** Builder for creating remote CDC subscriptions with optional filters. */
|
|
23
|
+
interface RemoteSubscriptionBuilder {
|
|
24
|
+
filter(conditions: Record<string, unknown>): RemoteSubscriptionBuilder;
|
|
25
|
+
subscribe(callback: (event: ChangeEvent) => void): Promise<RemoteSubscription>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Error originating from a remote sirannon-db server.
|
|
29
|
+
* Carries the machine-readable error code from the server's error response.
|
|
30
|
+
*/
|
|
31
|
+
declare class RemoteError extends Error {
|
|
32
|
+
readonly code: string;
|
|
33
|
+
constructor(code: string, message: string);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Proxy for a remote sirannon-db database. Mirrors the core
|
|
38
|
+
* {@link Database} query interface with async methods that send
|
|
39
|
+
* requests to the server via the configured transport.
|
|
40
|
+
*/
|
|
41
|
+
declare class RemoteDatabase {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
private readonly transport;
|
|
44
|
+
private readonly onDispose?;
|
|
45
|
+
constructor(id: string, transport: Transport, onDispose?: (() => void) | undefined);
|
|
46
|
+
/**
|
|
47
|
+
* Execute a SELECT and return all matching rows.
|
|
48
|
+
*
|
|
49
|
+
* ```ts
|
|
50
|
+
* const users = await db.query<{ id: number; name: string }>(
|
|
51
|
+
* 'SELECT * FROM users WHERE age > ?',
|
|
52
|
+
* [21],
|
|
53
|
+
* )
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
query<T = Record<string, unknown>>(sql: string, params?: Params): Promise<T[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Execute a mutation (INSERT, UPDATE, DELETE) and return
|
|
59
|
+
* the number of affected rows and last insert row ID.
|
|
60
|
+
*/
|
|
61
|
+
execute(sql: string, params?: Params): Promise<ExecuteResponse>;
|
|
62
|
+
/**
|
|
63
|
+
* Execute multiple statements as a single atomic transaction.
|
|
64
|
+
* Returns an array of results, one per statement.
|
|
65
|
+
*
|
|
66
|
+
* Requires HTTP transport. WebSocket transport does not
|
|
67
|
+
* support server-side transactions.
|
|
68
|
+
*/
|
|
69
|
+
transaction(statements: Array<{
|
|
70
|
+
sql: string;
|
|
71
|
+
params?: Params;
|
|
72
|
+
}>): Promise<ExecuteResponse[]>;
|
|
73
|
+
/**
|
|
74
|
+
* Start building a CDC subscription for the given table.
|
|
75
|
+
* Chain `.filter()` to narrow the events, then call `.subscribe()`
|
|
76
|
+
* with a callback to begin receiving real-time change events.
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* const sub = await db
|
|
80
|
+
* .on('orders')
|
|
81
|
+
* .filter({ status: 'pending' })
|
|
82
|
+
* .subscribe(event => console.log(event))
|
|
83
|
+
*
|
|
84
|
+
* // Later:
|
|
85
|
+
* sub.unsubscribe()
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
on(table: string): RemoteSubscriptionBuilder;
|
|
89
|
+
/**
|
|
90
|
+
* Close the transport for this database. After calling `close()`,
|
|
91
|
+
* all pending requests are rejected and new calls will throw.
|
|
92
|
+
*/
|
|
93
|
+
close(): void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Client for a remote sirannon-db server. Creates {@link RemoteDatabase}
|
|
98
|
+
* instances that communicate with the server over HTTP or WebSocket.
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* const client = new SirannonClient('http://localhost:9876', {
|
|
102
|
+
* transport: 'websocket',
|
|
103
|
+
* })
|
|
104
|
+
*
|
|
105
|
+
* const db = client.database('main')
|
|
106
|
+
* const rows = await db.query('SELECT * FROM users')
|
|
107
|
+
*
|
|
108
|
+
* client.close()
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare class SirannonClient {
|
|
112
|
+
private readonly baseUrl;
|
|
113
|
+
private readonly wsBaseUrl;
|
|
114
|
+
private readonly transport;
|
|
115
|
+
private readonly headers;
|
|
116
|
+
private readonly autoReconnect;
|
|
117
|
+
private readonly reconnectInterval;
|
|
118
|
+
private readonly databases;
|
|
119
|
+
private closed;
|
|
120
|
+
constructor(url: string, options?: ClientOptions);
|
|
121
|
+
/**
|
|
122
|
+
* Get a {@link RemoteDatabase} proxy for the given database ID.
|
|
123
|
+
* Returns a cached instance if one already exists for this ID.
|
|
124
|
+
*
|
|
125
|
+
* The underlying transport connection is established lazily on
|
|
126
|
+
* the first operation (query, execute, or subscribe).
|
|
127
|
+
*/
|
|
128
|
+
database(id: string): RemoteDatabase;
|
|
129
|
+
/**
|
|
130
|
+
* Close all database connections and release resources.
|
|
131
|
+
* After calling `close()`, new calls to `database()` will throw.
|
|
132
|
+
*/
|
|
133
|
+
close(): void;
|
|
134
|
+
private createTransport;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Builds a remote CDC subscription with optional row-level filters.
|
|
139
|
+
* Mirrors the core {@link SubscriptionBuilder} interface but returns
|
|
140
|
+
* a promise from `subscribe()` since confirming the subscription
|
|
141
|
+
* requires a server round-trip.
|
|
142
|
+
*/
|
|
143
|
+
declare class RemoteSubscriptionBuilderImpl implements RemoteSubscriptionBuilder {
|
|
144
|
+
private readonly table;
|
|
145
|
+
private readonly transport;
|
|
146
|
+
private conditions;
|
|
147
|
+
constructor(table: string, transport: Transport);
|
|
148
|
+
filter(conditions: Record<string, unknown>): RemoteSubscriptionBuilder;
|
|
149
|
+
subscribe(callback: (event: ChangeEvent) => void): Promise<RemoteSubscription>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* HTTP transport for sirannon-db. Sends requests via `fetch` to the
|
|
154
|
+
* server's REST endpoints. Supports query, execute, and transaction
|
|
155
|
+
* operations. Real-time subscriptions are not available over HTTP;
|
|
156
|
+
* use {@link WebSocketTransport} for CDC subscriptions.
|
|
157
|
+
*/
|
|
158
|
+
declare class HttpTransport implements Transport {
|
|
159
|
+
private readonly baseUrl;
|
|
160
|
+
private readonly headers;
|
|
161
|
+
private closed;
|
|
162
|
+
constructor(baseUrl: string, headers?: Record<string, string>);
|
|
163
|
+
query(sql: string, params?: Params): Promise<QueryResponse>;
|
|
164
|
+
execute(sql: string, params?: Params): Promise<ExecuteResponse>;
|
|
165
|
+
transaction(statements: Array<{
|
|
166
|
+
sql: string;
|
|
167
|
+
params?: Params;
|
|
168
|
+
}>): Promise<TransactionResponse>;
|
|
169
|
+
subscribe(_table: string, _filter: Record<string, unknown> | undefined, _callback: (event: ChangeEvent) => void): Promise<RemoteSubscription>;
|
|
170
|
+
close(): void;
|
|
171
|
+
private post;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* WebSocket transport for sirannon-db. Connects to
|
|
176
|
+
* `ws(s)://host:port/db/{id}` and supports query, execute,
|
|
177
|
+
* and real-time CDC subscriptions over a single persistent connection.
|
|
178
|
+
*
|
|
179
|
+
* Connections are established lazily on first use and will
|
|
180
|
+
* auto-reconnect (with subscription restoration) when
|
|
181
|
+
* `autoReconnect` is enabled.
|
|
182
|
+
*
|
|
183
|
+
* Transactions are not supported over WebSocket; use
|
|
184
|
+
* {@link HttpTransport} for batch transactions.
|
|
185
|
+
*/
|
|
186
|
+
declare class WebSocketTransport implements Transport {
|
|
187
|
+
private ws;
|
|
188
|
+
private readonly url;
|
|
189
|
+
private readonly autoReconnect;
|
|
190
|
+
private readonly reconnectInterval;
|
|
191
|
+
private readonly requestTimeout;
|
|
192
|
+
private pendingRequests;
|
|
193
|
+
private activeSubscriptions;
|
|
194
|
+
private idCounter;
|
|
195
|
+
private closed;
|
|
196
|
+
private connectPromise;
|
|
197
|
+
private reconnectTimer;
|
|
198
|
+
constructor(url: string, options?: {
|
|
199
|
+
autoReconnect?: boolean;
|
|
200
|
+
reconnectInterval?: number;
|
|
201
|
+
requestTimeout?: number;
|
|
202
|
+
});
|
|
203
|
+
query(sql: string, params?: Params): Promise<QueryResponse>;
|
|
204
|
+
execute(sql: string, params?: Params): Promise<ExecuteResponse>;
|
|
205
|
+
transaction(_statements: Array<{
|
|
206
|
+
sql: string;
|
|
207
|
+
params?: Params;
|
|
208
|
+
}>): Promise<TransactionResponse>;
|
|
209
|
+
subscribe(table: string, filter: Record<string, unknown> | undefined, callback: (event: ChangeEvent) => void): Promise<RemoteSubscription>;
|
|
210
|
+
close(): void;
|
|
211
|
+
private nextId;
|
|
212
|
+
private ensureConnected;
|
|
213
|
+
private connect;
|
|
214
|
+
private handleMessage;
|
|
215
|
+
private handleDisconnect;
|
|
216
|
+
private scheduleReconnect;
|
|
217
|
+
private resubscribeAll;
|
|
218
|
+
private request;
|
|
219
|
+
private rejectAllPending;
|
|
220
|
+
private cancelReconnect;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export { HttpTransport, RemoteDatabase, RemoteError, type RemoteSubscription, type RemoteSubscriptionBuilder, RemoteSubscriptionBuilderImpl, SirannonClient, type Transport, WebSocketTransport };
|