@jarenjs/db 0.56.0 → 0.67.0
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/ARCHITECTURE.md +412 -56
- package/README.md +600 -57
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +293 -45
- package/docs/LIVE-FORMAT.md +169 -20
- package/docs/MIGRATION-FORMAT.md +142 -17
- package/docs/MODEL-FORMAT.md +752 -64
- package/docs/REPLICATION-FORMAT.md +208 -0
- package/package.json +21 -7
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/schemas/jaren-replication-snapshot.draft-07.schema.json +83 -0
- package/schemas/jaren-replication-snapshot.schema.json +83 -0
- package/schemas/jaren-replication.draft-07.schema.json +82 -0
- package/schemas/jaren-replication.schema.json +82 -0
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +230 -47
- package/src/cli.js +165 -59
- package/src/cursor.js +417 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +102 -8
- package/src/dialect.js +268 -113
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +244 -11
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +448 -61
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +17 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit.js +339 -48
- package/src/entity.js +20 -22
- package/src/errors.js +430 -19
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -17
- package/src/introspect.js +583 -0
- package/src/jobs.js +843 -107
- package/src/json-bytes.js +58 -0
- package/src/live-join.js +250 -0
- package/src/live-nested.js +120 -0
- package/src/live.js +18 -4
- package/src/logical-rows.js +90 -0
- package/src/maintenance.js +175 -0
- package/src/migrate.js +248 -181
- package/src/model.js +68 -0
- package/src/plan.js +1119 -138
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1634 -323
- package/src/replication-format.js +115 -0
- package/src/replication.js +332 -0
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1567 -273
- package/src/tracker.js +203 -29
- package/src/udf.js +88 -7
- package/types/index.d.ts +1158 -27
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +69 -2
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +27 -4
- package/types/wasm.d.ts +14 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/** One writer and bounded read-only WAL workers behind a Connection. */
|
|
3
|
+
import { finishConnection } from '../driver.js';
|
|
4
|
+
import { DbCompileError, DbRuntimeError } from '../errors.js';
|
|
5
|
+
import { sqliteDialect } from '../dialects/sqlite.js';
|
|
6
|
+
import { positiveOption } from './worker-protocol.js';
|
|
7
|
+
import { workerQueue } from './worker-queue.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reads explicitly classified by their compiled operation may use a read worker.
|
|
11
|
+
* All other work uses the writer; an open transaction keeps one worker.
|
|
12
|
+
* @param {{ readers?: number, queueCapacity?: number, graceMs?: number,
|
|
13
|
+
* worker?: any }} [configuration]
|
|
14
|
+
* @returns {any} a Driver
|
|
15
|
+
*/
|
|
16
|
+
export function workerPoolDriver(configuration, driverFactory) {
|
|
17
|
+
const readers = configuration.readers ?? 2;
|
|
18
|
+
const capacity = configuration.queueCapacity ?? 64;
|
|
19
|
+
if (!Number.isSafeInteger(readers) || readers < 0 || readers > 32) throw new TypeError('readers must be an integer from 0 through 32');
|
|
20
|
+
if (!Number.isSafeInteger(capacity) || capacity < 0) throw new TypeError('queueCapacity must be a nonnegative safe integer');
|
|
21
|
+
const graceMs = positiveOption('graceMs', configuration.graceMs, 5000);
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
name: 'node-worker-pool-sqlite', dialect: sqliteDialect,
|
|
24
|
+
open: async (path = ':memory:', options = {}) => {
|
|
25
|
+
const driver = driverFactory(configuration.worker);
|
|
26
|
+
const slots = [];
|
|
27
|
+
let closed = false;
|
|
28
|
+
let transaction = null;
|
|
29
|
+
let depth = 0;
|
|
30
|
+
let committing = null;
|
|
31
|
+
let sequence = 0;
|
|
32
|
+
const memory = path === ':memory:' || path === '';
|
|
33
|
+
const makeSlot = async (readOnly) => {
|
|
34
|
+
const connection = await driver.open(path, { ...options, readOnly });
|
|
35
|
+
return { connection, readOnly, active: false, healthy: true, generation: connection.generation,
|
|
36
|
+
statements: new Map(), executions: 0 };
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
slots.push(await makeSlot(options.readOnly === true));
|
|
40
|
+
if (!memory) {
|
|
41
|
+
const writer = slots[0].connection;
|
|
42
|
+
const statement = await writer.prepare(options.readOnly === true ? sqliteDialect.introspect.pragma('journal_mode') : sqliteDialect.pragma.set('journal_mode', 'WAL'));
|
|
43
|
+
const mode = await statement.get();
|
|
44
|
+
if (String(Object.values(mode)[0]).toLowerCase() !== 'wal')
|
|
45
|
+
throw new DbCompileError('JD0008', 'a worker pool requires a file in WAL mode');
|
|
46
|
+
for (let i = 0; i < readers; i++) slots.push(await makeSlot(true));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) { await Promise.allSettled(slots.map((slot) => slot.connection.close())); throw error; }
|
|
50
|
+
const queue = workerQueue(slots, capacity, () => performance.now());
|
|
51
|
+
const readLane = (readOnly) => options.readOnly === true || (readOnly && slots.length > 1);
|
|
52
|
+
const replace = async (slot) => {
|
|
53
|
+
if (closed || slot.healthy) return;
|
|
54
|
+
await slot.connection.close().catch(() => {});
|
|
55
|
+
try {
|
|
56
|
+
const fresh = await makeSlot(slot.readOnly);
|
|
57
|
+
Object.assign(slot, fresh);
|
|
58
|
+
queue.wake();
|
|
59
|
+
}
|
|
60
|
+
catch (error) { queue.stop(error); }
|
|
61
|
+
};
|
|
62
|
+
const execute = async (lease, fn) => {
|
|
63
|
+
try { lease.slot.executions++; return await fn(lease.slot); }
|
|
64
|
+
catch (error) { if (error?.code === 'JD2090') lease.slot.healthy = false; throw error; }
|
|
65
|
+
};
|
|
66
|
+
const withLease = async (readOnly, fn) => {
|
|
67
|
+
if (closed) throw new DbRuntimeError('JD2063', 'the worker pool is closed');
|
|
68
|
+
if (transaction !== null) return execute(transaction, fn);
|
|
69
|
+
const lease = await queue.acquire(readLane(readOnly));
|
|
70
|
+
try { return await execute(lease, fn); }
|
|
71
|
+
finally { lease.release(); await replace(lease.slot); }
|
|
72
|
+
};
|
|
73
|
+
const raw = {
|
|
74
|
+
closeDrainsIterators: true,
|
|
75
|
+
exec: async (sql) => {
|
|
76
|
+
const begin = /^(?:SAVEPOINT|BEGIN)\b/.test(sql);
|
|
77
|
+
const release = /^RELEASE\b/.test(sql);
|
|
78
|
+
const end = /^(?:COMMIT|ROLLBACK(?! TO))\b/.test(sql);
|
|
79
|
+
const rollback = /^ROLLBACK\b/.test(sql);
|
|
80
|
+
if (begin && transaction === null) transaction = await queue.acquire(options.readOnly === true);
|
|
81
|
+
if (begin) depth++;
|
|
82
|
+
const operation = withLease(false, (slot) => slot.connection.exec(sql));
|
|
83
|
+
if (end || (release && depth === 1)) committing = operation;
|
|
84
|
+
let succeeded = false;
|
|
85
|
+
try { const value = await operation; succeeded = true; return value; }
|
|
86
|
+
catch (error) {
|
|
87
|
+
// A failed begin owns no new savepoint. A lost generation
|
|
88
|
+
// cannot acknowledge rollback or the following release; unwind
|
|
89
|
+
// exactly this scope, leaving any outer owner pinned until its
|
|
90
|
+
// own rollback settles. A failed COMMIT keeps its lease for
|
|
91
|
+
// the normal rollback path.
|
|
92
|
+
if (begin || (rollback && error?.code === 'JD2090'))
|
|
93
|
+
depth = end ? 0 : Math.max(0, depth - 1);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
if (succeeded && release) depth = Math.max(0, depth - 1);
|
|
98
|
+
if (succeeded && end) depth = 0;
|
|
99
|
+
if (transaction !== null && depth === 0) {
|
|
100
|
+
const lease = transaction;
|
|
101
|
+
transaction = null;
|
|
102
|
+
lease.release();
|
|
103
|
+
await replace(lease.slot);
|
|
104
|
+
}
|
|
105
|
+
if (committing === operation) committing = null;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
prepare: (sql, metadata = {}) => {
|
|
109
|
+
const id = ++sequence;
|
|
110
|
+
const statementFor = async (slot) => {
|
|
111
|
+
if (!slot.statements.has(id) || metadata.ephemeral) {
|
|
112
|
+
const statement = await slot.connection.prepare(sql, metadata);
|
|
113
|
+
if (metadata.ephemeral) return statement;
|
|
114
|
+
slot.statements.set(id, statement);
|
|
115
|
+
}
|
|
116
|
+
return slot.statements.get(id);
|
|
117
|
+
};
|
|
118
|
+
const call = (method, params) => withLease(metadata.readOnly === true,
|
|
119
|
+
async (slot) => (await statementFor(slot))[method](params));
|
|
120
|
+
return {
|
|
121
|
+
run: (params = []) => call('run', params),
|
|
122
|
+
get: (params = []) => call('get', params),
|
|
123
|
+
all: (params = []) => call('all', params),
|
|
124
|
+
iterate: async (params = []) => {
|
|
125
|
+
const pinned = transaction;
|
|
126
|
+
const lease = pinned ?? await queue.acquire(readLane(metadata.readOnly === true));
|
|
127
|
+
let iterator;
|
|
128
|
+
try { iterator = await execute(lease, async (slot) => (await statementFor(slot)).iterate(params)); }
|
|
129
|
+
catch (error) { if (pinned === null) { lease.release(); await replace(lease.slot); } throw error; }
|
|
130
|
+
let done = false;
|
|
131
|
+
const finish = async () => {
|
|
132
|
+
if (done) return;
|
|
133
|
+
done = true;
|
|
134
|
+
try { await iterator.return(); }
|
|
135
|
+
finally { if (pinned === null) { lease.release(); await replace(lease.slot); } }
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
next: async () => {
|
|
139
|
+
if (done) return { done: true, value: undefined };
|
|
140
|
+
try {
|
|
141
|
+
const step = await execute(lease, () => iterator.next());
|
|
142
|
+
if (step.done) await finish();
|
|
143
|
+
return step;
|
|
144
|
+
}
|
|
145
|
+
catch (error) { await finish().catch(() => {}); throw error; }
|
|
146
|
+
},
|
|
147
|
+
return: async () => { await finish(); return { done: true, value: undefined }; },
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
close: async () => {
|
|
153
|
+
if (closed) return;
|
|
154
|
+
closed = true;
|
|
155
|
+
queue.stop();
|
|
156
|
+
let timer;
|
|
157
|
+
await Promise.race([queue.drain(), new Promise((resolve) => { timer = setTimeout(resolve, graceMs); })]);
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
// A commit already sent to SQLite owns its settlement. Closing
|
|
160
|
+
// never terminates that writer while its outcome is pending.
|
|
161
|
+
if (committing !== null) await committing.catch(() => {});
|
|
162
|
+
try { await Promise.all(slots.map((slot) => slot.connection.close())); }
|
|
163
|
+
finally {
|
|
164
|
+
for (const slot of slots) {
|
|
165
|
+
slot.healthy = false;
|
|
166
|
+
slot.active = false;
|
|
167
|
+
slot.statements.clear();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const capabilities = Object.freeze({ ...slots[0].connection.capabilities,
|
|
173
|
+
pooling: true, poolReaders: slots.filter((slot) => slot.readOnly).length,
|
|
174
|
+
poolWriters: slots.filter((slot) => !slot.readOnly).length });
|
|
175
|
+
const connection = finishConnection(raw, sqliteDialect, false, capabilities, options.queueTimeout);
|
|
176
|
+
return Object.freeze({ ...connection, get mustQueue() { return connection.mustQueue; },
|
|
177
|
+
metrics: () => Object.freeze({ ...queue.metrics(), workers: Object.freeze(slots.map((slot) =>
|
|
178
|
+
Object.freeze({ readOnly: slot.readOnly, healthy: slot.healthy, active: slot.active,
|
|
179
|
+
generation: slot.generation, executions: slot.executions }))) }),
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/** Structured-clone frames; identities belong to one connection generation. */
|
|
3
|
+
import { DbRuntimeError } from '../errors.js';
|
|
4
|
+
import { jsonStringBytes } from '../json-bytes.js';
|
|
5
|
+
|
|
6
|
+
export const WORKER_PROTOCOL_VERSION = 1;
|
|
7
|
+
const operations = new Set(['exec', 'prepare', 'run', 'get', 'iterate', 'next', 'return', 'finalize', 'close']);
|
|
8
|
+
|
|
9
|
+
/** @param {number} generation @param {boolean} [transaction] @param {unknown} [cause] */
|
|
10
|
+
export function generationFailure(generation, transaction = false, cause = undefined) {
|
|
11
|
+
return Object.assign(new DbRuntimeError('JD2090',
|
|
12
|
+
`driver generation ${generation} is no longer available; reopen the connection; no operation was replayed`,
|
|
13
|
+
{ cause }), { class: 'generation', retryable: !transaction, generation });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @param {any} frame @returns {boolean} */
|
|
17
|
+
export function validRequest(frame) {
|
|
18
|
+
if (frame === null || typeof frame !== 'object' || frame.v !== WORKER_PROTOCOL_VERSION
|
|
19
|
+
|| frame.kind !== 'request' || !Number.isSafeInteger(frame.id) || frame.id < 1
|
|
20
|
+
|| !Number.isSafeInteger(frame.generation) || frame.generation < 1 || !operations.has(frame.op)) return false;
|
|
21
|
+
if (frame.op === 'exec' || frame.op === 'prepare') return typeof frame.sql === 'string';
|
|
22
|
+
if (['run', 'get', 'iterate'].includes(frame.op))
|
|
23
|
+
return Number.isSafeInteger(frame.statement) && frame.statement > 0 && Array.isArray(frame.params);
|
|
24
|
+
if (frame.op === 'next') return Number.isSafeInteger(frame.cursor) && frame.cursor > 0
|
|
25
|
+
&& Number.isSafeInteger(frame.rows) && frame.rows > 0 && Number.isSafeInteger(frame.bytes) && frame.bytes > 0;
|
|
26
|
+
if (frame.op === 'return') return Number.isSafeInteger(frame.cursor) && frame.cursor > 0;
|
|
27
|
+
if (frame.op === 'finalize') return Number.isSafeInteger(frame.statement) && frame.statement > 0;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Validate clone frames before dereferencing a remote payload.
|
|
32
|
+
* @param {any} frame @param {number} generation @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export function validResponse(frame, generation) {
|
|
35
|
+
if (frame === null || typeof frame !== 'object' || frame.v !== WORKER_PROTOCOL_VERSION
|
|
36
|
+
|| frame.generation !== generation || !Number.isSafeInteger(frame.id) || frame.id < 0) return false;
|
|
37
|
+
if (frame.kind === 'ready') return frame.id === 0 && frame.capabilities !== null
|
|
38
|
+
&& typeof frame.capabilities === 'object' && typeof frame.capabilities.version === 'string';
|
|
39
|
+
if (frame.kind === 'failure') return frame.error !== null && typeof frame.error === 'object'
|
|
40
|
+
&& typeof frame.error.message === 'string';
|
|
41
|
+
return frame.kind === 'result' && frame.id > 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {string} op @param {any} value @param {{rows:number, bytes:number}} limits */
|
|
45
|
+
export function validResult(op, value, limits) {
|
|
46
|
+
if (op === 'prepare' || op === 'iterate') return Number.isSafeInteger(value) && value > 0;
|
|
47
|
+
if (op !== 'next') return true;
|
|
48
|
+
return value !== null && typeof value === 'object' && Array.isArray(value.rows)
|
|
49
|
+
&& value.rows.length <= limits.rows && typeof value.done === 'boolean'
|
|
50
|
+
&& (value.rows.length > 0 || value.done) && Number.isSafeInteger(value.bytes)
|
|
51
|
+
&& value.bytes >= 0 && value.bytes <= limits.bytes
|
|
52
|
+
&& value.rows.reduce((sum, row) => sum + rowBytes(row), 0) === value.bytes;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A stable credit measure: JSON keys/scalars and raw blob bytes. Transport
|
|
56
|
+
* metadata is fixed per frame; no JSON string or copied blob is allocated.
|
|
57
|
+
* @param {any} value @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
export function rowBytes(value) {
|
|
60
|
+
if (value === null || value === undefined) return 4;
|
|
61
|
+
if (ArrayBuffer.isView(value)) return value.byteLength;
|
|
62
|
+
if (typeof value === 'string') return jsonStringBytes(value);
|
|
63
|
+
if (typeof value !== 'object') return String(value).length;
|
|
64
|
+
let bytes = 2;
|
|
65
|
+
for (const key of Object.keys(value)) bytes += jsonStringBytes(key) + 2 + rowBytes(value[key]);
|
|
66
|
+
return bytes;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** @param {string} name @param {any} value @param {number} fallback @returns {number} */
|
|
70
|
+
export function positiveOption(name, value, fallback) {
|
|
71
|
+
if (value === undefined) return fallback;
|
|
72
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${name} must be a positive safe integer`);
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @param {string} reason @param {number} depth */
|
|
77
|
+
export function queueFailure(reason, depth) {
|
|
78
|
+
return Object.assign(new DbRuntimeError('JD2091', reason), { class: 'queue', retryable: true, depth });
|
|
79
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/** Bounded FIFO admission, with a deterministic clock and inspectable leases. */
|
|
3
|
+
import { DbRuntimeError } from '../errors.js';
|
|
4
|
+
import { queueFailure } from './worker-protocol.js';
|
|
5
|
+
|
|
6
|
+
/** @param {any[]} slots @param {number} capacity @param {() => number} now */
|
|
7
|
+
export function workerQueue(slots, capacity, now) {
|
|
8
|
+
const waiting = [];
|
|
9
|
+
const waits = [];
|
|
10
|
+
const observers = new Set();
|
|
11
|
+
let stopped = null;
|
|
12
|
+
const idle = () => { if (slots.every((slot) => !slot.active)) for (const fn of observers) fn(); };
|
|
13
|
+
const choose = (readOnly) => slots.find((slot) => !slot.active && slot.healthy
|
|
14
|
+
&& (readOnly ? slot.readOnly : !slot.readOnly));
|
|
15
|
+
const grant = (slot, entry) => {
|
|
16
|
+
slot.active = true;
|
|
17
|
+
const wait = Math.max(0, now() - entry.at);
|
|
18
|
+
if (waits.length === 1024) waits.shift();
|
|
19
|
+
waits.push(wait);
|
|
20
|
+
let released = false;
|
|
21
|
+
entry.resolve({ slot, release: () => {
|
|
22
|
+
if (released) return;
|
|
23
|
+
released = true;
|
|
24
|
+
slot.active = false;
|
|
25
|
+
pump();
|
|
26
|
+
idle();
|
|
27
|
+
} });
|
|
28
|
+
};
|
|
29
|
+
const pump = () => {
|
|
30
|
+
while (waiting.length > 0) {
|
|
31
|
+
const slot = choose(waiting[0].readOnly);
|
|
32
|
+
if (slot === undefined) break;
|
|
33
|
+
grant(slot, waiting.shift());
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
acquire: (readOnly) => new Promise((resolve, reject) => {
|
|
38
|
+
if (stopped !== null) { reject(stopped); return; }
|
|
39
|
+
const entry = { readOnly, resolve, reject, at: now() };
|
|
40
|
+
const slot = waiting.length === 0 ? choose(readOnly) : undefined;
|
|
41
|
+
if (slot !== undefined) { grant(slot, entry); return; }
|
|
42
|
+
if (waiting.length >= capacity) { reject(queueFailure(`worker pool queue capacity ${capacity} exceeded`, waiting.length)); return; }
|
|
43
|
+
waiting.push(entry);
|
|
44
|
+
}),
|
|
45
|
+
stop(error = new DbRuntimeError('JD2063', 'the worker pool closed before the queued work ran')) {
|
|
46
|
+
stopped = error;
|
|
47
|
+
for (const entry of waiting.splice(0)) entry.reject(error);
|
|
48
|
+
},
|
|
49
|
+
drain: () => slots.every((slot) => !slot.active) ? Promise.resolve()
|
|
50
|
+
: new Promise((resolve) => { const done = () => { observers.delete(done); resolve(undefined); }; observers.add(done); }),
|
|
51
|
+
wake: pump,
|
|
52
|
+
metrics: () => {
|
|
53
|
+
const values = [...waits].sort((a, b) => a - b);
|
|
54
|
+
return Object.freeze({ active: slots.filter((s) => s.active).length,
|
|
55
|
+
idle: slots.filter((s) => !s.active && s.healthy).length, queued: waiting.length,
|
|
56
|
+
waitMs: Object.freeze({ p50: values[Math.floor(values.length * 0.5)] ?? 0,
|
|
57
|
+
p95: values[Math.min(values.length - 1, Math.floor(values.length * 0.95))] ?? 0 }) });
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|