@delali/sirannon-db 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +655 -80
- package/dist/backup-scheduler/index.d.ts +3 -0
- package/dist/backup-scheduler/index.mjs +2 -0
- package/dist/change-tracker-CFTQ9TSn.d.ts +89 -0
- package/dist/chunk-3MCMONVP.mjs +115 -0
- package/dist/chunk-74UN4DIE.mjs +14 -0
- package/dist/chunk-ER7ODTDA.mjs +23 -0
- package/dist/chunk-FB2U2Q3Y.mjs +21 -0
- package/dist/chunk-GS7T5YMI.mjs +51 -0
- package/dist/chunk-O7BHI3CF.mjs +90 -0
- package/dist/chunk-PXKAKK2V.mjs +124 -0
- package/dist/chunk-UTO3ZAFS.mjs +514 -0
- package/dist/chunk-UVMVN3OT.mjs +111 -0
- package/dist/client/index.d.ts +137 -44
- package/dist/client/index.mjs +726 -26
- package/dist/core/index.d.ts +32 -241
- package/dist/core/index.mjs +294 -568
- package/dist/database-BVY1GqE7.d.ts +95 -0
- package/dist/driver/better-sqlite3.d.ts +8 -0
- package/dist/driver/better-sqlite3.mjs +63 -0
- package/dist/driver/bun.mjs +61 -0
- package/dist/driver/expo.mjs +55 -0
- package/dist/driver/node.d.ts +8 -0
- package/dist/driver/node.mjs +60 -0
- package/dist/driver/wa-sqlite.d.ts +34 -0
- package/dist/driver/wa-sqlite.mjs +141 -0
- package/dist/errors-C00ed08Q.d.ts +101 -0
- package/dist/file-migrations/index.d.ts +16 -0
- package/dist/file-migrations/index.mjs +128 -0
- package/dist/index-CLdNrcPz.d.ts +16 -0
- package/dist/replication/coordinator/etcd.d.ts +44 -0
- package/dist/replication/coordinator/etcd.mjs +650 -0
- package/dist/replication/index.d.ts +491 -0
- package/dist/replication/index.mjs +3784 -0
- package/dist/server/index.d.ts +121 -54
- package/dist/server/index.mjs +347 -114
- package/dist/sirannon-Cd-lK6T0.d.ts +31 -0
- package/dist/transport/grpc.d.ts +316 -0
- package/dist/transport/grpc.mjs +3341 -0
- package/dist/transport/memory.d.ts +221 -0
- package/dist/transport/memory.mjs +337 -0
- package/dist/types-B2byqt0B.d.ts +273 -0
- package/dist/types-BEu1I_9_.d.ts +139 -0
- package/dist/types-BFSsG77t.d.ts +29 -0
- package/dist/types-BeozgNPr.d.ts +26 -0
- package/dist/{types-DArCObcu.d.ts → types-D-74JiXb.d.ts} +80 -1
- package/dist/vfs-INWQ5DTE.mjs +2 -0
- package/package.json +106 -11
- package/dist/chunk-VI4UP4RR.mjs +0 -417
- package/dist/protocol-BX1H-_Mz.d.ts +0 -104
- package/dist/sirannon-BJ8Yd1Uf.d.ts +0 -148
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delali/sirannon-db",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5",
|
|
5
5
|
"description": "A production-grade library that turns SQLite databases into a networked data layer with real-time subscriptions.",
|
|
6
6
|
"author": "Delali (https://sondelali.com)",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -22,13 +22,42 @@
|
|
|
22
22
|
"subscriptions",
|
|
23
23
|
"websocket",
|
|
24
24
|
"embedded",
|
|
25
|
-
"better-sqlite3"
|
|
25
|
+
"better-sqlite3",
|
|
26
|
+
"wa-sqlite",
|
|
27
|
+
"expo-sqlite",
|
|
28
|
+
"pluggable"
|
|
26
29
|
],
|
|
27
30
|
"exports": {
|
|
28
31
|
".": {
|
|
29
32
|
"types": "./dist/core/index.d.ts",
|
|
30
33
|
"import": "./dist/core/index.mjs"
|
|
31
34
|
},
|
|
35
|
+
"./driver/better-sqlite3": {
|
|
36
|
+
"types": "./dist/driver/better-sqlite3.d.ts",
|
|
37
|
+
"import": "./dist/driver/better-sqlite3.mjs"
|
|
38
|
+
},
|
|
39
|
+
"./driver/node": {
|
|
40
|
+
"types": "./dist/driver/node.d.ts",
|
|
41
|
+
"import": "./dist/driver/node.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./driver/bun": {
|
|
44
|
+
"import": "./dist/driver/bun.mjs"
|
|
45
|
+
},
|
|
46
|
+
"./driver/wa-sqlite": {
|
|
47
|
+
"types": "./dist/driver/wa-sqlite.d.ts",
|
|
48
|
+
"import": "./dist/driver/wa-sqlite.mjs"
|
|
49
|
+
},
|
|
50
|
+
"./driver/expo": {
|
|
51
|
+
"import": "./dist/driver/expo.mjs"
|
|
52
|
+
},
|
|
53
|
+
"./file-migrations": {
|
|
54
|
+
"types": "./dist/file-migrations/index.d.ts",
|
|
55
|
+
"import": "./dist/file-migrations/index.mjs"
|
|
56
|
+
},
|
|
57
|
+
"./backup-scheduler": {
|
|
58
|
+
"types": "./dist/backup-scheduler/index.d.ts",
|
|
59
|
+
"import": "./dist/backup-scheduler/index.mjs"
|
|
60
|
+
},
|
|
32
61
|
"./server": {
|
|
33
62
|
"types": "./dist/server/index.d.ts",
|
|
34
63
|
"import": "./dist/server/index.mjs"
|
|
@@ -36,6 +65,22 @@
|
|
|
36
65
|
"./client": {
|
|
37
66
|
"types": "./dist/client/index.d.ts",
|
|
38
67
|
"import": "./dist/client/index.mjs"
|
|
68
|
+
},
|
|
69
|
+
"./replication": {
|
|
70
|
+
"types": "./dist/replication/index.d.ts",
|
|
71
|
+
"import": "./dist/replication/index.mjs"
|
|
72
|
+
},
|
|
73
|
+
"./replication/coordinator/etcd": {
|
|
74
|
+
"types": "./dist/replication/coordinator/etcd.d.ts",
|
|
75
|
+
"import": "./dist/replication/coordinator/etcd.mjs"
|
|
76
|
+
},
|
|
77
|
+
"./transport/memory": {
|
|
78
|
+
"types": "./dist/transport/memory.d.ts",
|
|
79
|
+
"import": "./dist/transport/memory.mjs"
|
|
80
|
+
},
|
|
81
|
+
"./transport/grpc": {
|
|
82
|
+
"types": "./dist/transport/grpc.d.ts",
|
|
83
|
+
"import": "./dist/transport/grpc.mjs"
|
|
39
84
|
}
|
|
40
85
|
},
|
|
41
86
|
"files": [
|
|
@@ -46,29 +91,77 @@
|
|
|
46
91
|
"engines": {
|
|
47
92
|
"node": ">=22"
|
|
48
93
|
},
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
94
|
+
"peerDependencies": {
|
|
95
|
+
"@bufbuild/protobuf": ">=2.0.0",
|
|
96
|
+
"@grpc/grpc-js": ">=1.10.0",
|
|
97
|
+
"better-sqlite3": ">=12.0.0",
|
|
98
|
+
"croner": ">=10.0.0",
|
|
99
|
+
"expo-sqlite": ">=14.0.0",
|
|
100
|
+
"etcd3": ">=1.1.2",
|
|
101
|
+
"grpc-health-check": ">=2.0.0",
|
|
102
|
+
"uWebSockets.js": ">=20.0.0",
|
|
103
|
+
"wa-sqlite": ">=1.0.0"
|
|
104
|
+
},
|
|
105
|
+
"peerDependenciesMeta": {
|
|
106
|
+
"@bufbuild/protobuf": {
|
|
107
|
+
"optional": true
|
|
108
|
+
},
|
|
109
|
+
"@grpc/grpc-js": {
|
|
110
|
+
"optional": true
|
|
111
|
+
},
|
|
112
|
+
"better-sqlite3": {
|
|
113
|
+
"optional": true
|
|
114
|
+
},
|
|
115
|
+
"croner": {
|
|
116
|
+
"optional": true
|
|
117
|
+
},
|
|
118
|
+
"expo-sqlite": {
|
|
119
|
+
"optional": true
|
|
120
|
+
},
|
|
121
|
+
"etcd3": {
|
|
122
|
+
"optional": true
|
|
123
|
+
},
|
|
124
|
+
"grpc-health-check": {
|
|
125
|
+
"optional": true
|
|
126
|
+
},
|
|
127
|
+
"uWebSockets.js": {
|
|
128
|
+
"optional": true
|
|
129
|
+
},
|
|
130
|
+
"wa-sqlite": {
|
|
131
|
+
"optional": true
|
|
132
|
+
}
|
|
53
133
|
},
|
|
54
134
|
"devDependencies": {
|
|
135
|
+
"@bufbuild/protobuf": "2.11.0",
|
|
136
|
+
"@grpc/grpc-js": "1.14.3",
|
|
55
137
|
"@types/better-sqlite3": "7.6.13",
|
|
56
138
|
"@types/node": "25.3.3",
|
|
57
|
-
"@types/pg": "8.
|
|
139
|
+
"@types/pg": "8.20.0",
|
|
58
140
|
"@vitest/coverage-v8": "4.0.18",
|
|
59
|
-
"
|
|
141
|
+
"better-sqlite3": "12.6.2",
|
|
142
|
+
"croner": "10.0.1",
|
|
143
|
+
"etcd3": "1.1.2",
|
|
144
|
+
"grpc-health-check": "2.1.0",
|
|
145
|
+
"grpc-tools": "1.13.1",
|
|
146
|
+
"pg": "8.20.0",
|
|
147
|
+
"selfsigned": "5.5.0",
|
|
60
148
|
"simple-statistics": "7.8.7",
|
|
61
149
|
"tinybench": "6.0.0",
|
|
150
|
+
"ts-proto": "2.11.6",
|
|
62
151
|
"tsup": "8.5.1",
|
|
63
152
|
"tsx": "4.21.0",
|
|
64
153
|
"typescript": "5.9.3",
|
|
154
|
+
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.58.0",
|
|
65
155
|
"vitest": "4.0.18"
|
|
66
156
|
},
|
|
67
157
|
"scripts": {
|
|
68
|
-
"build": "tsup",
|
|
158
|
+
"build": "rm -rf dist && tsup",
|
|
69
159
|
"test": "vitest run",
|
|
70
160
|
"test:coverage": "vitest run --coverage",
|
|
71
|
-
"
|
|
161
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
162
|
+
"test:failover": "vitest run --config vitest.failover.config.ts",
|
|
163
|
+
"test:soak": "vitest run --config vitest.soak.config.ts",
|
|
164
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
72
165
|
"lint": "biome check .",
|
|
73
166
|
"lint:fix": "biome check --write .",
|
|
74
167
|
"format": "biome format --write .",
|
|
@@ -77,11 +170,13 @@
|
|
|
77
170
|
"bench:ycsb": "node --expose-gc --import tsx benchmarks/ycsb/workload-a.ts",
|
|
78
171
|
"bench:oltp": "node --expose-gc --import tsx benchmarks/oltp/tpc-c-lite.ts",
|
|
79
172
|
"bench:scaling": "node --expose-gc --import tsx benchmarks/scaling/concurrency.ts",
|
|
173
|
+
"bench:pool": "node --expose-gc --import tsx benchmarks/scaling/pool-sweep.ts",
|
|
80
174
|
"bench:cdc": "node --expose-gc --import tsx benchmarks/sirannon/cdc-latency.ts",
|
|
81
175
|
"bench:docker": "node --import tsx benchmarks/run-docker.ts",
|
|
82
176
|
"bench:docker:e2e": "node --import tsx benchmarks/run-e2e.ts",
|
|
83
177
|
"bench:docker:engine": "node --expose-gc --import tsx benchmarks/run-engine.ts",
|
|
84
178
|
"bench:statistical": "node --expose-gc --import tsx benchmarks/run-statistical.ts",
|
|
85
|
-
"bench:charts": "python3 benchmarks/scripts/generate-charts.py benchmarks/results/"
|
|
179
|
+
"bench:charts": "python3 benchmarks/scripts/generate-charts.py benchmarks/results/",
|
|
180
|
+
"proto:gen": "grpc_tools_node_protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=src/transport/grpc/generated --ts_proto_opt=forceLong=bigint --ts_proto_opt=esModuleInterop=true --ts_proto_opt=outputServices=grpc-js --ts_proto_opt=env=node -I src/transport/grpc/proto src/transport/grpc/proto/replication.proto"
|
|
86
181
|
}
|
|
87
182
|
}
|
package/dist/chunk-VI4UP4RR.mjs
DELETED
|
@@ -1,417 +0,0 @@
|
|
|
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 };
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { E as ExecuteResult } from './types-DArCObcu.js';
|
|
2
|
-
|
|
3
|
-
/** Body for POST /db/:id/query */
|
|
4
|
-
interface QueryRequest {
|
|
5
|
-
sql: string;
|
|
6
|
-
params?: Record<string, unknown> | unknown[];
|
|
7
|
-
}
|
|
8
|
-
/** Body for POST /db/:id/execute */
|
|
9
|
-
interface ExecuteRequest {
|
|
10
|
-
sql: string;
|
|
11
|
-
params?: Record<string, unknown> | unknown[];
|
|
12
|
-
}
|
|
13
|
-
/** A single statement within a transaction batch. */
|
|
14
|
-
interface TransactionStatement {
|
|
15
|
-
sql: string;
|
|
16
|
-
params?: Record<string, unknown> | unknown[];
|
|
17
|
-
}
|
|
18
|
-
/** Body for POST /db/:id/transaction */
|
|
19
|
-
interface TransactionRequest {
|
|
20
|
-
statements: TransactionStatement[];
|
|
21
|
-
}
|
|
22
|
-
/** Response for a successful query. */
|
|
23
|
-
interface QueryResponse {
|
|
24
|
-
rows: Record<string, unknown>[];
|
|
25
|
-
}
|
|
26
|
-
/** Response for a successful execute. */
|
|
27
|
-
interface ExecuteResponse {
|
|
28
|
-
changes: number;
|
|
29
|
-
lastInsertRowId: number | string;
|
|
30
|
-
}
|
|
31
|
-
/** Response for a successful transaction. */
|
|
32
|
-
interface TransactionResponse {
|
|
33
|
-
results: ExecuteResponse[];
|
|
34
|
-
}
|
|
35
|
-
/** Standard error response envelope. */
|
|
36
|
-
interface ErrorResponse {
|
|
37
|
-
error: {
|
|
38
|
-
code: string;
|
|
39
|
-
message: string;
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
/** Inbound WS message types. */
|
|
43
|
-
type WSClientMessage = WSSubscribeMessage | WSUnsubscribeMessage | WSQueryMessage | WSExecuteMessage;
|
|
44
|
-
interface WSSubscribeMessage {
|
|
45
|
-
type: 'subscribe';
|
|
46
|
-
id: string;
|
|
47
|
-
table: string;
|
|
48
|
-
filter?: Record<string, unknown>;
|
|
49
|
-
}
|
|
50
|
-
interface WSUnsubscribeMessage {
|
|
51
|
-
type: 'unsubscribe';
|
|
52
|
-
id: string;
|
|
53
|
-
}
|
|
54
|
-
interface WSQueryMessage {
|
|
55
|
-
type: 'query';
|
|
56
|
-
id: string;
|
|
57
|
-
sql: string;
|
|
58
|
-
params?: Record<string, unknown> | unknown[];
|
|
59
|
-
}
|
|
60
|
-
interface WSExecuteMessage {
|
|
61
|
-
type: 'execute';
|
|
62
|
-
id: string;
|
|
63
|
-
sql: string;
|
|
64
|
-
params?: Record<string, unknown> | unknown[];
|
|
65
|
-
}
|
|
66
|
-
/** Outbound WS message types. */
|
|
67
|
-
type WSServerMessage = WSSubscribedMessage | WSUnsubscribedMessage | WSChangeMessage | WSResultMessage | WSErrorMessage;
|
|
68
|
-
interface WSSubscribedMessage {
|
|
69
|
-
type: 'subscribed';
|
|
70
|
-
id: string;
|
|
71
|
-
}
|
|
72
|
-
interface WSUnsubscribedMessage {
|
|
73
|
-
type: 'unsubscribed';
|
|
74
|
-
id: string;
|
|
75
|
-
}
|
|
76
|
-
interface WSChangeMessage {
|
|
77
|
-
type: 'change';
|
|
78
|
-
id: string;
|
|
79
|
-
event: {
|
|
80
|
-
type: 'insert' | 'update' | 'delete';
|
|
81
|
-
table: string;
|
|
82
|
-
row: Record<string, unknown>;
|
|
83
|
-
oldRow?: Record<string, unknown>;
|
|
84
|
-
seq: string;
|
|
85
|
-
timestamp: number;
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
interface WSResultMessage {
|
|
89
|
-
type: 'result';
|
|
90
|
-
id: string;
|
|
91
|
-
data: QueryResponse | ExecuteResponse;
|
|
92
|
-
}
|
|
93
|
-
interface WSErrorMessage {
|
|
94
|
-
type: 'error';
|
|
95
|
-
id: string;
|
|
96
|
-
error: {
|
|
97
|
-
code: string;
|
|
98
|
-
message: string;
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
/** Convert an ExecuteResult (with possible bigint) to a JSON-safe response. */
|
|
102
|
-
declare function toExecuteResponse(result: ExecuteResult): ExecuteResponse;
|
|
103
|
-
|
|
104
|
-
export { type ErrorResponse as E, type QueryRequest as Q, type TransactionRequest as T, type WSChangeMessage as W, type ExecuteRequest as a, type ExecuteResponse as b, type QueryResponse as c, type TransactionResponse as d, type TransactionStatement as e, type WSClientMessage as f, type WSErrorMessage as g, type WSExecuteMessage as h, type WSQueryMessage as i, type WSResultMessage as j, type WSServerMessage as k, type WSSubscribeMessage as l, type WSSubscribedMessage as m, type WSUnsubscribeMessage as n, type WSUnsubscribedMessage as o, toExecuteResponse as t };
|