@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.
- package/LICENSE.md +20 -0
- package/README.md +605 -2
- package/RUST-LIBRARY-NOTICES.html +7328 -0
- package/THIRD_PARTY_CRATE_NOTICES.md +16853 -0
- package/THIRD_PARTY_NOTICES.md +1077 -0
- package/client-internal.cjs +2 -0
- package/client.cjs +397 -0
- package/client.d.ts +79 -0
- package/client.mjs +410 -0
- package/embedded-client.cjs +180 -0
- package/embedded-client.d.ts +19 -0
- package/embedded-worker.cjs +96 -0
- package/index.cjs +470 -0
- package/index.d.ts +109 -0
- package/native.cjs +20 -0
- package/package.json +69 -4
- package/request-journal.cjs +47 -0
- package/romedb.node +4 -0
- package/values.cjs +211 -0
- package/values.d.ts +15 -0
- package/values.mjs +220 -0
- package/worker.cjs +19 -0
package/client.mjs
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// Generated by build-esm.cjs from client.cjs. Edit client.cjs, then run `node build-esm.cjs`.
|
|
2
|
+
import * as module0 from './values.mjs';
|
|
3
|
+
|
|
4
|
+
const commonjs = { exports: {} };
|
|
5
|
+
(function (module, exports, require) {
|
|
6
|
+
'use strict';
|
|
7
|
+
// Web APIs only: this file also runs in Workers and browsers through client.mjs.
|
|
8
|
+
const { Float64, Record, Vector, encodeValue, decodeValue, plainObject, text } = require('./values.cjs');
|
|
9
|
+
|
|
10
|
+
const MAX_REQUEST = 65_536;
|
|
11
|
+
const MAX_CLOUD_REQUEST = 1_048_576;
|
|
12
|
+
const CLOUD_QUERY_VERSION = 3;
|
|
13
|
+
const MAX_RESPONSE = 524_288;
|
|
14
|
+
const MAX_SEQUENCE = Number.MAX_SAFE_INTEGER - 1;
|
|
15
|
+
const construction = Symbol('RemoteDatabase');
|
|
16
|
+
const warmAtConnect = Symbol('warmAtConnect');
|
|
17
|
+
const WARM_STATES = new Set(['ready', 'starting', 'skipped']);
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
|
|
20
|
+
class ClientError extends Error {
|
|
21
|
+
constructor(code, message, outcome, details = {}) {
|
|
22
|
+
const { cause, ...fields } = details;
|
|
23
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
24
|
+
this.name = 'ClientError';
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.outcome = outcome;
|
|
27
|
+
Object.assign(this, fields);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function connect(options) {
|
|
32
|
+
const db = new RemoteDatabase(options, construction);
|
|
33
|
+
db[warmAtConnect]();
|
|
34
|
+
if (db.deferred) return db;
|
|
35
|
+
try { await db.info(); return db; }
|
|
36
|
+
catch (error) { await db.close(); throw error; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class RemoteDatabase {
|
|
40
|
+
#url; #token; #fetch; #timeout; #sequence = 0; #id; #capabilities; #transport;
|
|
41
|
+
#closed = false; #active = new Set(); #inflightBytes = 0; #sequenceKnown = false; #deferred = false; #cloud = false; #warm = false; #loopbackOrganization = false;
|
|
42
|
+
constructor(options, key, transport) {
|
|
43
|
+
if (key !== construction) throw new TypeError('use connect()');
|
|
44
|
+
if (!plainObject(options)) throw new TypeError('connection options require an object');
|
|
45
|
+
this.#url = transport ? normalizeFileUrl(options.url) : normalizeUrl(options.url);
|
|
46
|
+
this.#transport = transport;
|
|
47
|
+
if (typeof options.token !== 'string' || !/^[\x21-\x7e]{1,4096}$/.test(options.token)) throw new TypeError('token requires a bounded bearer credential');
|
|
48
|
+
this.#token = options.token;
|
|
49
|
+
const fetch = options.fetch ?? globalThis.fetch;
|
|
50
|
+
if (typeof fetch !== 'function') throw new TypeError('fetch must be a function');
|
|
51
|
+
// Workers refuse fetch called on another object, so the default keeps globalThis.
|
|
52
|
+
this.#fetch = options.fetch ?? fetch.bind(globalThis);
|
|
53
|
+
this.#timeout = timeout(options.timeoutMs ?? 20_000);
|
|
54
|
+
if (options.deferInfo !== undefined && typeof options.deferInfo !== 'boolean') throw new TypeError('deferInfo requires a boolean');
|
|
55
|
+
if (options.warm !== undefined && typeof options.warm !== 'boolean') throw new TypeError('warm requires a boolean');
|
|
56
|
+
const cloudId = transport ? undefined : /\/databases\/([0-9a-f-]{36})$/.exec(this.#url)?.[1];
|
|
57
|
+
// A loopback organization URL is a local service directory or Cloud in development;
|
|
58
|
+
// the details reply tells which, so these connections always read details first.
|
|
59
|
+
this.#loopbackOrganization = cloudId !== undefined && loopbackHost(new URL(this.#url).hostname);
|
|
60
|
+
this.#cloud = cloudId !== undefined && !this.#loopbackOrganization;
|
|
61
|
+
if (cloudId === undefined && options.deferInfo === true) throw new TypeError('deferInfo applies only to Cloud database URLs');
|
|
62
|
+
if (cloudId === undefined && options.warm === true) throw new TypeError('warm applies only to Cloud database URLs');
|
|
63
|
+
// Cloud database URLs carry the database id and have documented capabilities,
|
|
64
|
+
// so the details request waits until a write needs a current sequence.
|
|
65
|
+
if (this.#cloud && options.deferInfo !== false) {
|
|
66
|
+
this.#id = cloudId; this.#capabilities = capabilities(this.#url, {}, false); this.#deferred = true;
|
|
67
|
+
}
|
|
68
|
+
this.#warm = this.#cloud && options.warm !== false;
|
|
69
|
+
}
|
|
70
|
+
[warmAtConnect]() {
|
|
71
|
+
if (this.#warm) this.warm().catch(() => {});
|
|
72
|
+
}
|
|
73
|
+
get deferred() { return this.#deferred; }
|
|
74
|
+
get url() { return this.#url; }
|
|
75
|
+
get cloud() { return this.#cloud; }
|
|
76
|
+
get sequence() { return this.#sequence; }
|
|
77
|
+
get sequenceKnown() { return this.#sequenceKnown; }
|
|
78
|
+
get databaseId() { return this.#id; }
|
|
79
|
+
async info(options = {}) {
|
|
80
|
+
const info = await this.#request('', undefined, options, false);
|
|
81
|
+
if (!plainObject(info) || typeof info.id !== 'string' || !/^[A-Za-z0-9_-]{1,80}$/.test(info.id) || !sequence(info.sequence, true) || (this.#id !== undefined && this.#id !== info.id)) {
|
|
82
|
+
throw new ClientError('FDB_PROTOCOL', 'Invalid database details or changed database identity', 'read-failed');
|
|
83
|
+
}
|
|
84
|
+
if (this.#loopbackOrganization) this.#cloud = info.serviceVersion === undefined;
|
|
85
|
+
const supported = capabilities(this.#url, info, !this.#cloud);
|
|
86
|
+
this.#id = info.id;
|
|
87
|
+
this.#sequence = Math.max(this.#sequence, info.sequence);
|
|
88
|
+
this.#sequenceKnown = true;
|
|
89
|
+
this.#capabilities = supported;
|
|
90
|
+
return { ...info, capabilities: supported };
|
|
91
|
+
}
|
|
92
|
+
async warm(options = {}) {
|
|
93
|
+
this.#assertOpen();
|
|
94
|
+
if (!this.#cloud) throw new ClientError('FDB_UNSUPPORTED', 'warm applies only to Cloud database URLs', 'not-sent');
|
|
95
|
+
const reply = await this.#request('/warm', '{}', options, false);
|
|
96
|
+
if (!plainObject(reply) || !WARM_STATES.has(reply.state)) throw new ClientError('FDB_PROTOCOL', 'Invalid warm reply', 'read-failed');
|
|
97
|
+
return { state: reply.state };
|
|
98
|
+
}
|
|
99
|
+
async read(statements, options = {}) {
|
|
100
|
+
this.#assertOpen();
|
|
101
|
+
if (this.#capabilities.readVersion !== 2) throw new ClientError('FDB_UNSUPPORTED', 'Database does not support fresh reads', 'not-sent');
|
|
102
|
+
if (options.ifSequence !== undefined) throw new TypeError('fresh reads do not accept ifSequence');
|
|
103
|
+
const afterSequence = options.afterSequence ?? this.#sequence;
|
|
104
|
+
checkSequence(afterSequence);
|
|
105
|
+
const workLimits = encodeWorkLimits(options.workLimits);
|
|
106
|
+
const queryVersion = this.#queryVersion();
|
|
107
|
+
const body = JSON.stringify({ readVersion: 2, requestId: crypto.randomUUID(), afterSequence, statements: encodeStatements(statements, queryVersion), ...(workLimits === undefined ? {} : { workLimits }) });
|
|
108
|
+
checkBodySize(body, queryVersion);
|
|
109
|
+
const request = JSON.parse(body);
|
|
110
|
+
const reply = await this.#request('/read', body, options, false, undefined, queryVersion);
|
|
111
|
+
const result = confirmReply(reply, request, true);
|
|
112
|
+
this.#sequence = Math.max(this.#sequence, result.sequence);
|
|
113
|
+
this.#sequenceKnown = true;
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
prepareWrite(statements, options = {}) {
|
|
117
|
+
this.#assertOpen();
|
|
118
|
+
if (!this.#sequenceKnown && this.#deferred && options.afterSequence === undefined) throw new ClientError('FDB_SEQUENCE_UNKNOWN', 'Read, call info() or pass afterSequence before preparing a write on a deferred connection', 'not-sent');
|
|
119
|
+
if (this.#capabilities.atomicWrites !== true || this.#capabilities.replayWindow !== 64) throw new ClientError('FDB_UNSUPPORTED', 'Database does not support atomic writes with recovery', 'not-sent');
|
|
120
|
+
const afterSequence = options.afterSequence ?? this.#sequence;
|
|
121
|
+
checkSequence(afterSequence);
|
|
122
|
+
if (options.ifSequence !== undefined) {
|
|
123
|
+
if (this.#capabilities.exactSequenceCondition !== true) throw new ClientError('FDB_UNSUPPORTED', 'Database does not support exact sequence conditions', 'not-sent');
|
|
124
|
+
checkSequence(options.ifSequence);
|
|
125
|
+
if (options.ifSequence < afterSequence) throw new RangeError('ifSequence must be at least afterSequence');
|
|
126
|
+
}
|
|
127
|
+
const requestId = options.requestId ?? crypto.randomUUID();
|
|
128
|
+
if (typeof requestId !== 'string' || !/^[A-Za-z0-9_-]{1,80}$/.test(requestId)) throw new TypeError('requestId requires 1..80 letters, digits, underscores or hyphens');
|
|
129
|
+
const workLimits = encodeWorkLimits(options.workLimits);
|
|
130
|
+
const queryVersion = this.#queryVersion();
|
|
131
|
+
const body = JSON.stringify({ requestId, afterSequence, ...(options.ifSequence === undefined ? {} : { ifSequence: options.ifSequence }), statements: encodeStatements(statements, queryVersion), ...(workLimits === undefined ? {} : { workLimits }) });
|
|
132
|
+
checkBodySize(body, queryVersion);
|
|
133
|
+
return Object.freeze({ version: 2, url: this.#url, databaseId: this.#id, queryVersion, body });
|
|
134
|
+
}
|
|
135
|
+
async submit(prepared, options = {}) {
|
|
136
|
+
this.#assertOpen();
|
|
137
|
+
const request = validatePrepared(prepared);
|
|
138
|
+
if (this.#capabilities.atomicWrites !== true || this.#capabilities.replayWindow !== 64 || (request.ifSequence !== undefined && this.#capabilities.exactSequenceCondition !== true)) throw new ClientError('FDB_UNSUPPORTED', 'Database does not support this write contract', 'not-sent', { request: prepared });
|
|
139
|
+
if (prepared.url !== this.#url || prepared.databaseId !== this.#id) throw new ClientError('FDB_REQUEST_IDENTITY', 'Saved request belongs to another database', 'not-sent');
|
|
140
|
+
try {
|
|
141
|
+
const reply = await this.#request('/query', prepared.body, options, true, prepared, preparedQueryVersion(prepared));
|
|
142
|
+
const result = confirmReply(reply, request, false, prepared);
|
|
143
|
+
this.#sequence = Math.max(this.#sequence, result.sequence);
|
|
144
|
+
this.#sequenceKnown = true;
|
|
145
|
+
return result;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof ClientError) {
|
|
148
|
+
if (error.outcome === 'rolled-back') this.#sequence = Math.max(this.#sequence, error.sequence);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
throw new ClientError('FDB_PROTOCOL', 'Write response could not be confirmed; recover the saved request', 'unknown', { request: prepared });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async recover(prepared, options = {}) {
|
|
155
|
+
try { return await this.submit(prepared, options); }
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (error instanceof ClientError && error.outcome === 'not-sent') throw new ClientError(error.code, 'Recovery did not confirm the earlier write; keep the saved request', 'unknown', { request: prepared });
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async writeAtomic(statements, options = {}) {
|
|
162
|
+
if (!this.#sequenceKnown && this.#deferred && options.afterSequence === undefined) await this.info(options);
|
|
163
|
+
return this.submit(this.prepareWrite(statements, options), options);
|
|
164
|
+
}
|
|
165
|
+
async close() {
|
|
166
|
+
this.#closed = true;
|
|
167
|
+
for (const controller of this.#active) controller.abort();
|
|
168
|
+
this.#token = '';
|
|
169
|
+
}
|
|
170
|
+
#assertOpen() {
|
|
171
|
+
if (this.#closed) throw new ClientError('FDB_CLOSED', 'Database client is closed', 'not-sent');
|
|
172
|
+
}
|
|
173
|
+
#queryVersion() { return this.#cloud ? CLOUD_QUERY_VERSION : 2; }
|
|
174
|
+
async #request(path, body, options, writing, prepared, queryVersion = 2) {
|
|
175
|
+
this.#assertOpen();
|
|
176
|
+
const duration = timeout(options.timeoutMs ?? this.#timeout);
|
|
177
|
+
if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) throw new TypeError('signal must be an AbortSignal');
|
|
178
|
+
if (options.signal?.aborted) throw new ClientError('FDB_CANCELLED', 'Request cancelled before dispatch', 'not-sent', prepared ? { request: prepared } : {});
|
|
179
|
+
const bodyBytes = body === undefined ? 0 : byteLength(body);
|
|
180
|
+
if (this.#active.size >= 32 || this.#inflightBytes + bodyBytes > 2_097_152) throw new ClientError('FDB_LIMIT', 'Client request limit reached', 'not-sent', prepared ? { request: prepared } : {});
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const abort = () => controller.abort();
|
|
183
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
184
|
+
const timer = setTimeout(abort, duration);
|
|
185
|
+
this.#active.add(controller);
|
|
186
|
+
this.#inflightBytes += bodyBytes;
|
|
187
|
+
const credential = this.#token;
|
|
188
|
+
const fetch = this.#fetch;
|
|
189
|
+
let status;
|
|
190
|
+
try {
|
|
191
|
+
if (this.#transport) return await this.#transport(path, body, controller.signal, duration);
|
|
192
|
+
const send = () => fetch(this.#url + path, {
|
|
193
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
194
|
+
headers: { authorization: `Bearer ${credential}`, accept: 'application/json', ...(body === undefined ? {} : { 'content-type': 'application/json' }), ...(this.#id === undefined ? {} : { 'x-romedb-database-id': prepared?.databaseId ?? this.#id }),
|
|
195
|
+
...(queryVersion === CLOUD_QUERY_VERSION && (path === '/query' || path === '/read') ? { 'x-romedb-query-version': String(queryVersion) } : {}) },
|
|
196
|
+
...(body === undefined ? {} : { body }),
|
|
197
|
+
// Workers have no redirect: 'error'; a redirect is refused below, never followed with the credential.
|
|
198
|
+
redirect: 'manual', signal: controller.signal,
|
|
199
|
+
});
|
|
200
|
+
let response = await send();
|
|
201
|
+
if (await queueRefused(response)) {
|
|
202
|
+
await pause(queueRetryDelay(response), controller.signal);
|
|
203
|
+
response = await send();
|
|
204
|
+
}
|
|
205
|
+
status = response.status;
|
|
206
|
+
if (response.type === 'opaqueredirect' || (status >= 300 && status < 400)) {
|
|
207
|
+
await response.body?.cancel().catch(() => {});
|
|
208
|
+
throw new ClientError('FDB_PROTOCOL', 'Database URL redirected; redirects are never followed with a credential', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
|
|
209
|
+
}
|
|
210
|
+
const retryAfterMs = retryAfter(response.headers.get('retry-after'));
|
|
211
|
+
const replyVersion = response.headers.get('x-romedb-query-version');
|
|
212
|
+
if ((path === '/query' || path === '/read') && replyVersion !== null && replyVersion !== '2' && replyVersion !== String(queryVersion)) {
|
|
213
|
+
await response.body?.cancel();
|
|
214
|
+
throw new ClientError('FDB_PROTOCOL', 'Unsupported database query response version', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
|
|
215
|
+
}
|
|
216
|
+
const reply = await boundedJson(response, MAX_RESPONSE);
|
|
217
|
+
if (plainObject(reply?.error) && typeof reply.error.message === 'string') reply.error.message = redactMessage(reply.error.message, credential);
|
|
218
|
+
if (!response.ok && !(writing && reply?.requestId && reply?.sequence !== undefined)) {
|
|
219
|
+
const code = plainObject(reply?.error) && typeof reply.error.code === 'string' && /^FDB_[A-Z0-9_]{1,80}$/.test(reply.error.code) ? reply.error.code : httpCode(status);
|
|
220
|
+
throw new ClientError(code, safeMessage(reply, credential), writing ? 'unknown' : 'read-failed', { status, ...(retryAfterMs === undefined ? {} : { retryAfterMs }), ...(prepared ? { request: prepared } : {}) });
|
|
221
|
+
}
|
|
222
|
+
return reply;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error instanceof ClientError) {
|
|
225
|
+
const outcome = writing
|
|
226
|
+
? this.#transport && error.outcome === 'not-sent' ? 'not-sent' : 'unknown'
|
|
227
|
+
: error.outcome === 'not-sent' ? 'not-sent' : 'read-failed';
|
|
228
|
+
const message = this.#transport ? error.message.slice(0, 2048) : redactMessage(error.message, credential);
|
|
229
|
+
throw new ClientError(error.code, message, outcome, {
|
|
230
|
+
...(error.status === undefined ? {} : { status: error.status }),
|
|
231
|
+
...(error.retryAfterMs === undefined ? {} : { retryAfterMs: error.retryAfterMs }),
|
|
232
|
+
...(prepared ? { request: prepared } : {}),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const code = controller.signal.aborted ? 'FDB_CANCELLED' : status === undefined ? 'FDB_TRANSPORT' : 'FDB_PROTOCOL';
|
|
236
|
+
throw new ClientError(code, writing ? 'Write was not confirmed; recover the saved request' : 'Database request failed', writing ? 'unknown' : 'read-failed', { ...(status === undefined ? {} : { status }), ...(prepared ? { request: prepared } : {}), cause: error });
|
|
237
|
+
} finally {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
options.signal?.removeEventListener('abort', abort);
|
|
240
|
+
this.#active.delete(controller);
|
|
241
|
+
this.#inflightBytes -= bodyBytes;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
require('./client-internal.cjs').createEmbeddedClient = async (options, transport) => {
|
|
247
|
+
const database = new RemoteDatabase({ ...options, token: 'embedded' }, construction, transport);
|
|
248
|
+
try { await database.info(); return database; }
|
|
249
|
+
catch (error) { await database.close(); throw error; }
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
function normalizeFileUrl(input) {
|
|
253
|
+
if (typeof input !== 'string' || input.length > 4096) throw new TypeError('file URL requires a bounded local path');
|
|
254
|
+
const url = new URL(input);
|
|
255
|
+
if (url.protocol !== 'file:' || url.hostname || url.search || url.hash || url.pathname === '/' || /%2f|%5c|%00/i.test(url.pathname)) throw new TypeError('use a local file URL without a query or fragment');
|
|
256
|
+
return url.href;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function normalizeUrl(input) {
|
|
260
|
+
if (typeof input !== 'string' || input.length > 4096) throw new TypeError('url requires a bounded database URL');
|
|
261
|
+
// romedb:// is an alias for https://; the same checks apply after the rewrite.
|
|
262
|
+
const url = new URL(input.startsWith('romedb://') ? 'https://' + input.slice('romedb://'.length) : input);
|
|
263
|
+
const loopback = loopbackHost(url.hostname);
|
|
264
|
+
if (url.username || url.password || url.search || url.hash || (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback))) throw new TypeError('use HTTPS or loopback HTTP without URL credentials, query or fragment');
|
|
265
|
+
let path = url.pathname.replace(/\/$/, '').replace(/\/(?:query|read)$/, '');
|
|
266
|
+
if (loopback && !path) path = '/v1/database';
|
|
267
|
+
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
|
|
268
|
+
if (!(loopback && path === '/v1/database') && !new RegExp(`^/v1/organizations/${uuid}/databases/${uuid}$`).test(path)) throw new TypeError('use a local service URL or stable organization-scoped Cloud database URL');
|
|
269
|
+
return url.origin + path;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function encodeStatements(statements, queryVersion = 2) {
|
|
273
|
+
if (!Array.isArray(statements) || statements.length < 1 || statements.length > 32) throw new RangeError('request requires 1..32 statements');
|
|
274
|
+
return statements.map(statement => {
|
|
275
|
+
if (!plainObject(statement) || Object.keys(statement).some(key => !['sql', 'params'].includes(key)) || typeof statement.sql !== 'string' || !statement.sql.trim()) throw new TypeError('statement requires SQL and optional params');
|
|
276
|
+
if (statement.params === undefined) return { sql: text(statement.sql) };
|
|
277
|
+
if (!plainObject(statement.params)) throw new TypeError('params require an object');
|
|
278
|
+
return { sql: text(statement.sql), params: Object.fromEntries(Object.entries(statement.params).map(([key, value]) => [text(key), encodeValue(value, 0, queryVersion === CLOUD_QUERY_VERSION)])) };
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function validatePrepared(prepared) {
|
|
283
|
+
const fields = prepared?.version === 1 ? 'body,databaseId,url,version' : 'body,databaseId,queryVersion,url,version';
|
|
284
|
+
if (!plainObject(prepared) || Object.keys(prepared).sort().join(',') !== fields || ![1, 2].includes(prepared.version) ||
|
|
285
|
+
(prepared.version === 2 && ![2, CLOUD_QUERY_VERSION].includes(prepared.queryVersion)) || typeof prepared.databaseId !== 'string' || !prepared.databaseId ||
|
|
286
|
+
(prepared.url?.startsWith('file:') ? normalizeFileUrl(prepared.url) : normalizeUrl(prepared.url)) !== prepared.url || typeof prepared.body !== 'string') throw new TypeError('invalid saved request');
|
|
287
|
+
checkBodySize(prepared.body, preparedQueryVersion(prepared));
|
|
288
|
+
const body = JSON.parse(prepared.body);
|
|
289
|
+
if (!plainObject(body) || Object.keys(body).some(key => !['requestId', 'afterSequence', 'ifSequence', 'statements', 'workLimits'].includes(key)) || typeof body.requestId !== 'string' || !/^[A-Za-z0-9_-]{1,80}$/.test(body.requestId)) throw new TypeError('invalid saved request identity');
|
|
290
|
+
encodeWorkLimits(body.workLimits);
|
|
291
|
+
checkSequence(body.afterSequence);
|
|
292
|
+
if (body.ifSequence !== undefined) {
|
|
293
|
+
checkSequence(body.ifSequence);
|
|
294
|
+
if (body.ifSequence < body.afterSequence) throw new RangeError('invalid saved sequence condition');
|
|
295
|
+
}
|
|
296
|
+
if (!Array.isArray(body.statements) || body.statements.length < 1 || body.statements.length > 32) throw new TypeError('invalid saved statements');
|
|
297
|
+
for (const statement of body.statements) {
|
|
298
|
+
if (!plainObject(statement) || Object.keys(statement).some(key => !['sql', 'params'].includes(key)) || typeof statement.sql !== 'string' || !statement.sql.trim() || (statement.params !== undefined && !plainObject(statement.params))) throw new TypeError('invalid saved statement');
|
|
299
|
+
for (const value of Object.values(statement.params ?? {})) decodeValue(value);
|
|
300
|
+
}
|
|
301
|
+
return body;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function confirmReply(reply, request, reading, prepared) {
|
|
305
|
+
const invalid = () => new ClientError('FDB_PROTOCOL', reading ? 'Read response identity or results differ' : 'Write response could not be confirmed; recover the saved request', reading ? 'read-failed' : 'unknown', prepared ? { request: prepared } : {});
|
|
306
|
+
if (!plainObject(reply) || reply.requestId !== request.requestId || !sequence(reply.sequence, true) || (reading ? reply.readVersion !== 2 || reply.sequence < request.afterSequence : reply.readVersion !== undefined || reply.sequence <= request.afterSequence || reply.sequence - request.afterSequence > 64)) throw invalid();
|
|
307
|
+
if (plainObject(reply.error) && typeof reply.error.code === 'string' && /^FDB_[A-Z0-9_]+$/.test(reply.error.code) && typeof reply.error.message === 'string' && reply.results === undefined) throw new ClientError(reply.error.code, reply.error.message.slice(0, 2048), reading ? 'read-failed' : 'rolled-back', { sequence: reply.sequence, request: prepared });
|
|
308
|
+
if (!Array.isArray(reply.results) || reply.results.length !== request.statements.length || reply.error !== undefined || byteLength(JSON.stringify(reply.results)) > 262_144) throw invalid();
|
|
309
|
+
try {
|
|
310
|
+
const results = reply.results.map(result => {
|
|
311
|
+
if (!plainObject(result) || !Array.isArray(result.columns) || result.columns.some(name => typeof name !== 'string') || !Array.isArray(result.rows) || result.rows.length > 1000 || typeof result.affected !== 'string' || !/^(?:0|[1-9][0-9]{0,19})$/.test(result.affected) || BigInt(result.affected) > (1n << 64n) - 1n) throw invalid();
|
|
312
|
+
return { columns: result.columns, rows: result.rows.map(row => {
|
|
313
|
+
if (!Array.isArray(row) || row.length !== result.columns.length) throw invalid();
|
|
314
|
+
return row.map(value => decodeValue(value));
|
|
315
|
+
}), affected: BigInt(result.affected) };
|
|
316
|
+
});
|
|
317
|
+
return { requestId: reply.requestId, sequence: reply.sequence, results, ...(reading ? { readVersion: 2 } : {}), ...(reply.executionWork === undefined ? {} : { executionWork: reply.executionWork }) };
|
|
318
|
+
} catch { throw invalid(); }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function boundedJson(response, limit) {
|
|
322
|
+
if (!/^application\/json(?:\s*;|$)/i.test(response.headers.get('content-type') ?? '')) throw new TypeError('response requires JSON');
|
|
323
|
+
if (!response.body) throw new TypeError('response body is missing');
|
|
324
|
+
const reader = response.body.getReader();
|
|
325
|
+
const chunks = [];
|
|
326
|
+
let length = 0;
|
|
327
|
+
try {
|
|
328
|
+
for (;;) {
|
|
329
|
+
const { done, value } = await reader.read();
|
|
330
|
+
if (done) break;
|
|
331
|
+
length += value.byteLength;
|
|
332
|
+
if (length > limit) throw new RangeError('response exceeds client byte limit');
|
|
333
|
+
chunks.push(value);
|
|
334
|
+
}
|
|
335
|
+
const bytes = new Uint8Array(length);
|
|
336
|
+
let offset = 0;
|
|
337
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
338
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
339
|
+
} finally { await reader.cancel().catch(() => {}); }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function loopbackHost(hostname) { return ['127.0.0.1', 'localhost', '[::1]'].includes(hostname); }
|
|
343
|
+
|
|
344
|
+
function capabilities(url, info, local) {
|
|
345
|
+
if (local) {
|
|
346
|
+
if ((url.startsWith('file:') ? info.adapterVersion : info.serviceVersion) !== 1 || !plainObject(info.capabilities) || !plainObject(info.limits) || info.capabilities.readVersion !== 2 || typeof info.capabilities.atomicWrites !== 'boolean' || typeof info.capabilities.exactSequenceCondition !== 'boolean' || (info.capabilities.atomicWrites && info.capabilities.replayWindow !== 64)) throw new ClientError('FDB_PROTOCOL', 'Unsupported database client contract', 'read-failed');
|
|
347
|
+
return { ...info.capabilities, provenance: 'negotiated' };
|
|
348
|
+
}
|
|
349
|
+
return { provenance: 'documented', readVersion: 2, atomicWrites: true, replayWindow: 64, exactSequenceCondition: true, negotiatedLimits: false };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function sequence(value, reply = false) { return Number.isSafeInteger(value) && value >= 0 && value <= (reply ? MAX_SEQUENCE + 1 : MAX_SEQUENCE); }
|
|
353
|
+
function encodeWorkLimits(value) {
|
|
354
|
+
if (value === undefined) return undefined;
|
|
355
|
+
const ceilings = { rowsRead: 1_000_000, rowMutations: 100_000, vmSteps: 10_000_000 };
|
|
356
|
+
if (!plainObject(value)) throw new TypeError('workLimits require an object');
|
|
357
|
+
for (const [name, limit] of Object.entries(value)) {
|
|
358
|
+
if (!Object.hasOwn(ceilings, name) || !Number.isSafeInteger(limit) || limit < 0 || limit > ceilings[name]) throw new RangeError('workLimits require nonnegative integers within the service ceilings');
|
|
359
|
+
}
|
|
360
|
+
return Object.fromEntries(Object.keys(ceilings).filter(key => Object.hasOwn(value, key)).map(key => [key, value[key]]));
|
|
361
|
+
}
|
|
362
|
+
function checkSequence(value) { if (!sequence(value)) throw new RangeError('sequence requires a nonnegative safe integer below Number.MAX_SAFE_INTEGER'); }
|
|
363
|
+
function timeout(value) { if (!Number.isInteger(value) || value < 1 || value > 120_000) throw new RangeError('timeoutMs must be 1..120000'); return value; }
|
|
364
|
+
function byteLength(text) { return encoder.encode(text).byteLength; }
|
|
365
|
+
function checkBodySize(body, queryVersion = 2) {
|
|
366
|
+
if (byteLength(body) > (queryVersion === CLOUD_QUERY_VERSION ? MAX_CLOUD_REQUEST : MAX_REQUEST))
|
|
367
|
+
throw new RangeError(queryVersion === CLOUD_QUERY_VERSION ? 'request exceeds 1 MiB' : 'request exceeds 64 KiB');
|
|
368
|
+
}
|
|
369
|
+
function preparedQueryVersion(prepared) { return prepared.version === 1 ? 2 : prepared.queryVersion; }
|
|
370
|
+
// Cloud refuses a queued request with 503 query_queue_* before dispatching it,
|
|
371
|
+
// so nothing ran and one retry is safe for reads and writes alike.
|
|
372
|
+
async function queueRefused(response) {
|
|
373
|
+
if (response.status !== 503) return false;
|
|
374
|
+
try { return /^query_queue_[a-z_]{1,40}$/.test((await response.clone().json())?.code ?? ''); } catch { return false; }
|
|
375
|
+
}
|
|
376
|
+
function queueRetryDelay(response) {
|
|
377
|
+
const header = response.headers.get('retry-after');
|
|
378
|
+
return header === null ? 0 : Math.min(retryAfter(header) ?? 1000, 2000);
|
|
379
|
+
}
|
|
380
|
+
// Retry-After holds delay seconds or an HTTP date.
|
|
381
|
+
function retryAfter(value) {
|
|
382
|
+
if (value === null) return undefined;
|
|
383
|
+
if (/^\s*\d{1,9}(?:\.\d+)?\s*$/.test(value)) return Math.round(Number(value) * 1000);
|
|
384
|
+
const date = /GMT\s*$/.test(value) ? Date.parse(value) : NaN;
|
|
385
|
+
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
386
|
+
}
|
|
387
|
+
function pause(ms, signal) {
|
|
388
|
+
return new Promise((resolve, reject) => {
|
|
389
|
+
if (signal.aborted) { reject(signal.reason); return; }
|
|
390
|
+
const timer = setTimeout(() => { signal.removeEventListener('abort', stop); resolve(); }, ms);
|
|
391
|
+
const stop = () => { clearTimeout(timer); reject(signal.reason); };
|
|
392
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function httpCode(status) { return status === 401 ? 'FDB_AUTH' : status === 403 ? 'FDB_FORBIDDEN' : status === 409 ? 'FDB_CONFLICT' : status === 429 || status === 413 ? 'FDB_LIMIT' : 'FDB_REMOTE'; }
|
|
396
|
+
function redactMessage(message, token) { return (token ? message.replaceAll(token, '[redacted]') : message).slice(0, 2048); }
|
|
397
|
+
function safeMessage(reply, token) { return redactMessage(typeof reply?.error === 'string' ? reply.error : typeof reply?.error?.message === 'string' ? reply.error.message : 'Database request was rejected', token); }
|
|
398
|
+
|
|
399
|
+
async function saveRequest(path, request) { return require('./request-journal.cjs').saveRequest(path, request, validatePrepared); }
|
|
400
|
+
async function loadRequest(path) { return require('./request-journal.cjs').loadRequest(path, validatePrepared); }
|
|
401
|
+
|
|
402
|
+
module.exports = { connect, RemoteDatabase, ClientError, Float64, Record, Vector, encodeValue, decodeValue, normalizeUrl, validatePrepared, saveRequest, loadRequest };
|
|
403
|
+
})(commonjs, commonjs.exports, specifier => {
|
|
404
|
+
if (specifier === './values.cjs') return module0;
|
|
405
|
+
if (specifier === './client-internal.cjs') return {};
|
|
406
|
+
if (specifier === './request-journal.cjs') throw new TypeError("saveRequest and loadRequest need Node.js file access; store the PreparedWrite yourself");
|
|
407
|
+
throw new TypeError(`Cannot load ${specifier}`);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
export const { connect, RemoteDatabase, ClientError, Float64, Record, Vector, encodeValue, decodeValue, normalizeUrl, validatePrepared, saveRequest, loadRequest } = commonjs.exports;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { Worker } = require('node:worker_threads');
|
|
3
|
+
const { addAbortListener } = require('node:events');
|
|
4
|
+
const { randomUUID } = require('node:crypto');
|
|
5
|
+
const fs = require('node:fs/promises');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { pathToFileURL } = require('node:url');
|
|
8
|
+
const common = require('./client.cjs');
|
|
9
|
+
const { connect, RemoteDatabase, ClientError, Float64, Record, Vector, encodeValue, decodeValue, normalizeUrl, validatePrepared, saveRequest, loadRequest } = common;
|
|
10
|
+
const { plainObject } = require('./values.cjs');
|
|
11
|
+
const { createEmbeddedClient } = require('./client-internal.cjs');
|
|
12
|
+
const native = require('./native.cjs');
|
|
13
|
+
const sessions = new Map();
|
|
14
|
+
const construction = Symbol('EmbeddedDatabase');
|
|
15
|
+
|
|
16
|
+
async function openEmbedded(options) {
|
|
17
|
+
const duration = options?.timeoutMs ?? 20000;
|
|
18
|
+
if (!Number.isInteger(duration) || duration < 1 || duration > 120000) throw new RangeError('timeoutMs must be 1..120000');
|
|
19
|
+
let abandoned = false;
|
|
20
|
+
const opening = openEmbeddedFile(options);
|
|
21
|
+
opening.then(database => { if (abandoned) void database.close().catch(() => {}); }, () => {});
|
|
22
|
+
let timer;
|
|
23
|
+
const deadline = new Promise((resolve, reject) => {
|
|
24
|
+
timer = setTimeout(() => { abandoned = true; reject(new common.ClientError('FDB_CANCELLED', 'Embedded connection deadline expired; startup cleanup will finish when native work returns', 'not-sent')); }, duration);
|
|
25
|
+
});
|
|
26
|
+
try { return await Promise.race([opening, deadline]); }
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof common.ClientError || error instanceof TypeError || error instanceof RangeError) throw error;
|
|
29
|
+
const code = error.code === 'EEXIST' ? 'FDB_ALREADY_EXISTS' : ['EACCES', 'EPERM'].includes(error.code) ? 'FDB_FORBIDDEN' : error.code === 'ENOENT' ? 'FDB_NOT_FOUND' : 'FDB_STORAGE';
|
|
30
|
+
throw new common.ClientError(code, 'Embedded filesystem operation failed', 'not-sent');
|
|
31
|
+
} finally { clearTimeout(timer); }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function openEmbeddedFile(options) {
|
|
35
|
+
if (!plainObject(options) || Object.keys(options).some(key => !['path', 'create', 'timeoutMs', 'maxOpen'].includes(key)) || typeof options.path !== 'string' || !options.path || options.path.length > 4096 || options.path.includes('\0') || (options.create !== undefined && typeof options.create !== 'boolean')) throw new TypeError('embedded options require a bounded path, optional create, timeoutMs and maxOpen');
|
|
36
|
+
if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1 || options.timeoutMs > 120000)) throw new RangeError('timeoutMs must be 1..120000');
|
|
37
|
+
const maxOpen = options.maxOpen ?? 16;
|
|
38
|
+
if (!Number.isInteger(maxOpen) || maxOpen < 1 || maxOpen > 1024) throw new RangeError('maxOpen must be 1..1024');
|
|
39
|
+
const file = path.resolve(options.path);
|
|
40
|
+
if (options.create) {
|
|
41
|
+
if (sessions.size >= maxOpen) throw openLimit();
|
|
42
|
+
const parsed = path.parse(file);
|
|
43
|
+
for (const sidecar of [file + '-wal', file + '-shm', file + '-log', file + '-journal', path.join(parsed.dir, parsed.name + '.db-log')]) {
|
|
44
|
+
try { await fs.lstat(sidecar); throw new common.ClientError('FDB_CONFLICT', 'Database sidecar exists; recover it before creating a file', 'not-sent'); }
|
|
45
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
46
|
+
}
|
|
47
|
+
const handle = await fs.open(file, 'wx', 0o600);
|
|
48
|
+
try { await handle.sync(); } finally { await handle.close(); }
|
|
49
|
+
const parent = await fs.open(path.dirname(file), 'r');
|
|
50
|
+
try { await parent.sync(); } finally { await parent.close(); }
|
|
51
|
+
}
|
|
52
|
+
let metadata;
|
|
53
|
+
try { metadata = await fs.lstat(file); }
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === 'ENOENT') throw new common.ClientError('FDB_NOT_FOUND', 'Open an existing database or explicitly create one', 'not-sent');
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
if (!metadata.isFile() || metadata.nlink !== 1) throw new common.ClientError('FDB_VALIDATION', 'Use a regular database file with one link', 'not-sent');
|
|
59
|
+
const canonical = await fs.realpath(file);
|
|
60
|
+
let session = sessions.get(canonical);
|
|
61
|
+
if (session?.closing) { await session.closed; return openEmbeddedFile({ ...options, create: false }); }
|
|
62
|
+
if (!session) {
|
|
63
|
+
if (sessions.size >= maxOpen) throw openLimit();
|
|
64
|
+
session = new Session(canonical, options.create === true);
|
|
65
|
+
sessions.set(canonical, session);
|
|
66
|
+
}
|
|
67
|
+
session.references++;
|
|
68
|
+
try {
|
|
69
|
+
await session.ready;
|
|
70
|
+
const client = await createEmbeddedClient({ url: session.url, timeoutMs: options.timeoutMs }, (route, body, signal, duration) => session.request(route === '' ? 'info' : route === '/read' ? 'read' : 'write', body, signal, duration));
|
|
71
|
+
return new EmbeddedDatabase(client, session, canonical, construction);
|
|
72
|
+
} catch (error) { await releaseSession(session, canonical); throw error; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function openLimit() {
|
|
76
|
+
return new common.ClientError('FDB_LIMIT', `Embedded database limit reached: ${sessions.size} files are open; close one or raise maxOpen`, 'not-sent');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class EmbeddedDatabase {
|
|
80
|
+
#client; #session; #path; #closing;
|
|
81
|
+
constructor(client, session, file, key) {
|
|
82
|
+
if (key !== construction) throw new TypeError('use openEmbedded()');
|
|
83
|
+
this.#client = client; this.#session = session; this.#path = file;
|
|
84
|
+
}
|
|
85
|
+
get url() { return this.#client.url; }
|
|
86
|
+
get sequence() { return this.#client.sequence; }
|
|
87
|
+
get sequenceKnown() { return this.#client.sequenceKnown; }
|
|
88
|
+
get databaseId() { return this.#client.databaseId; }
|
|
89
|
+
get path() { return this.#path; }
|
|
90
|
+
info(options) { return this.#client.info(options); }
|
|
91
|
+
read(statements, options) { return this.#client.read(statements, options); }
|
|
92
|
+
prepareWrite(statements, options) { return this.#client.prepareWrite(statements, options); }
|
|
93
|
+
submit(request, options) { return this.#client.submit(request, options); }
|
|
94
|
+
recover(request, options) { return this.#client.recover(request, options); }
|
|
95
|
+
writeAtomic(statements, options) { return this.#client.writeAtomic(statements, options); }
|
|
96
|
+
close() {
|
|
97
|
+
this.#closing ??= (async () => { await this.#client.close(); await releaseSession(this.#session, this.#path); })();
|
|
98
|
+
return this.#closing;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class Session {
|
|
103
|
+
#worker; #pending = new Map(); #bytes = 0; #next = 0; #failure;
|
|
104
|
+
references = 0; closing = false; ready; closed;
|
|
105
|
+
constructor(file, create) {
|
|
106
|
+
this.url = pathToFileURL(file).href;
|
|
107
|
+
this.#worker = new Worker(require.resolve('./embedded-worker.cjs'), { workerData: { path: file, url: this.url, databaseId: randomUUID(), create } });
|
|
108
|
+
let resolveReady, rejectReady, resolveExit;
|
|
109
|
+
this.ready = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
|
|
110
|
+
this.closed = new Promise(resolve => { resolveExit = resolve; });
|
|
111
|
+
this.ready.catch(() => {});
|
|
112
|
+
this.#worker.on('message', message => {
|
|
113
|
+
if (message.ready) return resolveReady();
|
|
114
|
+
if (message.readyError) {
|
|
115
|
+
this.#failure = new common.ClientError(message.readyError.code, message.readyError.message, 'not-sent');
|
|
116
|
+
rejectReady(this.#failure);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const pending = this.#pending.get(message.id);
|
|
120
|
+
if (!pending) return;
|
|
121
|
+
this.#pending.delete(message.id);
|
|
122
|
+
this.#bytes -= pending.bytes;
|
|
123
|
+
pending.listener?.[Symbol.dispose]();
|
|
124
|
+
native.releaseCancellationToken(pending.token);
|
|
125
|
+
if (message.error) pending.reject(new common.ClientError(message.error.code, message.error.message, message.error.outcome));
|
|
126
|
+
else pending.resolve(message.result);
|
|
127
|
+
});
|
|
128
|
+
this.#worker.on('error', () => {
|
|
129
|
+
this.#failure ??= new common.ClientError('FDB_TRANSPORT', 'Embedded worker failed; reconcile pending writes', 'unknown');
|
|
130
|
+
rejectReady(this.#failure);
|
|
131
|
+
});
|
|
132
|
+
this.#worker.on('exit', code => {
|
|
133
|
+
this.#failure ??= new common.ClientError('FDB_CLOSED', code === 0 ? 'Embedded worker closed' : 'Embedded worker exited unexpectedly', 'unknown');
|
|
134
|
+
rejectReady(this.#failure);
|
|
135
|
+
for (const pending of this.#pending.values()) {
|
|
136
|
+
pending.listener?.[Symbol.dispose]();
|
|
137
|
+
native.releaseCancellationToken(pending.token);
|
|
138
|
+
pending.reject(this.#failure);
|
|
139
|
+
}
|
|
140
|
+
this.#pending.clear(); this.#bytes = 0;
|
|
141
|
+
resolveExit();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
request(operation, body, signal, duration) {
|
|
145
|
+
if (this.#failure) return Promise.reject(this.#failure);
|
|
146
|
+
if (this.closing && operation !== 'close') return Promise.reject(new common.ClientError('FDB_CLOSED', 'Embedded session is closing', 'not-sent'));
|
|
147
|
+
if (signal?.aborted) return Promise.reject(new common.ClientError('FDB_CANCELLED', 'Request cancelled before dispatch', 'not-sent'));
|
|
148
|
+
const bytes = body === undefined ? 0 : Buffer.byteLength(body);
|
|
149
|
+
if (operation !== 'close' && (this.#pending.size >= 32 || this.#bytes + bytes > 2097152)) return Promise.reject(new common.ClientError('FDB_LIMIT', 'Embedded request queue limit reached', 'not-sent'));
|
|
150
|
+
const token = native.createCancellationToken(duration === undefined ? undefined : String(duration));
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const id = ++this.#next;
|
|
153
|
+
const listener = signal === undefined ? undefined : addAbortListener(signal, () => {
|
|
154
|
+
native.cancelOperation(token);
|
|
155
|
+
reject(new common.ClientError('FDB_CANCELLED', operation === 'write' ? 'Write was not confirmed; recover the saved request' : 'Embedded request cancelled', operation === 'write' ? 'unknown' : 'read-failed'));
|
|
156
|
+
});
|
|
157
|
+
this.#pending.set(id, { resolve, reject, token, listener, bytes }); this.#bytes += bytes;
|
|
158
|
+
try { this.#worker.postMessage({ id, operation, body, cancellationKey: token }); }
|
|
159
|
+
catch {
|
|
160
|
+
this.#pending.delete(id); this.#bytes -= bytes;
|
|
161
|
+
listener?.[Symbol.dispose](); native.releaseCancellationToken(token);
|
|
162
|
+
reject(new common.ClientError('FDB_TRANSPORT', 'Embedded request could not be dispatched', 'not-sent'));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async close() {
|
|
167
|
+
this.closing = true;
|
|
168
|
+
try { if (!this.#failure) await this.request('close'); }
|
|
169
|
+
finally { await this.closed; }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function releaseSession(session, file) {
|
|
174
|
+
if (--session.references === 0) {
|
|
175
|
+
try { await session.close(); }
|
|
176
|
+
finally { if (sessions.get(file) === session) sessions.delete(file); }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = { openEmbedded, EmbeddedDatabase, connect, RemoteDatabase, ClientError, Float64, Record, Vector, encodeValue, decodeValue, normalizeUrl, validatePrepared, saveRequest, loadRequest };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from './client';
|
|
2
|
+
import type { Client, DatabaseInfo, Statement, RequestOptions, ReadOptions, WriteOptions, BatchResult, PreparedWrite } from './client';
|
|
3
|
+
export interface EmbeddedOptions { path: string; create?: boolean; timeoutMs?: number; maxOpen?: number; }
|
|
4
|
+
export class EmbeddedDatabase implements Client {
|
|
5
|
+
private constructor();
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly url: string;
|
|
8
|
+
readonly sequence: number;
|
|
9
|
+
readonly sequenceKnown: boolean;
|
|
10
|
+
readonly databaseId: string;
|
|
11
|
+
info(options?: RequestOptions): Promise<DatabaseInfo>;
|
|
12
|
+
read(statements: Statement[], options?: ReadOptions): Promise<BatchResult>;
|
|
13
|
+
prepareWrite(statements: Statement[], options?: WriteOptions): PreparedWrite;
|
|
14
|
+
submit(request: PreparedWrite, options?: RequestOptions): Promise<BatchResult>;
|
|
15
|
+
recover(request: PreparedWrite, options?: RequestOptions): Promise<BatchResult>;
|
|
16
|
+
writeAtomic(statements: Statement[], options?: WriteOptions): Promise<BatchResult>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export function openEmbedded(options: EmbeddedOptions): Promise<EmbeddedDatabase>;
|