@delali/sirannon-db 0.1.1 → 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.
@@ -1,7 +1,108 @@
1
- export { E as ErrorResponse, a as ExecuteRequest, b as ExecuteResponse, Q as QueryRequest, c as QueryResponse, T as TransactionRequest, d as TransactionResponse, e as TransactionStatement, W as WSChangeMessage, f as WSClientMessage, g as WSErrorMessage, h as WSExecuteMessage, i as WSQueryMessage, j as WSResultMessage, k as WSServerMessage, l as WSSubscribeMessage, m as WSSubscribedMessage, n as WSUnsubscribeMessage, o as WSUnsubscribedMessage, t as toExecuteResponse } from '../protocol-BX1H-_Mz.js';
2
- import { S as Sirannon } from '../sirannon-BJ8Yd1Uf.js';
3
- import { S as ServerOptions, W as WSHandlerOptions } from '../types-DArCObcu.js';
4
- import 'better-sqlite3';
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 };
@@ -1,6 +1,6 @@
1
- import { ChangeTracker, SubscriptionManager, SirannonError } from '../chunk-VI4UP4RR.mjs';
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
- const db = sirannon.get(id);
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
- return body.statements.map((stmt) => tx.execute(stmt.sql, stmt.params));
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
- // --- Public API ---
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.get(databaseId);
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.cdcDb.close();
363
+ await ctx.cdcConn.close();
366
364
  } catch {
367
365
  }
368
366
  }
369
367
  this.cdcContexts.clear();
370
368
  }
371
- // --- Private message handlers ---
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.cdcDb, msg.table);
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
- // --- CDC management ---
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 cdcDb = new SqliteDatabase(database.path);
467
- cdcDb.pragma("journal_mode = WAL");
468
- cdcDb.pragma("busy_timeout = 5000");
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(cdcDb);
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") interval.unref();
493
- const ctx = { cdcDb, tracker, manager, stopPolling };
494
- this.cdcContexts.set(databaseId, ctx);
495
- return ctx;
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
- try {
507
- ctx.cdcDb.close();
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
- return new Promise((resolve) => {
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
- resolve();
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
- handler(res, dbId, rawBody);
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
- handler(res, dbId, rawBody);
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
  };