@romedb/node 0.0.0-stage → 0.2.12

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.
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+ const { parentPort, workerData } = require('node:worker_threads');
3
+ const { createHash } = require('node:crypto');
4
+ const { NativeDatabase } = require('./native.cjs');
5
+ const { validatePrepared } = require('./client.cjs');
6
+ let database;
7
+ let sequence = 0;
8
+ let fenced = false;
9
+ const receipts = new Map();
10
+ try {
11
+ database = new NativeDatabase(workerData.path, '10000', '67108864', workerData.create ? 'create-mvcc' : 'mvcc');
12
+ const foreignKeys = JSON.parse(database.execute('PRAGMA foreign_keys=ON', '{}'));
13
+ if (foreignKeys.execution.error) throw new Error(foreignKeys.execution.error.message);
14
+ parentPort.postMessage({ ready: true });
15
+ } catch (error) {
16
+ const coded = /^(FDB_[A-Z0-9_]+): (.*)$/s.exec(error.message);
17
+ parentPort.postMessage({ readyError: { code: /file is locked/i.test(error.message) ? 'FDB_BUSY' : coded?.[1] ?? (error.code?.startsWith('FDB_') ? error.code : 'FDB_STORAGE'), message: coded?.[2] ?? error.message } });
18
+ database?.close();
19
+ parentPort.close();
20
+ }
21
+
22
+ if (database) parentPort.on('message', ({ id, operation, body, cancellationKey }) => {
23
+ try {
24
+ if (operation === 'close') {
25
+ if (!fenced) try { database.execute('PRAGMA wal_checkpoint(TRUNCATE)', '{}'); } catch {}
26
+ database.close();
27
+ parentPort.postMessage({ id });
28
+ parentPort.close();
29
+ return;
30
+ }
31
+ if (fenced) fail('FDB_STORAGE', 'Embedded outcome is uncertain; close and reconcile the database', 'unknown');
32
+ if (operation === 'info') {
33
+ const mode = JSON.parse(database.execute('PRAGMA journal_mode', '{}'));
34
+ if (mode.execution.error) fail('FDB_STORAGE', 'Cannot inspect embedded journal mode', 'read-failed');
35
+ const engine = JSON.parse(database.execute('SELECT romedb_version()', '{}'));
36
+ if (engine.execution.error) fail('FDB_STORAGE', 'Cannot read the embedded engine version', 'read-failed');
37
+ parentPort.postMessage({ id, result: {
38
+ id: workerData.databaseId, sequence, adapterVersion: 1,
39
+ engineVersion: engine.execution.result.rows[0][0].value,
40
+ journalMode: mode.execution.result.rows[0][0].value,
41
+ capabilities: { readVersion: 2, atomicWrites: true, replayWindow: 64, exactSequenceCondition: true, sequenceScope: 'adapter', replayDurability: 'adapter-lifetime', metadata: true, checkedWork: true, foreignKeys: true },
42
+ limits: { maxRequestBytes: 65536, maxStatements: 32, maxRows: 1000, maxResultBytes: 262144, activeRequests: 1, queuedRequests: 32, rowsRead: 1000000, rowMutations: 100000, vmSteps: 10000000 },
43
+ } });
44
+ return;
45
+ }
46
+ if (!['read', 'write'].includes(operation)) fail('FDB_UNSUPPORTED', 'Unknown embedded operation', 'not-sent');
47
+ const reading = operation === 'read';
48
+ const request = JSON.parse(body);
49
+ let writeBody = body;
50
+ if (reading) {
51
+ if (request.readVersion !== 2 || request.ifSequence !== undefined) fail('FDB_VALIDATION', 'Invalid fresh read envelope', 'read-failed');
52
+ const { readVersion, ...rest } = request;
53
+ writeBody = JSON.stringify(rest);
54
+ }
55
+ validatePrepared({ version: 1, url: workerData.url, databaseId: workerData.databaseId, body: writeBody });
56
+ const hash = createHash('sha256').update(body).digest('hex');
57
+ if (!reading && receipts.has(request.requestId)) {
58
+ const receipt = receipts.get(request.requestId);
59
+ if (receipt.hash !== hash) fail('FDB_REQUEST_IDENTITY', 'Request ID was reused with another body', 'unknown');
60
+ parentPort.postMessage({ id, result: receipt.result });
61
+ return;
62
+ }
63
+ if (request.afterSequence > sequence || (!reading && sequence - request.afterSequence >= 64)) fail('FDB_CONFLICT', 'Request position is ahead or its replay window expired; reconcile the request', reading ? 'read-failed' : 'unknown');
64
+ if (sequence >= Number.MAX_SAFE_INTEGER - 1 && !reading) fail('FDB_LIMIT', 'Adapter sequence is exhausted', 'not-sent');
65
+ let result;
66
+ if (!reading && request.ifSequence !== undefined && request.ifSequence !== sequence) {
67
+ result = { error: { code: 'FDB_CONFLICT', message: 'Sequence changed; refresh before saving' } };
68
+ } else {
69
+ const report = JSON.parse(database.executeAtomicRequest(JSON.stringify({ readOnly: reading, statements: request.statements, ...(request.workLimits === undefined ? {} : { workLimits: request.workLimits }) }), cancellationKey));
70
+ if (report.execution.error) {
71
+ const error = report.execution.error;
72
+ if (['FDB_STORAGE', 'FDB_ROLLBACK'].includes(error.code) || report.transaction.after !== 'autocommit') {
73
+ fenced = true;
74
+ fail('FDB_STORAGE', 'Atomic request outcome is uncertain; close and reconcile the database', 'unknown');
75
+ }
76
+ if (reading) fail(error.code, error.message, 'read-failed');
77
+ result = { error };
78
+ } else result = report.execution.result;
79
+ }
80
+ if (reading) result = { ...result, readVersion: 2, requestId: request.requestId, sequence };
81
+ else {
82
+ result = { ...result, requestId: request.requestId, sequence: ++sequence };
83
+ receipts.set(request.requestId, { hash, result });
84
+ if (receipts.size > 64) receipts.delete(receipts.keys().next().value);
85
+ }
86
+ parentPort.postMessage({ id, result });
87
+ } catch (error) {
88
+ parentPort.postMessage({ id, error: { code: error.code ?? 'FDB_VALIDATION', message: String(error.message).slice(0, 2048), outcome: error.outcome ?? (operation === 'read' ? 'read-failed' : 'unknown') } });
89
+ }
90
+ });
91
+
92
+ function fail(code, message, outcome) {
93
+ const error = new Error(message);
94
+ Object.assign(error, { code, outcome });
95
+ throw error;
96
+ }
package/index.cjs ADDED
@@ -0,0 +1,470 @@
1
+ 'use strict';
2
+ const { NativeDatabase, interruptConnection, createCancellationToken, cancelOperation, releaseCancellationToken, vectorFromComponents, vectorFromSparseEntries } = require('./native.cjs');
3
+ class Record {
4
+ constructor(table, key) {
5
+ if (typeof table !== 'string' || !['string','bigint'].includes(typeof key)) throw new TypeError('Record requires a table and string or bigint key');
6
+ this.table = table; this.key = key;
7
+ Object.freeze(this);
8
+ }
9
+ }
10
+ class Vector {
11
+ static float32(values) { return constructVector('float32', values); }
12
+ static float64(values) { return constructVector('float64', values); }
13
+ static sparse32(values) { return constructVector('sparse32', values); }
14
+ static sparse32Entries(dimensions, entries) {
15
+ if (!Number.isInteger(dimensions) || dimensions < 1 || dimensions > 65536) throw new RangeError('vector dimensions must be 1..65536');
16
+ if (!Array.isArray(entries)) throw new TypeError('sparse entries require an array of index/value pairs');
17
+ if (entries.length > dimensions) throw new RangeError('sparse entry count exceeds dimensions');
18
+ const bytes = Buffer.alloc(entries.length * 12);
19
+ let previous = -1;
20
+ for (let i = 0; i < entries.length; i++) {
21
+ const entry = entries[i];
22
+ if (!Array.isArray(entry) || entry.length !== 2) throw new TypeError('sparse entries require index/value pairs');
23
+ const [index, value] = entry;
24
+ if (!Number.isInteger(index) || index <= previous || index >= dimensions) throw new RangeError('sparse indices must be increasing and within dimensions');
25
+ if (typeof value !== 'number' || !Number.isFinite(value)) throw new TypeError('vector components must be finite numbers');
26
+ if (!Number.isFinite(Math.fround(value))) throw new RangeError('vector component is outside the finite float32 range');
27
+ bytes.writeUInt32LE(index, i * 12);
28
+ bytes.writeDoubleLE(value, i * 12 + 4);
29
+ previous = index;
30
+ }
31
+ return new Vector(vectorFromSparseEntries(dimensions, bytes));
32
+ }
33
+ static quantized8(values) { return constructVector('quantized8', values); }
34
+ static bit1(values) { return constructVector('bit1', values); }
35
+ constructor(bytes) {
36
+ if (!(bytes instanceof Uint8Array)) throw new TypeError('Vector requires encoded bytes');
37
+ this.bytes = Buffer.from(bytes);
38
+ }
39
+ }
40
+ function constructVector(encoding, values) {
41
+ if (!Array.isArray(values) && !(values instanceof Float32Array) && !(values instanceof Float64Array)) {
42
+ throw new TypeError('vector components require a number array, Float32Array or Float64Array');
43
+ }
44
+ if (values.length < 1 || values.length > 65536) throw new RangeError('vector dimensions must be 1..65536');
45
+ const bytes = Buffer.alloc(values.length * 8);
46
+ for (let i = 0; i < values.length; i++) {
47
+ const value = values[i];
48
+ if (typeof value !== 'number' || !Number.isFinite(value)) throw new TypeError('vector components must be finite numbers');
49
+ if (encoding !== 'float64' && !Number.isFinite(Math.fround(value))) throw new RangeError('vector component is outside the finite float32 range');
50
+ bytes.writeDoubleLE(value, i * 8);
51
+ }
52
+ return new Vector(vectorFromComponents(encoding, bytes));
53
+ }
54
+ function encode(value, depth = 0) {
55
+ if (depth > 64) throw new RangeError('value nesting exceeds 64');
56
+ const tagged = (type, value) => ({ type, value });
57
+ if (value === null) return { type: 'Null' };
58
+ if (typeof value === 'boolean') return tagged('Boolean', value);
59
+ if (typeof value === 'string') return tagged('String', text(value));
60
+ if (typeof value === 'bigint') {
61
+ if (value < -(1n << 63n) || value >= (1n << 63n)) throw new RangeError('integer outside int64');
62
+ return tagged('Integer', value.toString());
63
+ }
64
+ if (typeof value === 'number') {
65
+ if (!Number.isFinite(value)) throw new TypeError('non-finite number');
66
+ if (Number.isInteger(value) && !Object.is(value, -0)) {
67
+ if (!Number.isSafeInteger(value)) throw new RangeError('use bigint for integers outside the safe Number range');
68
+ return tagged('Integer', String(value));
69
+ }
70
+ const bytes = Buffer.alloc(8); bytes.writeDoubleBE(value);
71
+ return tagged('Number', bytes.toString('hex'));
72
+ }
73
+ if (value instanceof Record) return tagged('Record', { table: text(value.table), key: encode(value.key, depth) });
74
+ if (value instanceof Vector) return tagged('Vector', [...value.bytes]);
75
+ if (value instanceof Uint8Array) return tagged('Binary', [...value]);
76
+ if (Array.isArray(value)) {
77
+ const entries = [];
78
+ for (let i = 0; i < value.length; i++) {
79
+ if (!Object.hasOwn(value, i)) throw new TypeError('array holes are not database values');
80
+ entries.push(encode(value[i], depth + 1));
81
+ }
82
+ return tagged('Array', entries);
83
+ }
84
+ if (value && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) {
85
+ return tagged('Object', Object.fromEntries(Object.entries(value).map(([k,v]) => [text(k), encode(v, depth + 1)])));
86
+ }
87
+ throw new TypeError('unsupported parameter value');
88
+ }
89
+ function text(value) {
90
+ if (!value.isWellFormed()) throw new TypeError('strings must be well-formed Unicode');
91
+ return value;
92
+ }
93
+ function decode(value) {
94
+ switch (value.type) {
95
+ case 'Null': return null;
96
+ case 'Boolean': case 'String': return value.value;
97
+ case 'Integer': return BigInt(value.value);
98
+ case 'Number': return Buffer.from(value.value, 'hex').readDoubleBE();
99
+ case 'Binary': return Buffer.from(value.value);
100
+ case 'Vector': return new Vector(Buffer.from(value.value));
101
+ case 'Record': return new Record(value.value.table, decode(value.value.key));
102
+ case 'Array': return value.value.map(decode);
103
+ case 'Object': return Object.fromEntries(Object.entries(value.value).map(([k,v]) => [k, decode(v)]));
104
+ default: throw new Error('unsupported native value encoding');
105
+ }
106
+ }
107
+ function unwrap(raw) {
108
+ const report = JSON.parse(raw);
109
+ if (report.version !== 1) throw new Error('unsupported native report version');
110
+ if (report.execution.error) {
111
+ const error = new Error(report.execution.error.message);
112
+ error.code = report.execution.error.code;
113
+ error.transaction = report.transaction;
114
+ if (report.execution.error.migration) {
115
+ const migration = report.execution.error.migration;
116
+ error.migration = {version:BigInt(migration.version),offset:BigInt(migration.offset),cause:migration.cause};
117
+ }
118
+ throw error;
119
+ }
120
+ return report;
121
+ }
122
+ // Parsed wire rows are private to this response. Replacing cells releases their
123
+ // wire representation as decoding proceeds instead of retaining a second rowset.
124
+ function decodeRows(rows) {
125
+ for (const row of rows) {
126
+ for (let i = 0; i < row.length; i++) row[i] = decode(row[i]);
127
+ }
128
+ return rows;
129
+ }
130
+ function decodeBatch(raw) {
131
+ return unwrap(raw).execution.result.map(entry => {
132
+ if (entry.result) entry.result = { ...entry.result,
133
+ rows: decodeRows(entry.result.rows), affected: BigInt(entry.result.affected) };
134
+ return entry;
135
+ });
136
+ }
137
+ function migrationPlan(migrations) {
138
+ const plan = migrations.map(m => {
139
+ if (typeof m.version !== 'bigint' || typeof m.name !== 'string' || typeof m.sql !== 'string') {
140
+ throw new TypeError('migration requires bigint version, string name and SQL');
141
+ }
142
+ return { version: encode(m.version).value, name: m.name, sql: m.sql };
143
+ });
144
+ return plan;
145
+ }
146
+ function resultLimits(limits) {
147
+ if (limits === null || typeof limits !== 'object' || Array.isArray(limits)) throw new TypeError('result limits must be an object');
148
+ for (const key of Object.keys(limits)) {
149
+ if (key !== 'maxRows' && key !== 'maxPayloadBytes') throw new TypeError(`unknown result limit ${key}`);
150
+ }
151
+ return ['maxRows', 'maxPayloadBytes'].map(key => {
152
+ const value = limits[key];
153
+ if (typeof value !== 'bigint') throw new TypeError('result limits require both bigint fields');
154
+ if (value < 0n || value > 18446744073709551615n) throw new RangeError('result limits must fit uint64');
155
+ return value.toString();
156
+ });
157
+ }
158
+ function connectionOptions(options) {
159
+ if (options === null || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('database options must be an object');
160
+ for (const key of Object.keys(options)) {
161
+ if (key !== 'writeBufferLimits') throw new TypeError(`unknown database option ${key}`);
162
+ }
163
+ return options.writeBufferLimits === undefined ? [] : resultLimits(options.writeBufferLimits);
164
+ }
165
+ function integrityLimits(limits) {
166
+ if (limits === null || typeof limits !== 'object' || Array.isArray(limits)) throw new TypeError('integrity limits must be an object');
167
+ const fields = { maxDocuments: '', maxEncodedBytes: '' };
168
+ for (const [key, value] of Object.entries(limits)) {
169
+ if (key !== 'maxDocuments' && key !== 'maxEncodedBytes') throw new TypeError(`unknown integrity limit ${key}`);
170
+ if (value === undefined) continue;
171
+ if (typeof value !== 'bigint') throw new TypeError('integrity limits require bigint');
172
+ if (value < 0n || value > 18446744073709551615n) throw new RangeError('integrity limits must fit uint64');
173
+ fields[key] = value.toString();
174
+ }
175
+ return [fields.maxDocuments, fields.maxEncodedBytes];
176
+ }
177
+ function decodeIntegrity(raw) {
178
+ const report = unwrap(raw);
179
+ return { ...Object.fromEntries(Object.entries(report.execution.result).map(([key, value]) => [key, BigInt(value)])), transaction: report.transaction };
180
+ }
181
+ function decodeProfile(raw) {
182
+ const report = unwrap(raw);
183
+ const { result, metrics } = report.execution.result;
184
+ return { result: { columns: result.columns, rows: decodeRows(result.rows),
185
+ affected: BigInt(result.affected), transaction: report.transaction },
186
+ metrics: Object.fromEntries(Object.entries(metrics).map(([key, value]) => [key, BigInt(value)])) };
187
+ }
188
+ function isRomeDBError(value) {
189
+ if (!(value instanceof Error) || typeof value.code !== 'string' || !/^FDB_[A-Z][A-Z0-9_]*$/.test(value.code)) return false;
190
+ const transaction = value.transaction;
191
+ if (!(transaction === undefined || (transaction !== null && typeof transaction === 'object' &&
192
+ (transaction.before === 'autocommit' || transaction.before === 'active') &&
193
+ (transaction.after === 'autocommit' || transaction.after === 'active')))) return false;
194
+ const migration = value.migration;
195
+ return migration === undefined || (migration !== null && typeof migration === 'object' &&
196
+ typeof migration.version === 'bigint' && migration.version > 0n &&
197
+ typeof migration.offset === 'bigint' && migration.offset >= 0n &&
198
+ migration.cause !== null && typeof migration.cause === 'object' &&
199
+ typeof migration.cause.code === 'string' && /^FDB_[A-Z][A-Z0-9_]*$/.test(migration.cause.code) &&
200
+ typeof migration.cause.message === 'string');
201
+ }
202
+ exports.isRomeDBError = isRomeDBError;
203
+ function cardinalityError(count, transaction) {
204
+ const error = new RangeError(`expected exactly one row, got ${count}`);
205
+ error.code = 'FDB_CARDINALITY';
206
+ error.transaction = transaction;
207
+ return error;
208
+ }
209
+ function closedError(message = 'database is closed') {
210
+ const error = new Error(message);
211
+ error.code = 'FDB_CLOSED';
212
+ return error;
213
+ }
214
+ class Database {
215
+ collection(name) { return collectionClient(this, name, false); }
216
+ #handle; #closed = false;
217
+ constructor(path = ':memory:', options = {}) { this.#handle = new NativeDatabase(path, ...connectionOptions(options)); }
218
+ get #native() {
219
+ if (this.#closed) throw closedError();
220
+ return this.#handle;
221
+ }
222
+ close() { this.#handle.close(); this.#closed = true; }
223
+ execute(sql, parameters = {}) {
224
+ const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
225
+ const report = unwrap(this.#native.execute(sql, JSON.stringify(params)));
226
+ const result = report.execution.result;
227
+ return { columns: result.columns, rows: decodeRows(result.rows), affected: BigInt(result.affected), transaction: report.transaction };
228
+ }
229
+ profileSelect(sql, parameters = {}) {
230
+ const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
231
+ return decodeProfile(this.#native.profileSelect(sql, JSON.stringify(params)));
232
+ }
233
+ selectWithLimits(sql, limits, parameters = {}) {
234
+ return this.profileSelectWithLimits(sql, limits, parameters).result;
235
+ }
236
+ profileSelectWithLimits(sql, limits, parameters = {}) {
237
+ const args = resultLimits(limits);
238
+ const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
239
+ return decodeProfile(this.#native.profileSelectWithLimits(sql, JSON.stringify(params), ...args));
240
+ }
241
+ writeWithResultLimits(sql, limits, parameters = {}) {
242
+ const args = resultLimits(limits);
243
+ const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
244
+ const report = unwrap(this.#native.writeWithResultLimits(sql, JSON.stringify(params), ...args));
245
+ const result = report.execution.result;
246
+ return { columns: result.columns, rows: decodeRows(result.rows), affected: BigInt(result.affected), transaction: report.transaction };
247
+ }
248
+ checkCollectionIntegrity(table, limits = {}) {
249
+ return decodeIntegrity(this.#native.checkCollectionIntegrity(table, ...integrityLimits(limits)));
250
+ }
251
+ executeBatch(script) { return decodeBatch(this.#native.executeBatch(script)); }
252
+ exportDocuments(table, format = 'json') {
253
+ return unwrap(this.#native.exportDocuments(table, format)).execution.result;
254
+ }
255
+ importDocuments(table, input, format = 'json') {
256
+ const report = unwrap(this.#native.importDocuments(table, input, format));
257
+ return { ...report.execution.result, transaction: report.transaction };
258
+ }
259
+ migrate(migrations) {
260
+ const plan = migrationPlan(migrations);
261
+ const report = unwrap(this.#native.migrate(JSON.stringify(plan)));
262
+ return { alreadyApplied: report.execution.result.alreadyApplied,
263
+ applied: report.execution.result.applied.map(BigInt), transaction: report.transaction };
264
+ }
265
+ all(sql, parameters) { return this.execute(sql, parameters).rows; }
266
+ first(sql, parameters) { return this.all(sql, parameters)[0]; }
267
+ exactlyOne(sql, parameters) {
268
+ const { rows, transaction } = this.execute(sql, parameters);
269
+ if (rows.length !== 1) throw cardinalityError(rows.length, transaction);
270
+ return rows[0];
271
+ }
272
+ }
273
+ exports.Database = Database;
274
+ exports.Record = Record;
275
+ exports.Vector = Vector;
276
+
277
+
278
+ const asyncConstruction = Symbol('AsyncDatabase');
279
+ class AsyncDatabase {
280
+ collection(name) { return collectionClient(this, name, true); }
281
+ #interruptKey;
282
+ #worker; #pending = new Map(); #next = 0; #bytes = 0;
283
+ #ready; #readyResolve; #readyReject; #exited;
284
+ #failure; #closing = false; #closePromise; #stopped = false; #shutdownRequested = false;
285
+ constructor(path, token, limits) {
286
+ if (token !== asyncConstruction) throw new TypeError('use AsyncDatabase.open()');
287
+ const { Worker } = require('node:worker_threads');
288
+ this.#ready = new Promise((resolve, reject) => { this.#readyResolve = resolve; this.#readyReject = reject; });
289
+ this.#worker = new Worker(require.resolve('./worker.cjs'), { workerData: { path, limits } });
290
+ this.#exited = new Promise(resolve => this.#worker.once('exit', code => {
291
+ this.#stopped = true;
292
+ if (!this.#closing || this.#pending.size) this.#fail(new Error(`database worker exited (${code})`));
293
+ resolve();
294
+ }));
295
+ this.#worker.on('error', error => this.#fail(error));
296
+ this.#worker.on('messageerror', error => this.#fail(error, true));
297
+ this.#worker.on('message', message => {
298
+ if (message.ready) { this.#interruptKey = message.interruptKey; this.#readyResolve(); return; }
299
+ const pending = this.#pending.get(message.id);
300
+ if (!pending) return;
301
+ this.#pending.delete(message.id); this.#bytes -= pending.bytes; pending.cleanup();
302
+ if (message.error) pending.reject(new Error(message.error.message));
303
+ else pending.resolve(message.result);
304
+ });
305
+ }
306
+ interrupt() { return this.#interruptKey !== undefined && interruptConnection(this.#interruptKey); }
307
+ static async open(path = ':memory:', options = {}) {
308
+ const limits = connectionOptions(options);
309
+ const db = new AsyncDatabase(path, asyncConstruction, limits);
310
+ try { await db.#ready; return db; }
311
+ catch (error) { await db.#exited; throw error; }
312
+ }
313
+ #fail(error, shutdown = false) {
314
+ if (!this.#failure) {
315
+ this.#failure = new Error(`database worker failure: ${error.message}`, { cause: error });
316
+ this.#failure.code = 'FDB_WORKER';
317
+ }
318
+ this.#readyReject(this.#failure);
319
+ for (const pending of this.#pending.values()) { pending.cleanup(); pending.reject(this.#failure); }
320
+ this.#pending.clear(); this.#bytes = 0;
321
+ if (shutdown && !this.#stopped && !this.#shutdownRequested) {
322
+ this.#shutdownRequested = true;
323
+ // The response channel is damaged. Ask the worker to close after the
324
+ // accepted queue; their write outcomes can no longer be inferred here.
325
+ try { this.#worker.postMessage({ id: 0, method: 'close', args: [] }); }
326
+ catch { void this.#worker.terminate(); }
327
+ }
328
+ }
329
+ #request(method, args, closing = false, signal, timeoutMs) {
330
+ if (this.#failure) return Promise.reject(this.#failure);
331
+ if (this.#closing && !closing) return Promise.reject(closedError('database is closing or closed'));
332
+ const bytes = args.reduce((size, arg) => size + Buffer.byteLength(arg), 0);
333
+ if (!closing && (this.#pending.size >= 256 || this.#bytes + bytes > 128 * 1024 * 1024)) {
334
+ const error = new RangeError('database worker queue limit exceeded'); error.code = 'FDB_LIMIT';
335
+ return Promise.reject(error);
336
+ }
337
+ const id = ++this.#next;
338
+ return new Promise((resolve, reject) => {
339
+ let cancellationKey, listener;
340
+ const cleanup = () => {
341
+ listener?.[Symbol.dispose]();
342
+ if (cancellationKey !== undefined) releaseCancellationToken(cancellationKey);
343
+ };
344
+ try {
345
+ if (timeoutMs !== undefined && (!Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 4294967295)) {
346
+ throw new RangeError('timeoutMs must be an integer from 0 through 4294967295');
347
+ }
348
+ if (signal !== undefined || timeoutMs !== undefined) {
349
+ cancellationKey = createCancellationToken(timeoutMs === undefined ? undefined : String(timeoutMs));
350
+ }
351
+ if (signal !== undefined) {
352
+ listener = require('node:events').addAbortListener(signal, () => cancelOperation(cancellationKey));
353
+ if (signal.aborted) cancelOperation(cancellationKey);
354
+ }
355
+ } catch (error) { cleanup(); reject(error); return; }
356
+ this.#pending.set(id, { resolve, reject, bytes, cleanup }); this.#bytes += bytes;
357
+ try { this.#worker.postMessage({ id, method, args, cancellationKey }); }
358
+ catch (error) { this.#pending.delete(id); this.#bytes -= bytes; cleanup(); reject(error); }
359
+ });
360
+ }
361
+ close() {
362
+ if (!this.#closePromise) {
363
+ this.#closing = true;
364
+ this.#closePromise = this.#failure ? this.#exited : this.#request('close', [], true)
365
+ .then(() => this.#exited)
366
+ .catch(async error => {
367
+ this.#fail(error, true);
368
+ await this.#exited;
369
+ throw this.#failure;
370
+ });
371
+ }
372
+ return this.#closePromise;
373
+ }
374
+ async execute(sql, parameters = {}, options = {}) {
375
+ const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
376
+ const report = unwrap(await this.#request('execute', [sql, JSON.stringify(params)], false, options.signal, options.timeoutMs));
377
+ const result = report.execution.result;
378
+ return { columns: result.columns, rows: decodeRows(result.rows), affected: BigInt(result.affected), transaction: report.transaction };
379
+ }
380
+ async profileSelect(sql, parameters = {}, options = {}) {
381
+ const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
382
+ return decodeProfile(await this.#request('profileSelect', [sql, JSON.stringify(params)], false, options.signal, options.timeoutMs));
383
+ }
384
+ async selectWithLimits(sql, limits, parameters = {}, options = {}) {
385
+ return (await this.profileSelectWithLimits(sql, limits, parameters, options)).result;
386
+ }
387
+ async profileSelectWithLimits(sql, limits, parameters = {}, options = {}) {
388
+ const args = resultLimits(limits);
389
+ const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
390
+ return decodeProfile(await this.#request('profileSelectWithLimits', [sql, JSON.stringify(params), ...args], false, options.signal, options.timeoutMs));
391
+ }
392
+ async writeWithResultLimits(sql, limits, parameters = {}, options = {}) {
393
+ const args = resultLimits(limits);
394
+ const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
395
+ const report = unwrap(await this.#request('writeWithResultLimits', [sql, JSON.stringify(params), ...args], false, options.signal, options.timeoutMs));
396
+ const result = report.execution.result;
397
+ return { columns: result.columns, rows: decodeRows(result.rows), affected: BigInt(result.affected), transaction: report.transaction };
398
+ }
399
+ async checkCollectionIntegrity(table, limits = {}, options = {}) {
400
+ return decodeIntegrity(await this.#request('checkCollectionIntegrity', [table, ...integrityLimits(limits)], false, options.signal, options.timeoutMs));
401
+ }
402
+ async executeBatch(script, options = {}) { return decodeBatch(await this.#request('executeBatch', [script], false, options.signal, options.timeoutMs)); }
403
+ async exportDocuments(table, format = 'json', options = {}) {
404
+ return unwrap(await this.#request('exportDocuments', [table, format], false, options.signal, options.timeoutMs)).execution.result;
405
+ }
406
+ async importDocuments(table, input, format = 'json', options = {}) {
407
+ const report = unwrap(await this.#request('importDocuments', [table, input, format], false, options.signal, options.timeoutMs));
408
+ return { ...report.execution.result, transaction: report.transaction };
409
+ }
410
+ async migrate(migrations, options = {}) {
411
+ const report = unwrap(await this.#request('migrate', [JSON.stringify(migrationPlan(migrations))], false, options.signal, options.timeoutMs));
412
+ return { alreadyApplied: report.execution.result.alreadyApplied,
413
+ applied: report.execution.result.applied.map(BigInt), transaction: report.transaction };
414
+ }
415
+ async all(sql, parameters, options) { return (await this.execute(sql, parameters, options)).rows; }
416
+ async first(sql, parameters, options) { return (await this.all(sql, parameters, options))[0]; }
417
+ async exactlyOne(sql, parameters, options) {
418
+ const { rows, transaction } = await this.execute(sql, parameters, options);
419
+ if (rows.length !== 1) throw cardinalityError(rows.length, transaction);
420
+ return rows[0];
421
+ }
422
+ }
423
+ exports.AsyncDatabase = AsyncDatabase;
424
+
425
+ // Collection identifiers are quoted; document values and record keys are bound.
426
+ function collectionClient(db, name, asynchronous) {
427
+ if (typeof name !== 'string' || !name.length || name.includes('\0')) throw new TypeError('collection name must be a nonempty string without NUL');
428
+ const quote = value => '"' + value.replaceAll('"', '""') + '"';
429
+ const table = quote(name);
430
+ const record = key => new Record(name, key);
431
+ const document = value => {
432
+ if (!value || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw new TypeError('expected a document object');
433
+ return value;
434
+ };
435
+ const fields = value => {
436
+ document(value);
437
+ if (Object.hasOwn(value, 'id')) throw new TypeError('pass the record key separately; document must omit id');
438
+ return Object.entries(value);
439
+ };
440
+ const run = (sql, params, options, many = false) => {
441
+ const project = result => many ? result.rows.map(row => row[0]) : result.rows[0]?.[0];
442
+ const check = info => {
443
+ if (info.rows[0]?.[0]?.model !== 'document') throw new TypeError('collection methods require a document collection');
444
+ };
445
+ const missing = error => { if (error.code !== 'FDB_NOT_FOUND') throw error; };
446
+ if (asynchronous) return (async () => {
447
+ try { check(await db.execute(`INFO FOR TABLE ${table}`, {}, options)); } catch (error) { missing(error); }
448
+ return project(await db.execute(sql, params, options));
449
+ })();
450
+ try { check(db.execute(`INFO FOR TABLE ${table}`)); } catch (error) { missing(error); }
451
+ return project(db.execute(sql, params));
452
+ };
453
+ return Object.freeze({
454
+ all(options) { return run(`SELECT * FROM ${table}`, {}, options, true); },
455
+ get(key, options) { return run(`SELECT * FROM ${table} WHERE id=$id`, {$id:record(key)}, options); },
456
+ insert(value, options) { return run(`INSERT INTO ${table} DOCUMENT $doc RETURNING *`, {$doc:document(value)}, options); },
457
+ upsert(key, value, options) {
458
+ const params = {$id:record(key)};
459
+ const body = fields(value).map(([field, v], i) => { params[`$v${i}`] = v; return `${quote(field)}:$v${i}`; });
460
+ return run(`UPSERT ${table} {id:$id${body.length ? ',' + body.join(',') : ''}} RETURNING *`, params, options);
461
+ },
462
+ merge(key, value, options) {
463
+ const params = {$id:record(key)};
464
+ const sets = fields(value).map(([field, v], i) => { params[`$v${i}`] = v; return `${quote(field)}=$v${i}`; });
465
+ return sets.length ? run(`UPDATE ${table} SET ${sets.join(',')} WHERE id=$id RETURNING *`, params, options)
466
+ : run(`SELECT * FROM ${table} WHERE id=$id`, params, options);
467
+ },
468
+ delete(key, options) { return run(`DELETE FROM ${table} WHERE id=$id RETURNING *`, {$id:record(key)}, options); },
469
+ });
470
+ }