@delali/sirannon-db 0.1.3 → 0.1.4
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 +276 -77
- package/dist/backup-scheduler/index.d.ts +3 -0
- package/dist/backup-scheduler/index.mjs +2 -0
- package/dist/chunk-74UN4DIE.mjs +14 -0
- package/dist/{chunk-VI4UP4RR.mjs → chunk-AX66KWBR.mjs} +74 -139
- package/dist/chunk-FB2U2Q3Y.mjs +21 -0
- package/dist/chunk-O7BHI3CF.mjs +90 -0
- package/dist/chunk-PXKAKK2V.mjs +124 -0
- package/dist/client/index.d.ts +38 -2
- package/dist/core/index.d.ts +30 -142
- package/dist/core/index.mjs +229 -469
- 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/file-migrations/index.d.ts +16 -0
- package/dist/file-migrations/index.mjs +128 -0
- package/dist/index-hXiis3N-.d.ts +16 -0
- package/dist/server/index.d.ts +110 -54
- package/dist/server/index.mjs +107 -92
- package/dist/{sirannon-BJ8Yd1Uf.d.ts → sirannon-B1oTfebD.d.ts} +30 -58
- package/dist/types-BFSsG77t.d.ts +29 -0
- package/dist/types-DRkJlqex.d.ts +38 -0
- package/dist/{types-DArCObcu.d.ts → types-DtDutWRU.d.ts} +4 -1
- package/dist/vfs-INWQ5DTE.mjs +2 -0
- package/package.json +58 -7
- package/dist/protocol-BX1H-_Mz.d.ts +0 -104
package/dist/server/index.d.ts
CHANGED
|
@@ -1,7 +1,108 @@
|
|
|
1
|
-
|
|
2
|
-
import { S as Sirannon } from '../sirannon-
|
|
3
|
-
import
|
|
4
|
-
import '
|
|
1
|
+
import { E as ExecuteResult, S as ServerOptions, W as WSHandlerOptions } from '../types-DtDutWRU.js';
|
|
2
|
+
import { S as Sirannon } from '../sirannon-B1oTfebD.js';
|
|
3
|
+
import '../types-BFSsG77t.js';
|
|
4
|
+
import '../types-DRkJlqex.js';
|
|
5
|
+
|
|
6
|
+
/** Body for POST /db/:id/query */
|
|
7
|
+
interface QueryRequest {
|
|
8
|
+
sql: string;
|
|
9
|
+
params?: Record<string, unknown> | unknown[];
|
|
10
|
+
}
|
|
11
|
+
/** Body for POST /db/:id/execute */
|
|
12
|
+
interface ExecuteRequest {
|
|
13
|
+
sql: string;
|
|
14
|
+
params?: Record<string, unknown> | unknown[];
|
|
15
|
+
}
|
|
16
|
+
/** A single statement within a transaction batch. */
|
|
17
|
+
interface TransactionStatement {
|
|
18
|
+
sql: string;
|
|
19
|
+
params?: Record<string, unknown> | unknown[];
|
|
20
|
+
}
|
|
21
|
+
/** Body for POST /db/:id/transaction */
|
|
22
|
+
interface TransactionRequest {
|
|
23
|
+
statements: TransactionStatement[];
|
|
24
|
+
}
|
|
25
|
+
/** Response for a successful query. */
|
|
26
|
+
interface QueryResponse {
|
|
27
|
+
rows: Record<string, unknown>[];
|
|
28
|
+
}
|
|
29
|
+
/** Response for a successful execute. */
|
|
30
|
+
interface ExecuteResponse {
|
|
31
|
+
changes: number;
|
|
32
|
+
lastInsertRowId: number | string;
|
|
33
|
+
}
|
|
34
|
+
/** Response for a successful transaction. */
|
|
35
|
+
interface TransactionResponse {
|
|
36
|
+
results: ExecuteResponse[];
|
|
37
|
+
}
|
|
38
|
+
/** Standard error response envelope. */
|
|
39
|
+
interface ErrorResponse {
|
|
40
|
+
error: {
|
|
41
|
+
code: string;
|
|
42
|
+
message: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Inbound WS message types. */
|
|
46
|
+
type WSClientMessage = WSSubscribeMessage | WSUnsubscribeMessage | WSQueryMessage | WSExecuteMessage;
|
|
47
|
+
interface WSSubscribeMessage {
|
|
48
|
+
type: 'subscribe';
|
|
49
|
+
id: string;
|
|
50
|
+
table: string;
|
|
51
|
+
filter?: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
interface WSUnsubscribeMessage {
|
|
54
|
+
type: 'unsubscribe';
|
|
55
|
+
id: string;
|
|
56
|
+
}
|
|
57
|
+
interface WSQueryMessage {
|
|
58
|
+
type: 'query';
|
|
59
|
+
id: string;
|
|
60
|
+
sql: string;
|
|
61
|
+
params?: Record<string, unknown> | unknown[];
|
|
62
|
+
}
|
|
63
|
+
interface WSExecuteMessage {
|
|
64
|
+
type: 'execute';
|
|
65
|
+
id: string;
|
|
66
|
+
sql: string;
|
|
67
|
+
params?: Record<string, unknown> | unknown[];
|
|
68
|
+
}
|
|
69
|
+
/** Outbound WS message types. */
|
|
70
|
+
type WSServerMessage = WSSubscribedMessage | WSUnsubscribedMessage | WSChangeMessage | WSResultMessage | WSErrorMessage;
|
|
71
|
+
interface WSSubscribedMessage {
|
|
72
|
+
type: 'subscribed';
|
|
73
|
+
id: string;
|
|
74
|
+
}
|
|
75
|
+
interface WSUnsubscribedMessage {
|
|
76
|
+
type: 'unsubscribed';
|
|
77
|
+
id: string;
|
|
78
|
+
}
|
|
79
|
+
interface WSChangeMessage {
|
|
80
|
+
type: 'change';
|
|
81
|
+
id: string;
|
|
82
|
+
event: {
|
|
83
|
+
type: 'insert' | 'update' | 'delete';
|
|
84
|
+
table: string;
|
|
85
|
+
row: Record<string, unknown>;
|
|
86
|
+
oldRow?: Record<string, unknown>;
|
|
87
|
+
seq: string;
|
|
88
|
+
timestamp: number;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
interface WSResultMessage {
|
|
92
|
+
type: 'result';
|
|
93
|
+
id: string;
|
|
94
|
+
data: QueryResponse | ExecuteResponse;
|
|
95
|
+
}
|
|
96
|
+
interface WSErrorMessage {
|
|
97
|
+
type: 'error';
|
|
98
|
+
id: string;
|
|
99
|
+
error: {
|
|
100
|
+
code: string;
|
|
101
|
+
message: string;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Convert an ExecuteResult (with possible bigint) to a JSON-safe response. */
|
|
105
|
+
declare function toExecuteResponse(result: ExecuteResult): ExecuteResponse;
|
|
5
106
|
|
|
6
107
|
declare class SirannonServer {
|
|
7
108
|
private app;
|
|
@@ -23,73 +124,29 @@ declare class SirannonServer {
|
|
|
23
124
|
}
|
|
24
125
|
declare function createServer(sirannon: Sirannon, options?: ServerOptions): SirannonServer;
|
|
25
126
|
|
|
26
|
-
/** Transport-agnostic WebSocket connection. */
|
|
27
127
|
interface WSConnection {
|
|
28
128
|
send(data: string): void;
|
|
29
129
|
close(code?: number, reason?: string): void;
|
|
30
130
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Manages WebSocket connections, routes messages to databases, and integrates
|
|
33
|
-
* with CDC for real-time change subscriptions.
|
|
34
|
-
*
|
|
35
|
-
* Designed to be transport-agnostic: any WebSocket implementation can drive
|
|
36
|
-
* this handler by calling handleOpen/handleMessage/handleClose with a
|
|
37
|
-
* WSConnection adapter.
|
|
38
|
-
*/
|
|
39
131
|
declare class WSHandler {
|
|
40
132
|
private readonly sirannon;
|
|
41
133
|
private readonly maxPayloadLength;
|
|
42
134
|
private readonly connections;
|
|
43
135
|
private readonly cdcContexts;
|
|
136
|
+
private readonly cdcPending;
|
|
44
137
|
private closed;
|
|
45
138
|
constructor(sirannon: Sirannon, options?: WSHandlerOptions);
|
|
46
|
-
|
|
47
|
-
* Register a new WebSocket connection for the given database.
|
|
48
|
-
*
|
|
49
|
-
* Sends an error and closes the connection if the database does not exist,
|
|
50
|
-
* is closed, or the handler itself has been shut down.
|
|
51
|
-
*/
|
|
52
|
-
handleOpen(conn: WSConnection, databaseId: string): void;
|
|
53
|
-
/**
|
|
54
|
-
* Process an incoming WebSocket message.
|
|
55
|
-
*
|
|
56
|
-
* Validates the JSON payload, extracts the message type and correlation ID,
|
|
57
|
-
* and routes to the appropriate handler.
|
|
58
|
-
*/
|
|
139
|
+
handleOpen(conn: WSConnection, databaseId: string): Promise<void>;
|
|
59
140
|
handleMessage(conn: WSConnection, data: string): void;
|
|
60
|
-
/**
|
|
61
|
-
* Clean up after a WebSocket connection closes.
|
|
62
|
-
*
|
|
63
|
-
* Unsubscribes all active CDC subscriptions for this connection and
|
|
64
|
-
* releases the per-database CDC context if no subscribers remain.
|
|
65
|
-
*/
|
|
66
141
|
handleClose(conn: WSConnection): void;
|
|
67
|
-
/** Number of active WebSocket connections. */
|
|
68
142
|
get connectionCount(): number;
|
|
69
|
-
|
|
70
|
-
* Shut down the handler.
|
|
71
|
-
*
|
|
72
|
-
* Unsubscribes all CDC subscriptions, closes all WebSocket connections
|
|
73
|
-
* with code 1001 (Going Away), and releases all CDC resources.
|
|
74
|
-
*/
|
|
75
|
-
close(): void;
|
|
143
|
+
close(): Promise<void>;
|
|
76
144
|
private handleQuery;
|
|
77
145
|
private handleExecute;
|
|
78
146
|
private handleSubscribe;
|
|
79
147
|
private handleUnsubscribe;
|
|
80
|
-
/**
|
|
81
|
-
* Get or create a CDC context for the given database.
|
|
82
|
-
*
|
|
83
|
-
* Opens a separate better-sqlite3 connection for trigger installation and
|
|
84
|
-
* change polling. WAL mode and a 5-second busy timeout are set so that
|
|
85
|
-
* the CDC connection coexists with the Database class's connection pool.
|
|
86
|
-
*/
|
|
87
148
|
private ensureCDC;
|
|
88
|
-
|
|
89
|
-
* Release CDC resources for a database if no subscribers remain.
|
|
90
|
-
*
|
|
91
|
-
* Stops the polling loop and closes the dedicated CDC connection.
|
|
92
|
-
*/
|
|
149
|
+
private createCDCContext;
|
|
93
150
|
private maybeCleanupCDC;
|
|
94
151
|
private isValidParams;
|
|
95
152
|
private send;
|
|
@@ -97,7 +154,6 @@ declare class WSHandler {
|
|
|
97
154
|
private sendSirannonError;
|
|
98
155
|
private sendChange;
|
|
99
156
|
}
|
|
100
|
-
/** Create a transport-agnostic WebSocket handler bound to a Sirannon registry. */
|
|
101
157
|
declare function createWSHandler(sirannon: Sirannon, options?: WSHandlerOptions): WSHandler;
|
|
102
158
|
|
|
103
|
-
export { SirannonServer, type WSConnection, WSHandler, createServer, createWSHandler };
|
|
159
|
+
export { type ErrorResponse, type ExecuteRequest, type ExecuteResponse, type QueryRequest, type QueryResponse, SirannonServer, type TransactionRequest, type TransactionResponse, type TransactionStatement, type WSChangeMessage, type WSClientMessage, type WSConnection, type WSErrorMessage, type WSExecuteMessage, WSHandler, type WSQueryMessage, type WSResultMessage, type WSServerMessage, type WSSubscribeMessage, type WSSubscribedMessage, type WSUnsubscribeMessage, type WSUnsubscribedMessage, createServer, createWSHandler, toExecuteResponse };
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ChangeTracker, SubscriptionManager
|
|
1
|
+
import { ChangeTracker, SubscriptionManager } from '../chunk-AX66KWBR.mjs';
|
|
2
|
+
import { SirannonError } from '../chunk-O7BHI3CF.mjs';
|
|
2
3
|
import uWS from 'uWebSockets.js';
|
|
3
|
-
import SqliteDatabase from 'better-sqlite3';
|
|
4
4
|
|
|
5
5
|
// src/server/protocol.ts
|
|
6
6
|
function toExecuteResponse(result) {
|
|
@@ -141,8 +141,18 @@ function httpStatusForError(err) {
|
|
|
141
141
|
return 500;
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
-
function resolveDatabase(res, sirannon, id) {
|
|
145
|
-
|
|
144
|
+
async function resolveDatabase(res, sirannon, id) {
|
|
145
|
+
let db;
|
|
146
|
+
try {
|
|
147
|
+
db = await sirannon.resolve(id);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err instanceof SirannonError) {
|
|
150
|
+
sendError(res, httpStatusForError(err), err.code, err.message);
|
|
151
|
+
} else {
|
|
152
|
+
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
146
156
|
if (!db) {
|
|
147
157
|
sendError(res, 404, "DATABASE_NOT_FOUND", `Database '${id}' not found`);
|
|
148
158
|
return null;
|
|
@@ -150,19 +160,21 @@ function resolveDatabase(res, sirannon, id) {
|
|
|
150
160
|
return db;
|
|
151
161
|
}
|
|
152
162
|
function handleQuery(sirannon) {
|
|
153
|
-
return (res, dbId, rawBody) => {
|
|
163
|
+
return async (res, dbId, rawBody, abort) => {
|
|
154
164
|
const body = parseBody(res, rawBody);
|
|
155
165
|
if (!body) return;
|
|
156
166
|
if (!body.sql || typeof body.sql !== "string") {
|
|
157
167
|
sendError(res, 400, "INVALID_REQUEST", 'Field "sql" is required and must be a string');
|
|
158
168
|
return;
|
|
159
169
|
}
|
|
160
|
-
const db = resolveDatabase(res, sirannon, dbId);
|
|
170
|
+
const db = await resolveDatabase(res, sirannon, dbId);
|
|
161
171
|
if (!db) return;
|
|
162
172
|
try {
|
|
163
|
-
const rows = db.query(body.sql, body.params);
|
|
173
|
+
const rows = await db.query(body.sql, body.params);
|
|
174
|
+
if (abort.aborted) return;
|
|
164
175
|
sendJson(res, { rows });
|
|
165
176
|
} catch (err) {
|
|
177
|
+
if (abort.aborted) return;
|
|
166
178
|
if (err instanceof SirannonError) {
|
|
167
179
|
sendError(res, httpStatusForError(err), err.code, err.message);
|
|
168
180
|
} else {
|
|
@@ -172,19 +184,21 @@ function handleQuery(sirannon) {
|
|
|
172
184
|
};
|
|
173
185
|
}
|
|
174
186
|
function handleExecute(sirannon) {
|
|
175
|
-
return (res, dbId, rawBody) => {
|
|
187
|
+
return async (res, dbId, rawBody, abort) => {
|
|
176
188
|
const body = parseBody(res, rawBody);
|
|
177
189
|
if (!body) return;
|
|
178
190
|
if (!body.sql || typeof body.sql !== "string") {
|
|
179
191
|
sendError(res, 400, "INVALID_REQUEST", 'Field "sql" is required and must be a string');
|
|
180
192
|
return;
|
|
181
193
|
}
|
|
182
|
-
const db = resolveDatabase(res, sirannon, dbId);
|
|
194
|
+
const db = await resolveDatabase(res, sirannon, dbId);
|
|
183
195
|
if (!db) return;
|
|
184
196
|
try {
|
|
185
|
-
const result = db.execute(body.sql, body.params);
|
|
197
|
+
const result = await db.execute(body.sql, body.params);
|
|
198
|
+
if (abort.aborted) return;
|
|
186
199
|
sendJson(res, toExecuteResponse(result));
|
|
187
200
|
} catch (err) {
|
|
201
|
+
if (abort.aborted) return;
|
|
188
202
|
if (err instanceof SirannonError) {
|
|
189
203
|
sendError(res, httpStatusForError(err), err.code, err.message);
|
|
190
204
|
} else {
|
|
@@ -194,7 +208,7 @@ function handleExecute(sirannon) {
|
|
|
194
208
|
};
|
|
195
209
|
}
|
|
196
210
|
function handleTransaction(sirannon) {
|
|
197
|
-
return (res, dbId, rawBody) => {
|
|
211
|
+
return async (res, dbId, rawBody, abort) => {
|
|
198
212
|
const body = parseBody(res, rawBody);
|
|
199
213
|
if (!body) return;
|
|
200
214
|
if (!Array.isArray(body.statements)) {
|
|
@@ -212,16 +226,23 @@ function handleTransaction(sirannon) {
|
|
|
212
226
|
return;
|
|
213
227
|
}
|
|
214
228
|
}
|
|
215
|
-
const db = resolveDatabase(res, sirannon, dbId);
|
|
229
|
+
const db = await resolveDatabase(res, sirannon, dbId);
|
|
216
230
|
if (!db) return;
|
|
217
231
|
try {
|
|
218
|
-
const results = db.transaction((tx) => {
|
|
219
|
-
|
|
232
|
+
const results = await db.transaction(async (tx) => {
|
|
233
|
+
const txResults = [];
|
|
234
|
+
for (const stmt of body.statements) {
|
|
235
|
+
if (abort.aborted) throw new Error("Request aborted");
|
|
236
|
+
txResults.push(await tx.execute(stmt.sql, stmt.params));
|
|
237
|
+
}
|
|
238
|
+
return txResults;
|
|
220
239
|
});
|
|
240
|
+
if (abort.aborted) return;
|
|
221
241
|
sendJson(res, {
|
|
222
242
|
results: results.map(toExecuteResponse)
|
|
223
243
|
});
|
|
224
244
|
} catch (err) {
|
|
245
|
+
if (abort.aborted) return;
|
|
225
246
|
if (err instanceof SirannonError) {
|
|
226
247
|
sendError(res, httpStatusForError(err), err.code, err.message);
|
|
227
248
|
} else {
|
|
@@ -230,6 +251,8 @@ function handleTransaction(sirannon) {
|
|
|
230
251
|
}
|
|
231
252
|
};
|
|
232
253
|
}
|
|
254
|
+
|
|
255
|
+
// src/server/ws-handler.ts
|
|
233
256
|
var DEFAULT_POLL_INTERVAL_MS = 50;
|
|
234
257
|
var DEFAULT_MAX_PAYLOAD_LENGTH = 1048576;
|
|
235
258
|
var WSHandler = class {
|
|
@@ -237,25 +260,19 @@ var WSHandler = class {
|
|
|
237
260
|
maxPayloadLength;
|
|
238
261
|
connections = /* @__PURE__ */ new Map();
|
|
239
262
|
cdcContexts = /* @__PURE__ */ new Map();
|
|
263
|
+
cdcPending = /* @__PURE__ */ new Map();
|
|
240
264
|
closed = false;
|
|
241
265
|
constructor(sirannon, options) {
|
|
242
266
|
this.sirannon = sirannon;
|
|
243
267
|
this.maxPayloadLength = options?.maxPayloadLength ?? DEFAULT_MAX_PAYLOAD_LENGTH;
|
|
244
268
|
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Register a new WebSocket connection for the given database.
|
|
248
|
-
*
|
|
249
|
-
* Sends an error and closes the connection if the database does not exist,
|
|
250
|
-
* is closed, or the handler itself has been shut down.
|
|
251
|
-
*/
|
|
252
|
-
handleOpen(conn, databaseId) {
|
|
269
|
+
async handleOpen(conn, databaseId) {
|
|
253
270
|
if (this.closed) {
|
|
254
271
|
this.sendError(conn, "", "HANDLER_CLOSED", "WebSocket handler is shut down");
|
|
255
272
|
conn.close(1013, "Handler shutting down");
|
|
256
273
|
return;
|
|
257
274
|
}
|
|
258
|
-
const database = this.sirannon.
|
|
275
|
+
const database = await this.sirannon.resolve(databaseId);
|
|
259
276
|
if (!database) {
|
|
260
277
|
this.sendError(conn, "", "DATABASE_NOT_FOUND", `Database '${databaseId}' not found`);
|
|
261
278
|
conn.close(1008, "Database not found");
|
|
@@ -272,12 +289,6 @@ var WSHandler = class {
|
|
|
272
289
|
subscriptions: /* @__PURE__ */ new Map()
|
|
273
290
|
});
|
|
274
291
|
}
|
|
275
|
-
/**
|
|
276
|
-
* Process an incoming WebSocket message.
|
|
277
|
-
*
|
|
278
|
-
* Validates the JSON payload, extracts the message type and correlation ID,
|
|
279
|
-
* and routes to the appropriate handler.
|
|
280
|
-
*/
|
|
281
292
|
handleMessage(conn, data) {
|
|
282
293
|
const state = this.connections.get(conn);
|
|
283
294
|
if (!state) return;
|
|
@@ -322,12 +333,6 @@ var WSHandler = class {
|
|
|
322
333
|
this.sendError(conn, id, "UNKNOWN_TYPE", `Unknown message type: '${msg.type}'`);
|
|
323
334
|
}
|
|
324
335
|
}
|
|
325
|
-
/**
|
|
326
|
-
* Clean up after a WebSocket connection closes.
|
|
327
|
-
*
|
|
328
|
-
* Unsubscribes all active CDC subscriptions for this connection and
|
|
329
|
-
* releases the per-database CDC context if no subscribers remain.
|
|
330
|
-
*/
|
|
331
336
|
handleClose(conn) {
|
|
332
337
|
const state = this.connections.get(conn);
|
|
333
338
|
if (!state) return;
|
|
@@ -338,17 +343,10 @@ var WSHandler = class {
|
|
|
338
343
|
this.maybeCleanupCDC(state.databaseId);
|
|
339
344
|
this.connections.delete(conn);
|
|
340
345
|
}
|
|
341
|
-
/** Number of active WebSocket connections. */
|
|
342
346
|
get connectionCount() {
|
|
343
347
|
return this.connections.size;
|
|
344
348
|
}
|
|
345
|
-
|
|
346
|
-
* Shut down the handler.
|
|
347
|
-
*
|
|
348
|
-
* Unsubscribes all CDC subscriptions, closes all WebSocket connections
|
|
349
|
-
* with code 1001 (Going Away), and releases all CDC resources.
|
|
350
|
-
*/
|
|
351
|
-
close() {
|
|
349
|
+
async close() {
|
|
352
350
|
if (this.closed) return;
|
|
353
351
|
this.closed = true;
|
|
354
352
|
for (const [conn, state] of this.connections) {
|
|
@@ -362,14 +360,13 @@ var WSHandler = class {
|
|
|
362
360
|
for (const ctx of this.cdcContexts.values()) {
|
|
363
361
|
ctx.stopPolling();
|
|
364
362
|
try {
|
|
365
|
-
ctx.
|
|
363
|
+
await ctx.cdcConn.close();
|
|
366
364
|
} catch {
|
|
367
365
|
}
|
|
368
366
|
}
|
|
369
367
|
this.cdcContexts.clear();
|
|
370
368
|
}
|
|
371
|
-
|
|
372
|
-
handleQuery(conn, state, msg, id) {
|
|
369
|
+
async handleQuery(conn, state, msg, id) {
|
|
373
370
|
if (typeof msg.sql !== "string") {
|
|
374
371
|
this.sendError(conn, id, "INVALID_MESSAGE", 'Query message requires a "sql" string field');
|
|
375
372
|
return;
|
|
@@ -380,13 +377,13 @@ var WSHandler = class {
|
|
|
380
377
|
}
|
|
381
378
|
try {
|
|
382
379
|
const params = msg.params ?? void 0;
|
|
383
|
-
const rows = state.database.query(msg.sql, params);
|
|
380
|
+
const rows = await state.database.query(msg.sql, params);
|
|
384
381
|
this.send(conn, { type: "result", id, data: { rows } });
|
|
385
382
|
} catch (err) {
|
|
386
383
|
this.sendSirannonError(conn, id, err);
|
|
387
384
|
}
|
|
388
385
|
}
|
|
389
|
-
handleExecute(conn, state, msg, id) {
|
|
386
|
+
async handleExecute(conn, state, msg, id) {
|
|
390
387
|
if (typeof msg.sql !== "string") {
|
|
391
388
|
this.sendError(conn, id, "INVALID_MESSAGE", 'Execute message requires a "sql" string field');
|
|
392
389
|
return;
|
|
@@ -397,7 +394,7 @@ var WSHandler = class {
|
|
|
397
394
|
}
|
|
398
395
|
try {
|
|
399
396
|
const params = msg.params ?? void 0;
|
|
400
|
-
const result = state.database.execute(msg.sql, params);
|
|
397
|
+
const result = await state.database.execute(msg.sql, params);
|
|
401
398
|
this.send(conn, {
|
|
402
399
|
type: "result",
|
|
403
400
|
id,
|
|
@@ -407,7 +404,7 @@ var WSHandler = class {
|
|
|
407
404
|
this.sendSirannonError(conn, id, err);
|
|
408
405
|
}
|
|
409
406
|
}
|
|
410
|
-
handleSubscribe(conn, state, msg, id) {
|
|
407
|
+
async handleSubscribe(conn, state, msg, id) {
|
|
411
408
|
if (typeof msg.table !== "string") {
|
|
412
409
|
this.sendError(conn, id, "INVALID_MESSAGE", 'Subscribe message requires a "table" string field');
|
|
413
410
|
return;
|
|
@@ -430,8 +427,8 @@ var WSHandler = class {
|
|
|
430
427
|
}
|
|
431
428
|
const filter = msg.filter ?? void 0;
|
|
432
429
|
try {
|
|
433
|
-
const ctx = this.ensureCDC(state.databaseId, state.database);
|
|
434
|
-
ctx.tracker.watch(ctx.
|
|
430
|
+
const ctx = await this.ensureCDC(state.databaseId, state.database);
|
|
431
|
+
await ctx.tracker.watch(ctx.cdcConn, msg.table);
|
|
435
432
|
const sub = ctx.manager.subscribe(msg.table, filter, (event) => {
|
|
436
433
|
this.sendChange(conn, id, event);
|
|
437
434
|
});
|
|
@@ -452,31 +449,43 @@ var WSHandler = class {
|
|
|
452
449
|
this.send(conn, { type: "unsubscribed", id });
|
|
453
450
|
this.maybeCleanupCDC(state.databaseId);
|
|
454
451
|
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Get or create a CDC context for the given database.
|
|
458
|
-
*
|
|
459
|
-
* Opens a separate better-sqlite3 connection for trigger installation and
|
|
460
|
-
* change polling. WAL mode and a 5-second busy timeout are set so that
|
|
461
|
-
* the CDC connection coexists with the Database class's connection pool.
|
|
462
|
-
*/
|
|
463
|
-
ensureCDC(databaseId, database) {
|
|
452
|
+
async ensureCDC(databaseId, database) {
|
|
464
453
|
const existing = this.cdcContexts.get(databaseId);
|
|
465
454
|
if (existing) return existing;
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
455
|
+
const pending = this.cdcPending.get(databaseId);
|
|
456
|
+
if (pending) return pending;
|
|
457
|
+
const promise = this.createCDCContext(database);
|
|
458
|
+
this.cdcPending.set(databaseId, promise);
|
|
459
|
+
try {
|
|
460
|
+
const ctx = await promise;
|
|
461
|
+
if (this.closed) {
|
|
462
|
+
ctx.stopPolling();
|
|
463
|
+
await ctx.cdcConn.close().catch(() => {
|
|
464
|
+
});
|
|
465
|
+
throw new SirannonError("WebSocket handler is shut down", "HANDLER_CLOSED");
|
|
466
|
+
}
|
|
467
|
+
this.cdcContexts.set(databaseId, ctx);
|
|
468
|
+
return ctx;
|
|
469
|
+
} finally {
|
|
470
|
+
this.cdcPending.delete(databaseId);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async createCDCContext(database) {
|
|
474
|
+
const cdcConn = await this.sirannon.driver.open(database.path, { walMode: true });
|
|
469
475
|
const tracker = new ChangeTracker();
|
|
470
476
|
const manager = new SubscriptionManager();
|
|
477
|
+
let polling = false;
|
|
471
478
|
let consecutiveErrors = 0;
|
|
472
479
|
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
473
480
|
const stopPolling = () => {
|
|
474
481
|
clearInterval(interval);
|
|
475
482
|
};
|
|
476
|
-
const tick = () => {
|
|
483
|
+
const tick = async () => {
|
|
477
484
|
if (manager.size === 0) return;
|
|
485
|
+
if (polling) return;
|
|
486
|
+
polling = true;
|
|
478
487
|
try {
|
|
479
|
-
const events = tracker.poll(
|
|
488
|
+
const events = await tracker.poll(cdcConn);
|
|
480
489
|
if (events.length > 0) {
|
|
481
490
|
manager.dispatch(events);
|
|
482
491
|
}
|
|
@@ -486,35 +495,28 @@ var WSHandler = class {
|
|
|
486
495
|
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
487
496
|
stopPolling();
|
|
488
497
|
}
|
|
498
|
+
} finally {
|
|
499
|
+
polling = false;
|
|
489
500
|
}
|
|
490
501
|
};
|
|
491
502
|
const interval = setInterval(tick, DEFAULT_POLL_INTERVAL_MS);
|
|
492
|
-
if (typeof interval.unref === "function")
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
return
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Release CDC resources for a database if no subscribers remain.
|
|
499
|
-
*
|
|
500
|
-
* Stops the polling loop and closes the dedicated CDC connection.
|
|
501
|
-
*/
|
|
503
|
+
if (typeof interval === "object" && "unref" in interval && typeof interval.unref === "function") {
|
|
504
|
+
interval.unref();
|
|
505
|
+
}
|
|
506
|
+
return { cdcConn, tracker, manager, stopPolling };
|
|
507
|
+
}
|
|
502
508
|
maybeCleanupCDC(databaseId) {
|
|
503
509
|
const ctx = this.cdcContexts.get(databaseId);
|
|
504
510
|
if (!ctx || ctx.manager.size > 0) return;
|
|
505
511
|
ctx.stopPolling();
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
} catch {
|
|
509
|
-
}
|
|
512
|
+
ctx.cdcConn.close().catch(() => {
|
|
513
|
+
});
|
|
510
514
|
this.cdcContexts.delete(databaseId);
|
|
511
515
|
}
|
|
512
|
-
// --- Validation ---
|
|
513
516
|
isValidParams(params) {
|
|
514
517
|
if (params === void 0 || params === null) return true;
|
|
515
518
|
return typeof params === "object";
|
|
516
519
|
}
|
|
517
|
-
// --- Send helpers ---
|
|
518
520
|
send(conn, msg) {
|
|
519
521
|
try {
|
|
520
522
|
conn.send(JSON.stringify(msg));
|
|
@@ -628,15 +630,15 @@ var SirannonServer = class {
|
|
|
628
630
|
});
|
|
629
631
|
});
|
|
630
632
|
}
|
|
631
|
-
close() {
|
|
632
|
-
|
|
633
|
-
this.wsHandler.close();
|
|
633
|
+
async close() {
|
|
634
|
+
try {
|
|
635
|
+
await this.wsHandler.close();
|
|
636
|
+
} finally {
|
|
634
637
|
if (this.listenSocket) {
|
|
635
638
|
uWS.us_listen_socket_close(this.listenSocket);
|
|
636
639
|
this.listenSocket = null;
|
|
637
640
|
}
|
|
638
|
-
|
|
639
|
-
});
|
|
641
|
+
}
|
|
640
642
|
}
|
|
641
643
|
get listeningPort() {
|
|
642
644
|
if (!this.listenSocket) return -1;
|
|
@@ -735,7 +737,8 @@ var SirannonServer = class {
|
|
|
735
737
|
}
|
|
736
738
|
};
|
|
737
739
|
userData.conn = conn;
|
|
738
|
-
wsHandler.handleOpen(conn, userData.databaseId)
|
|
740
|
+
wsHandler.handleOpen(conn, userData.databaseId).catch(() => {
|
|
741
|
+
});
|
|
739
742
|
},
|
|
740
743
|
message: (ws, message) => {
|
|
741
744
|
const userData = ws.getUserData();
|
|
@@ -773,9 +776,15 @@ var SirannonServer = class {
|
|
|
773
776
|
const abort = initAbortHandler(res);
|
|
774
777
|
const bodyPromise = readBody(res, MAX_BODY, abort);
|
|
775
778
|
if (!onRequestHook) {
|
|
776
|
-
bodyPromise.then((rawBody) => {
|
|
779
|
+
bodyPromise.then(async (rawBody) => {
|
|
777
780
|
if (abort.aborted) return;
|
|
778
|
-
|
|
781
|
+
try {
|
|
782
|
+
await handler(res, dbId, rawBody, abort);
|
|
783
|
+
} catch {
|
|
784
|
+
if (!abort.aborted) {
|
|
785
|
+
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
779
788
|
}).catch(() => {
|
|
780
789
|
});
|
|
781
790
|
return;
|
|
@@ -793,9 +802,15 @@ var SirannonServer = class {
|
|
|
793
802
|
remoteAddress
|
|
794
803
|
};
|
|
795
804
|
const hookPromise = runOnRequest(res, ctx, onRequestHook);
|
|
796
|
-
Promise.all([bodyPromise, hookPromise]).then(([rawBody, allowed]) => {
|
|
805
|
+
Promise.all([bodyPromise, hookPromise]).then(async ([rawBody, allowed]) => {
|
|
797
806
|
if (abort.aborted || !allowed) return;
|
|
798
|
-
|
|
807
|
+
try {
|
|
808
|
+
await handler(res, dbId, rawBody, abort);
|
|
809
|
+
} catch {
|
|
810
|
+
if (!abort.aborted) {
|
|
811
|
+
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
812
|
+
}
|
|
813
|
+
}
|
|
799
814
|
}).catch(() => {
|
|
800
815
|
});
|
|
801
816
|
};
|