@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.
- package/README.md +415 -39
- package/dist/backup-scheduler/index.d.ts +2 -2
- package/dist/change-tracker-CFTQ9TSn.d.ts +89 -0
- package/dist/chunk-3MCMONVP.mjs +115 -0
- package/dist/chunk-ER7ODTDA.mjs +23 -0
- package/dist/chunk-GS7T5YMI.mjs +51 -0
- package/dist/{chunk-AX66KWBR.mjs → chunk-UTO3ZAFS.mjs} +226 -64
- package/dist/chunk-UVMVN3OT.mjs +111 -0
- package/dist/client/index.d.ts +99 -42
- package/dist/client/index.mjs +726 -26
- package/dist/core/index.d.ts +11 -108
- package/dist/core/index.mjs +134 -168
- package/dist/{sirannon-B1oTfebD.d.ts → database-BVY1GqE7.d.ts} +8 -33
- package/dist/errors-C00ed08Q.d.ts +101 -0
- package/dist/file-migrations/index.d.ts +2 -2
- package/dist/{index-hXiis3N-.d.ts → index-CLdNrcPz.d.ts} +1 -1
- 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 +14 -3
- package/dist/server/index.mjs +262 -44
- 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-BeozgNPr.d.ts +26 -0
- package/dist/{types-DtDutWRU.d.ts → types-D-74JiXb.d.ts} +78 -2
- package/package.json +54 -10
- package/dist/types-DRkJlqex.d.ts +0 -38
package/dist/server/index.d.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { E as ExecuteResult,
|
|
2
|
-
import { S as Sirannon } from '../sirannon-
|
|
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 '../
|
|
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;
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ChangeTracker, SubscriptionManager } from '../chunk-
|
|
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
|
|
145
|
-
let
|
|
186
|
+
async function resolveExecutionTarget(res, sirannon, id, resolver) {
|
|
187
|
+
let target;
|
|
146
188
|
try {
|
|
147
|
-
|
|
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 (!
|
|
198
|
+
if (!target) {
|
|
157
199
|
sendError(res, 404, "DATABASE_NOT_FOUND", `Database '${id}' not found`);
|
|
158
200
|
return null;
|
|
159
201
|
}
|
|
160
|
-
return
|
|
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
|
|
171
|
-
if (!
|
|
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
|
|
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
|
|
195
|
-
if (!
|
|
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
|
|
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
|
|
230
|
-
if (!
|
|
285
|
+
const target = await resolveExecutionTarget(res, sirannon, dbId, resolveTarget);
|
|
286
|
+
if (!target) return;
|
|
231
287
|
try {
|
|
232
|
-
const results = await
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
662
|
-
this.app.post("/db/:id/
|
|
663
|
-
this.app.post("/db/:id/
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|