@delali/sirannon-db 0.1.4 → 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.
@@ -1,17 +1,20 @@
1
- import { E as ExecuteResult, S as ServerOptions, W as WSHandlerOptions } from '../types-DtDutWRU.js';
2
- import { S as Sirannon } from '../sirannon-B1oTfebD.js';
1
+ import { W as WriteConcern, R as ReadConcern, E as ExecuteResult, d as ServerOptions, e as WSHandlerOptions } from '../types-D-74JiXb.js';
2
+ import { S as Sirannon } from '../sirannon-Cd-lK6T0.js';
3
3
  import '../types-BFSsG77t.js';
4
- import '../types-DRkJlqex.js';
4
+ import '../database-BVY1GqE7.js';
5
+ import '../types-BeozgNPr.js';
5
6
 
6
7
  /** Body for POST /db/:id/query */
7
8
  interface QueryRequest {
8
9
  sql: string;
9
10
  params?: Record<string, unknown> | unknown[];
11
+ readConcern?: ReadConcern;
10
12
  }
11
13
  /** Body for POST /db/:id/execute */
12
14
  interface ExecuteRequest {
13
15
  sql: string;
14
16
  params?: Record<string, unknown> | unknown[];
17
+ writeConcern?: WriteConcern;
15
18
  }
16
19
  /** A single statement within a transaction batch. */
17
20
  interface TransactionStatement {
@@ -21,6 +24,7 @@ interface TransactionStatement {
21
24
  /** Body for POST /db/:id/transaction */
22
25
  interface TransactionRequest {
23
26
  statements: TransactionStatement[];
27
+ writeConcern?: WriteConcern;
24
28
  }
25
29
  /** Response for a successful query. */
26
30
  interface QueryResponse {
@@ -40,6 +44,7 @@ interface ErrorResponse {
40
44
  error: {
41
45
  code: string;
42
46
  message: string;
47
+ details?: Record<string, unknown>;
43
48
  };
44
49
  }
45
50
  /** Inbound WS message types. */
@@ -111,6 +116,9 @@ declare class SirannonServer {
111
116
  private readonly port;
112
117
  private readonly cors;
113
118
  private readonly onRequestHook;
119
+ private readonly resolveExecutionTarget;
120
+ private readonly getReplicationStatus;
121
+ private readonly getClusterStatus;
114
122
  private readonly sirannon;
115
123
  private readonly wsHandler;
116
124
  constructor(sirannon: Sirannon, options?: ServerOptions);
@@ -121,6 +129,7 @@ declare class SirannonServer {
121
129
  private registerWebSocketRoute;
122
130
  private withCors;
123
131
  private wrapDbRoute;
132
+ private wrapDbGetRoute;
124
133
  }
125
134
  declare function createServer(sirannon: Sirannon, options?: ServerOptions): SirannonServer;
126
135
 
@@ -131,6 +140,7 @@ interface WSConnection {
131
140
  declare class WSHandler {
132
141
  private readonly sirannon;
133
142
  private readonly maxPayloadLength;
143
+ private readonly resolveExecutionTarget;
134
144
  private readonly connections;
135
145
  private readonly cdcContexts;
136
146
  private readonly cdcPending;
@@ -149,6 +159,7 @@ declare class WSHandler {
149
159
  private createCDCContext;
150
160
  private maybeCleanupCDC;
151
161
  private isValidParams;
162
+ private resolveTarget;
152
163
  private send;
153
164
  private sendError;
154
165
  private sendSirannonError;
@@ -1,4 +1,5 @@
1
- import { ChangeTracker, SubscriptionManager } from '../chunk-AX66KWBR.mjs';
1
+ import { ChangeTracker, SubscriptionManager } from '../chunk-UTO3ZAFS.mjs';
2
+ import '../chunk-GS7T5YMI.mjs';
2
3
  import { SirannonError } from '../chunk-O7BHI3CF.mjs';
3
4
  import uWS from 'uWebSockets.js';
4
5
 
@@ -19,7 +20,7 @@ function handleLiveness() {
19
20
  });
20
21
  };
21
22
  }
22
- function handleReadiness(sirannon) {
23
+ function handleReadiness(sirannon, getReplicationStatus) {
23
24
  return (res) => {
24
25
  const dbs = sirannon.databases();
25
26
  const databases = [];
@@ -37,12 +38,43 @@ function handleReadiness(sirannon) {
37
38
  status: degraded ? "degraded" : "ok",
38
39
  databases
39
40
  };
41
+ if (getReplicationStatus) {
42
+ const replStatus = getReplicationStatus();
43
+ if (replStatus) {
44
+ body.replication = {
45
+ role: replStatus.role,
46
+ writeForwarding: replStatus.writeForwarding,
47
+ peers: replStatus.peers,
48
+ localSeq: replStatus.localSeq.toString(),
49
+ replicationGroupId: replStatus.replicationGroupId,
50
+ primaryTerm: replStatus.primaryTerm?.toString(),
51
+ currentPrimary: replStatus.currentPrimary,
52
+ coordinator: replStatus.coordinator,
53
+ controller: replStatus.controller,
54
+ inSyncReplicas: replStatus.inSyncReplicas,
55
+ laggingReplicas: replStatus.laggingReplicas,
56
+ syncState: replStatus.syncState,
57
+ readAvailability: replStatus.readAvailability,
58
+ writeAvailability: replStatus.writeAvailability
59
+ };
60
+ body.status = readinessStatusForReplication(replStatus, body.status);
61
+ }
62
+ }
40
63
  const payload = JSON.stringify(body);
41
64
  res.cork(() => {
42
65
  res.writeStatus("200 OK").writeHeader("Content-Type", "application/json").end(payload);
43
66
  });
44
67
  };
45
68
  }
69
+ function readinessStatusForReplication(replication, current) {
70
+ if (replication.syncState === "syncing" || replication.syncState === "catching-up") return "syncing";
71
+ if (replication.controller?.state === "active" && replication.writeAvailability === "unavailable")
72
+ return "failing_over";
73
+ if (replication.readAvailability === "unavailable" && replication.writeAvailability === "unavailable")
74
+ return "unavailable";
75
+ if ((replication.laggingReplicas?.length ?? 0) > 0) return "degraded";
76
+ return current;
77
+ }
46
78
 
47
79
  // src/server/http-handler.ts
48
80
  function initAbortHandler(res) {
@@ -116,8 +148,8 @@ function sendJson(res, data) {
116
148
  res.writeStatus("200 OK").writeHeader("Content-Type", "application/json").end(payload);
117
149
  });
118
150
  }
119
- function sendError(res, status, code, message) {
120
- const body = { error: { code, message } };
151
+ function sendError(res, status, code, message, details) {
152
+ const body = { error: details ? { code, message, details } : { code, message } };
121
153
  const payload = JSON.stringify(body);
122
154
  res.cork(() => {
123
155
  res.writeStatus(`${status}`).writeHeader("Content-Type", "application/json").end(payload);
@@ -132,19 +164,29 @@ function httpStatusForError(err) {
132
164
  case "QUERY_ERROR":
133
165
  case "TRANSACTION_ERROR":
134
166
  return 400;
167
+ case "STALE_PRIMARY":
168
+ case "PROTOCOL_VERSION_MISMATCH":
169
+ return 409;
135
170
  case "HOOK_DENIED":
136
171
  return 403;
137
172
  case "DATABASE_CLOSED":
138
173
  case "SHUTDOWN":
174
+ case "READ_CONCERN_ERROR":
175
+ case "COORDINATOR_UNAVAILABLE":
176
+ case "AUTHORITY_LOST":
177
+ case "NO_SAFE_PRIMARY":
178
+ case "NODE_NOT_IN_SYNC":
179
+ case "NODE_DRAINING":
180
+ case "UNSAFE_RECOVERY_REQUIRED":
139
181
  return 503;
140
182
  default:
141
183
  return 500;
142
184
  }
143
185
  }
144
- async function resolveDatabase(res, sirannon, id) {
145
- let db;
186
+ async function resolveExecutionTarget(res, sirannon, id, resolver) {
187
+ let target;
146
188
  try {
147
- db = await sirannon.resolve(id);
189
+ target = resolver ? await resolver(id) : await sirannon.resolve(id);
148
190
  } catch (err) {
149
191
  if (err instanceof SirannonError) {
150
192
  sendError(res, httpStatusForError(err), err.code, err.message);
@@ -153,13 +195,13 @@ async function resolveDatabase(res, sirannon, id) {
153
195
  }
154
196
  return null;
155
197
  }
156
- if (!db) {
198
+ if (!target) {
157
199
  sendError(res, 404, "DATABASE_NOT_FOUND", `Database '${id}' not found`);
158
200
  return null;
159
201
  }
160
- return db;
202
+ return target;
161
203
  }
162
- function handleQuery(sirannon) {
204
+ function handleQuery(sirannon, resolveTarget) {
163
205
  return async (res, dbId, rawBody, abort) => {
164
206
  const body = parseBody(res, rawBody);
165
207
  if (!body) return;
@@ -167,23 +209,29 @@ function handleQuery(sirannon) {
167
209
  sendError(res, 400, "INVALID_REQUEST", 'Field "sql" is required and must be a string');
168
210
  return;
169
211
  }
170
- const db = await resolveDatabase(res, sirannon, dbId);
171
- if (!db) return;
212
+ const readConcern = parseReadConcern(res, body.readConcern);
213
+ if (!readConcern.ok) return;
214
+ const target = await resolveExecutionTarget(res, sirannon, dbId, resolveTarget);
215
+ if (!target) return;
172
216
  try {
173
- const rows = await db.query(body.sql, body.params);
217
+ const rows = await target.query(
218
+ body.sql,
219
+ body.params,
220
+ readConcern.value ? { readConcern: readConcern.value } : void 0
221
+ );
174
222
  if (abort.aborted) return;
175
223
  sendJson(res, { rows });
176
224
  } catch (err) {
177
225
  if (abort.aborted) return;
178
226
  if (err instanceof SirannonError) {
179
- sendError(res, httpStatusForError(err), err.code, err.message);
227
+ sendError(res, httpStatusForError(err), err.code, err.message, errorDetails(err));
180
228
  } else {
181
229
  sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
182
230
  }
183
231
  }
184
232
  };
185
233
  }
186
- function handleExecute(sirannon) {
234
+ function handleExecute(sirannon, resolveTarget) {
187
235
  return async (res, dbId, rawBody, abort) => {
188
236
  const body = parseBody(res, rawBody);
189
237
  if (!body) return;
@@ -191,23 +239,29 @@ function handleExecute(sirannon) {
191
239
  sendError(res, 400, "INVALID_REQUEST", 'Field "sql" is required and must be a string');
192
240
  return;
193
241
  }
194
- const db = await resolveDatabase(res, sirannon, dbId);
195
- if (!db) return;
242
+ const writeConcern = parseWriteConcern(res, body.writeConcern);
243
+ if (!writeConcern.ok) return;
244
+ const target = await resolveExecutionTarget(res, sirannon, dbId, resolveTarget);
245
+ if (!target) return;
196
246
  try {
197
- const result = await db.execute(body.sql, body.params);
247
+ const result = await target.execute(
248
+ body.sql,
249
+ body.params,
250
+ writeConcern.value ? { writeConcern: writeConcern.value } : void 0
251
+ );
198
252
  if (abort.aborted) return;
199
253
  sendJson(res, toExecuteResponse(result));
200
254
  } catch (err) {
201
255
  if (abort.aborted) return;
202
256
  if (err instanceof SirannonError) {
203
- sendError(res, httpStatusForError(err), err.code, err.message);
257
+ sendError(res, httpStatusForError(err), err.code, err.message, errorDetails(err));
204
258
  } else {
205
259
  sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
206
260
  }
207
261
  }
208
262
  };
209
263
  }
210
- function handleTransaction(sirannon) {
264
+ function handleTransaction(sirannon, resolveTarget) {
211
265
  return async (res, dbId, rawBody, abort) => {
212
266
  const body = parseBody(res, rawBody);
213
267
  if (!body) return;
@@ -219,6 +273,8 @@ function handleTransaction(sirannon) {
219
273
  sendError(res, 400, "INVALID_REQUEST", "Transaction requires at least one statement");
220
274
  return;
221
275
  }
276
+ const writeConcern = parseWriteConcern(res, body.writeConcern);
277
+ if (!writeConcern.ok) return;
222
278
  for (let i = 0; i < body.statements.length; i++) {
223
279
  const stmt = body.statements[i];
224
280
  if (!stmt.sql || typeof stmt.sql !== "string") {
@@ -226,17 +282,20 @@ function handleTransaction(sirannon) {
226
282
  return;
227
283
  }
228
284
  }
229
- const db = await resolveDatabase(res, sirannon, dbId);
230
- if (!db) return;
285
+ const target = await resolveExecutionTarget(res, sirannon, dbId, resolveTarget);
286
+ if (!target) return;
231
287
  try {
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;
239
- });
288
+ const results = await target.transaction(
289
+ async (tx) => {
290
+ const txResults = [];
291
+ for (const stmt of body.statements) {
292
+ if (abort.aborted) throw new Error("Request aborted");
293
+ txResults.push(await tx.execute(stmt.sql, stmt.params));
294
+ }
295
+ return txResults;
296
+ },
297
+ writeConcern.value ? { writeConcern: writeConcern.value } : void 0
298
+ );
240
299
  if (abort.aborted) return;
241
300
  sendJson(res, {
242
301
  results: results.map(toExecuteResponse)
@@ -244,13 +303,93 @@ function handleTransaction(sirannon) {
244
303
  } catch (err) {
245
304
  if (abort.aborted) return;
246
305
  if (err instanceof SirannonError) {
247
- sendError(res, httpStatusForError(err), err.code, err.message);
306
+ sendError(res, httpStatusForError(err), err.code, err.message, errorDetails(err));
248
307
  } else {
249
308
  sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
250
309
  }
251
310
  }
252
311
  };
253
312
  }
313
+ function parseReadConcern(res, value) {
314
+ if (value === void 0) return { ok: true, value: void 0 };
315
+ if (!isPlainRecord(value)) {
316
+ sendError(res, 400, "INVALID_REQUEST", 'Field "readConcern" must be an object when provided');
317
+ return { ok: false };
318
+ }
319
+ const keys = Object.keys(value);
320
+ if (keys.length !== 1 || !keys.includes("level")) {
321
+ sendError(res, 400, "INVALID_REQUEST", 'Field "readConcern" must contain only "level"');
322
+ return { ok: false };
323
+ }
324
+ if (!isReadConcernLevel(value.level)) {
325
+ sendError(res, 400, "INVALID_REQUEST", 'Field "readConcern.level" is invalid');
326
+ return { ok: false };
327
+ }
328
+ return { ok: true, value: { level: value.level } };
329
+ }
330
+ function parseWriteConcern(res, value) {
331
+ if (value === void 0) return { ok: true, value: void 0 };
332
+ if (!isPlainRecord(value)) {
333
+ sendError(res, 400, "INVALID_REQUEST", 'Field "writeConcern" must be an object when provided');
334
+ return { ok: false };
335
+ }
336
+ const allowedKeys = /* @__PURE__ */ new Set(["level", "timeoutMs"]);
337
+ if (!Object.keys(value).every((key) => allowedKeys.has(key))) {
338
+ sendError(res, 400, "INVALID_REQUEST", 'Field "writeConcern" contains unsupported keys');
339
+ return { ok: false };
340
+ }
341
+ if (!isWriteConcernLevel(value.level)) {
342
+ sendError(res, 400, "INVALID_REQUEST", 'Field "writeConcern.level" is invalid');
343
+ return { ok: false };
344
+ }
345
+ const timeoutMs = value.timeoutMs;
346
+ if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)) {
347
+ sendError(res, 400, "INVALID_REQUEST", 'Field "writeConcern.timeoutMs" must be a positive safe integer');
348
+ return { ok: false };
349
+ }
350
+ return {
351
+ ok: true,
352
+ value: timeoutMs === void 0 ? { level: value.level } : { level: value.level, timeoutMs }
353
+ };
354
+ }
355
+ function isPlainRecord(value) {
356
+ return typeof value === "object" && value !== null && !Array.isArray(value);
357
+ }
358
+ function isReadConcernLevel(value) {
359
+ return value === "local" || value === "majority" || value === "linearizable";
360
+ }
361
+ function isWriteConcernLevel(value) {
362
+ return value === "local" || value === "majority" || value === "all";
363
+ }
364
+ function handleClusterStatus(getClusterStatus) {
365
+ return (res, dbId) => {
366
+ if (!getClusterStatus) {
367
+ sendError(res, 404, "NOT_FOUND", "Cluster status is not configured");
368
+ return;
369
+ }
370
+ const status = getClusterStatus(dbId);
371
+ if (!status) {
372
+ sendError(res, 404, "DATABASE_NOT_FOUND", `Database '${dbId}' not found`);
373
+ return;
374
+ }
375
+ sendJson(res, toClusterStatusResponse(status));
376
+ };
377
+ }
378
+ function toClusterStatusResponse(status) {
379
+ return {
380
+ ...status,
381
+ currentPrimary: status.currentPrimary ? { ...status.currentPrimary } : status.currentPrimary,
382
+ readEndpoints: status.readEndpoints?.map((endpoint) => ({
383
+ ...endpoint,
384
+ readConcerns: [...endpoint.readConcerns]
385
+ })),
386
+ primaryTerm: status.primaryTerm?.toString()
387
+ };
388
+ }
389
+ function errorDetails(err) {
390
+ const details = err.details;
391
+ return details && Object.keys(details).length > 0 ? details : void 0;
392
+ }
254
393
 
255
394
  // src/server/ws-handler.ts
256
395
  var DEFAULT_POLL_INTERVAL_MS = 50;
@@ -258,6 +397,7 @@ var DEFAULT_MAX_PAYLOAD_LENGTH = 1048576;
258
397
  var WSHandler = class {
259
398
  sirannon;
260
399
  maxPayloadLength;
400
+ resolveExecutionTarget;
261
401
  connections = /* @__PURE__ */ new Map();
262
402
  cdcContexts = /* @__PURE__ */ new Map();
263
403
  cdcPending = /* @__PURE__ */ new Map();
@@ -265,6 +405,7 @@ var WSHandler = class {
265
405
  constructor(sirannon, options) {
266
406
  this.sirannon = sirannon;
267
407
  this.maxPayloadLength = options?.maxPayloadLength ?? DEFAULT_MAX_PAYLOAD_LENGTH;
408
+ this.resolveExecutionTarget = options?.resolveExecutionTarget;
268
409
  }
269
410
  async handleOpen(conn, databaseId) {
270
411
  if (this.closed) {
@@ -283,9 +424,23 @@ var WSHandler = class {
283
424
  conn.close(1008, "Database closed");
284
425
  return;
285
426
  }
427
+ let executionTarget;
428
+ try {
429
+ executionTarget = await this.resolveTarget(databaseId);
430
+ } catch (err) {
431
+ this.sendSirannonError(conn, "", err);
432
+ conn.close(1011, "Execution target resolution failed");
433
+ return;
434
+ }
435
+ if (!executionTarget) {
436
+ this.sendError(conn, "", "DATABASE_NOT_FOUND", `Database '${databaseId}' not found`);
437
+ conn.close(1008, "Database not found");
438
+ return;
439
+ }
286
440
  this.connections.set(conn, {
287
441
  databaseId,
288
442
  database,
443
+ executionTarget,
289
444
  subscriptions: /* @__PURE__ */ new Map()
290
445
  });
291
446
  }
@@ -377,7 +532,7 @@ var WSHandler = class {
377
532
  }
378
533
  try {
379
534
  const params = msg.params ?? void 0;
380
- const rows = await state.database.query(msg.sql, params);
535
+ const rows = await state.executionTarget.query(msg.sql, params);
381
536
  this.send(conn, { type: "result", id, data: { rows } });
382
537
  } catch (err) {
383
538
  this.sendSirannonError(conn, id, err);
@@ -394,7 +549,7 @@ var WSHandler = class {
394
549
  }
395
550
  try {
396
551
  const params = msg.params ?? void 0;
397
- const result = await state.database.execute(msg.sql, params);
552
+ const result = await state.executionTarget.execute(msg.sql, params);
398
553
  this.send(conn, {
399
554
  type: "result",
400
555
  id,
@@ -426,8 +581,9 @@ var WSHandler = class {
426
581
  return;
427
582
  }
428
583
  const filter = msg.filter ?? void 0;
584
+ let ctx = null;
429
585
  try {
430
- const ctx = await this.ensureCDC(state.databaseId, state.database);
586
+ ctx = await this.ensureCDC(state.databaseId, state.database);
431
587
  await ctx.tracker.watch(ctx.cdcConn, msg.table);
432
588
  const sub = ctx.manager.subscribe(msg.table, filter, (event) => {
433
589
  this.sendChange(conn, id, event);
@@ -435,6 +591,9 @@ var WSHandler = class {
435
591
  state.subscriptions.set(id, sub);
436
592
  this.send(conn, { type: "subscribed", id });
437
593
  } catch (err) {
594
+ if (ctx?.manager.size === 0) {
595
+ this.maybeCleanupCDC(state.databaseId);
596
+ }
438
597
  this.sendSirannonError(conn, id, err);
439
598
  }
440
599
  }
@@ -474,6 +633,13 @@ var WSHandler = class {
474
633
  const cdcConn = await this.sirannon.driver.open(database.path, { walMode: true });
475
634
  const tracker = new ChangeTracker();
476
635
  const manager = new SubscriptionManager();
636
+ try {
637
+ await tracker.advanceToLatest(cdcConn);
638
+ } catch (err) {
639
+ await cdcConn.close().catch(() => {
640
+ });
641
+ throw err;
642
+ }
477
643
  let polling = false;
478
644
  let consecutiveErrors = 0;
479
645
  const MAX_CONSECUTIVE_ERRORS = 10;
@@ -500,9 +666,7 @@ var WSHandler = class {
500
666
  }
501
667
  };
502
668
  const interval = setInterval(tick, DEFAULT_POLL_INTERVAL_MS);
503
- if (typeof interval === "object" && "unref" in interval && typeof interval.unref === "function") {
504
- interval.unref();
505
- }
669
+ interval.unref?.();
506
670
  return { cdcConn, tracker, manager, stopPolling };
507
671
  }
508
672
  maybeCleanupCDC(databaseId) {
@@ -517,6 +681,12 @@ var WSHandler = class {
517
681
  if (params === void 0 || params === null) return true;
518
682
  return typeof params === "object";
519
683
  }
684
+ async resolveTarget(databaseId) {
685
+ if (!this.resolveExecutionTarget) {
686
+ return await this.sirannon.resolve(databaseId) ?? null;
687
+ }
688
+ return await this.resolveExecutionTarget(databaseId) ?? null;
689
+ }
520
690
  send(conn, msg) {
521
691
  try {
522
692
  conn.send(JSON.stringify(msg));
@@ -580,6 +750,10 @@ function writeCorsOrigin(res, cors, requestOrigin) {
580
750
  res.writeHeader("Vary", "Origin");
581
751
  }
582
752
  }
753
+ function selectWebSocketProtocol(header) {
754
+ const [firstProtocol] = header.split(",");
755
+ return firstProtocol?.trim() ?? "";
756
+ }
583
757
  function decodeRemoteAddress(res) {
584
758
  return Buffer.from(res.getRemoteAddressAsText()).toString();
585
759
  }
@@ -606,6 +780,9 @@ var SirannonServer = class {
606
780
  port;
607
781
  cors;
608
782
  onRequestHook;
783
+ resolveExecutionTarget;
784
+ getReplicationStatus;
785
+ getClusterStatus;
609
786
  sirannon;
610
787
  wsHandler;
611
788
  constructor(sirannon, options) {
@@ -614,7 +791,10 @@ var SirannonServer = class {
614
791
  this.port = options?.port ?? 9876;
615
792
  this.cors = resolveCors(options?.cors);
616
793
  this.onRequestHook = options?.onRequest;
617
- this.wsHandler = new WSHandler(sirannon);
794
+ this.resolveExecutionTarget = options?.resolveExecutionTarget;
795
+ this.getReplicationStatus = options?.getReplicationStatus;
796
+ this.getClusterStatus = options?.getClusterStatus;
797
+ this.wsHandler = new WSHandler(sirannon, { resolveExecutionTarget: this.resolveExecutionTarget });
618
798
  this.app = uWS.App();
619
799
  this.registerRoutes();
620
800
  }
@@ -657,10 +837,14 @@ var SirannonServer = class {
657
837
  });
658
838
  }
659
839
  this.app.get("/health", this.withCors(handleLiveness()));
660
- this.app.get("/health/ready", this.withCors(handleReadiness(this.sirannon)));
661
- this.app.post("/db/:id/query", this.wrapDbRoute(handleQuery(this.sirannon)));
662
- this.app.post("/db/:id/execute", this.wrapDbRoute(handleExecute(this.sirannon)));
663
- this.app.post("/db/:id/transaction", this.wrapDbRoute(handleTransaction(this.sirannon)));
840
+ this.app.get("/health/ready", this.withCors(handleReadiness(this.sirannon, this.getReplicationStatus)));
841
+ this.app.get("/db/:id/cluster", this.wrapDbGetRoute(handleClusterStatus(this.getClusterStatus)));
842
+ this.app.post("/db/:id/query", this.wrapDbRoute(handleQuery(this.sirannon, this.resolveExecutionTarget)));
843
+ this.app.post("/db/:id/execute", this.wrapDbRoute(handleExecute(this.sirannon, this.resolveExecutionTarget)));
844
+ this.app.post(
845
+ "/db/:id/transaction",
846
+ this.wrapDbRoute(handleTransaction(this.sirannon, this.resolveExecutionTarget))
847
+ );
664
848
  this.registerWebSocketRoute();
665
849
  this.app.any("/*", (res) => {
666
850
  sendError(res, 404, "NOT_FOUND", "Route not found");
@@ -679,6 +863,7 @@ var SirannonServer = class {
679
863
  const method = req.getMethod();
680
864
  const secWebSocketKey = req.getHeader("sec-websocket-key");
681
865
  const secWebSocketProtocol = req.getHeader("sec-websocket-protocol");
866
+ const selectedWebSocketProtocol = selectWebSocketProtocol(secWebSocketProtocol);
682
867
  const secWebSocketExtensions = req.getHeader("sec-websocket-extensions");
683
868
  const headers = {};
684
869
  req.forEach((key, value) => {
@@ -694,7 +879,7 @@ var SirannonServer = class {
694
879
  res.upgrade(
695
880
  { databaseId: dbId },
696
881
  secWebSocketKey,
697
- secWebSocketProtocol,
882
+ selectedWebSocketProtocol,
698
883
  secWebSocketExtensions,
699
884
  context
700
885
  );
@@ -713,7 +898,7 @@ var SirannonServer = class {
713
898
  res.upgrade(
714
899
  { databaseId: dbId },
715
900
  secWebSocketKey,
716
- secWebSocketProtocol,
901
+ selectedWebSocketProtocol,
717
902
  secWebSocketExtensions,
718
903
  context
719
904
  );
@@ -815,6 +1000,39 @@ var SirannonServer = class {
815
1000
  });
816
1001
  };
817
1002
  }
1003
+ wrapDbGetRoute(handler) {
1004
+ const onRequestHook = this.onRequestHook;
1005
+ const corsHeaders = this.cors;
1006
+ return (res, req) => {
1007
+ const dbId = req.getParameter(0) ?? "";
1008
+ const method = req.getMethod();
1009
+ const path = req.getUrl();
1010
+ if (corsHeaders) {
1011
+ writeCorsOrigin(res, corsHeaders, req.getHeader("origin"));
1012
+ }
1013
+ if (!onRequestHook) {
1014
+ handler(res, dbId);
1015
+ return;
1016
+ }
1017
+ const headers = {};
1018
+ req.forEach((key, value) => {
1019
+ headers[key] = value;
1020
+ });
1021
+ const ctx = {
1022
+ headers,
1023
+ method,
1024
+ path,
1025
+ databaseId: dbId,
1026
+ remoteAddress: decodeRemoteAddress(res)
1027
+ };
1028
+ runOnRequest(res, ctx, onRequestHook).then((allowed) => {
1029
+ if (allowed) {
1030
+ handler(res, dbId);
1031
+ }
1032
+ }).catch(() => {
1033
+ });
1034
+ };
1035
+ }
818
1036
  };
819
1037
  function createServer(sirannon, options) {
820
1038
  return new SirannonServer(sirannon, options);
@@ -0,0 +1,31 @@
1
+ import { D as Database } from './database-BVY1GqE7.js';
2
+ import { S as SQLiteDriver } from './types-BFSsG77t.js';
3
+ import { S as SirannonOptions, D as DatabaseOptions, B as BeforeQueryHook, A as AfterQueryHook, a as BeforeConnectHook, b as DatabaseOpenHook, c as DatabaseCloseHook } from './types-D-74JiXb.js';
4
+
5
+ declare class Sirannon {
6
+ readonly options: SirannonOptions;
7
+ private readonly dbs;
8
+ private readonly opening;
9
+ private _shutdown;
10
+ private readonly _driver;
11
+ private readonly hookRegistry;
12
+ private readonly metricsCollector;
13
+ private readonly lifecycleManager;
14
+ constructor(options: SirannonOptions);
15
+ get driver(): SQLiteDriver;
16
+ open(id: string, path: string, options?: DatabaseOptions): Promise<Database>;
17
+ close(id: string): Promise<void>;
18
+ get(id: string): Database | undefined;
19
+ resolve(id: string): Promise<Database | undefined>;
20
+ has(id: string): boolean;
21
+ databases(): Map<string, Database>;
22
+ shutdown(): Promise<void>;
23
+ onBeforeQuery(hook: BeforeQueryHook): void;
24
+ onAfterQuery(hook: AfterQueryHook): void;
25
+ onBeforeConnect(hook: BeforeConnectHook): void;
26
+ onDatabaseOpen(hook: DatabaseOpenHook): void;
27
+ onDatabaseClose(hook: DatabaseCloseHook): void;
28
+ private ensureRunning;
29
+ }
30
+
31
+ export { Sirannon as S };