@romedb/node 0.2.12 → 0.2.13

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.
@@ -4,7 +4,7 @@ Generated from the pinned Cargo dependency inventory and checksummed source arch
4
4
 
5
5
  This is a partial notice collection. It supplements LICENSE.md and THIRD_PARTY_NOTICES.md. Inline attributions, unconventional filenames and other bundled components require further review before a complete distribution-notice claim.
6
6
 
7
- Target: `x86_64-unknown-linux-gnu`. Cargo.lock SHA-256: `f065eb377ff1eed208add1ebb20ee8039e125fcfb4dbd38cd663c4933167c709`.
7
+ Target: `x86_64-unknown-linux-gnu`. Cargo.lock SHA-256: `092e973bbc52cf5defe48c0c1c1cf5bc8755b603f5a6ef47927c31bfa5276aa3`.
8
8
 
9
9
  ## Packages without collected archive or supplemental texts
10
10
 
@@ -703,13 +703,13 @@ Target: `x86_64-unknown-linux-gnu`. Cargo.lock SHA-256: `f065eb377ff1eed208add1e
703
703
  - `LICENSE-APACHE`: [source text a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2](#text-a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2)
704
704
  - `LICENSE-MIT`: [source text 42d3bf7e7d4d49d72c0555d14ed99c3ee7ce9ce3cbffbc38bbafe8c103f50969](#text-42d3bf7e7d4d49d72c0555d14ed99c3ee7ce9ce3cbffbc38bbafe8c103f50969)
705
705
 
706
- ### romedb-node 0.2.12
706
+ ### romedb-node 0.2.13
707
707
  - Workspace `LICENSE.md`: [source text b646f9ee8bcaf87e8de75153b9df7a2861c7ac445c87e741768b3c2bccf47bc5](#text-b646f9ee8bcaf87e8de75153b9df7a2861c7ac445c87e741768b3c2bccf47bc5)
708
708
 
709
- ### romedb 0.2.12
709
+ ### romedb 0.2.13
710
710
  - Workspace `LICENSE.md`: [source text b646f9ee8bcaf87e8de75153b9df7a2861c7ac445c87e741768b3c2bccf47bc5](#text-b646f9ee8bcaf87e8de75153b9df7a2861c7ac445c87e741768b3c2bccf47bc5)
711
711
 
712
- ### romeql-parser 0.2.12
712
+ ### romeql-parser 0.2.13
713
713
  - Workspace `LICENSE.md`: [source text b646f9ee8bcaf87e8de75153b9df7a2861c7ac445c87e741768b3c2bccf47bc5](#text-b646f9ee8bcaf87e8de75153b9df7a2861c7ac445c87e741768b3c2bccf47bc5)
714
714
 
715
715
  ### rquickjs-core 0.13.0
package/client.cjs CHANGED
@@ -7,6 +7,7 @@ const MAX_CLOUD_REQUEST = 1_048_576;
7
7
  const CLOUD_QUERY_VERSION = 3;
8
8
  const MAX_RESPONSE = 524_288;
9
9
  const MAX_SEQUENCE = Number.MAX_SAFE_INTEGER - 1;
10
+ const DATABASE_FILE_TYPE = 'application/vnd.romedb.database';
10
11
  const construction = Symbol('RemoteDatabase');
11
12
  const warmAtConnect = Symbol('warmAtConnect');
12
13
  const WARM_STATES = new Set(['ready', 'starting', 'skipped']);
@@ -74,7 +75,7 @@ class RemoteDatabase {
74
75
  async info(options = {}) {
75
76
  const info = await this.#request('', undefined, options, false);
76
77
  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)) {
77
- throw new ClientError('FDB_PROTOCOL', 'Invalid database details or changed database identity', 'read-failed');
78
+ throw new ClientError('RDB_PROTOCOL', 'Invalid database details or changed database identity', 'read-failed');
78
79
  }
79
80
  if (this.#loopbackOrganization) this.#cloud = info.serviceVersion === undefined;
80
81
  const supported = capabilities(this.#url, info, !this.#cloud);
@@ -86,14 +87,14 @@ class RemoteDatabase {
86
87
  }
87
88
  async warm(options = {}) {
88
89
  this.#assertOpen();
89
- if (!this.#cloud) throw new ClientError('FDB_UNSUPPORTED', 'warm applies only to Cloud database URLs', 'not-sent');
90
+ if (!this.#cloud) throw new ClientError('RDB_UNSUPPORTED', 'warm applies only to Cloud database URLs', 'not-sent');
90
91
  const reply = await this.#request('/warm', '{}', options, false);
91
- if (!plainObject(reply) || !WARM_STATES.has(reply.state)) throw new ClientError('FDB_PROTOCOL', 'Invalid warm reply', 'read-failed');
92
+ if (!plainObject(reply) || !WARM_STATES.has(reply.state)) throw new ClientError('RDB_PROTOCOL', 'Invalid warm reply', 'read-failed');
92
93
  return { state: reply.state };
93
94
  }
94
95
  async read(statements, options = {}) {
95
96
  this.#assertOpen();
96
- if (this.#capabilities.readVersion !== 2) throw new ClientError('FDB_UNSUPPORTED', 'Database does not support fresh reads', 'not-sent');
97
+ if (this.#capabilities.readVersion !== 2) throw new ClientError('RDB_UNSUPPORTED', 'Database does not support fresh reads', 'not-sent');
97
98
  if (options.ifSequence !== undefined) throw new TypeError('fresh reads do not accept ifSequence');
98
99
  const afterSequence = options.afterSequence ?? this.#sequence;
99
100
  checkSequence(afterSequence);
@@ -110,12 +111,12 @@ class RemoteDatabase {
110
111
  }
111
112
  prepareWrite(statements, options = {}) {
112
113
  this.#assertOpen();
113
- 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');
114
- if (this.#capabilities.atomicWrites !== true || this.#capabilities.replayWindow !== 64) throw new ClientError('FDB_UNSUPPORTED', 'Database does not support atomic writes with recovery', 'not-sent');
114
+ if (!this.#sequenceKnown && this.#deferred && options.afterSequence === undefined) throw new ClientError('RDB_SEQUENCE_UNKNOWN', 'Read, call info() or pass afterSequence before preparing a write on a deferred connection', 'not-sent');
115
+ if (this.#capabilities.atomicWrites !== true || this.#capabilities.replayWindow !== 64) throw new ClientError('RDB_UNSUPPORTED', 'Database does not support atomic writes with recovery', 'not-sent');
115
116
  const afterSequence = options.afterSequence ?? this.#sequence;
116
117
  checkSequence(afterSequence);
117
118
  if (options.ifSequence !== undefined) {
118
- if (this.#capabilities.exactSequenceCondition !== true) throw new ClientError('FDB_UNSUPPORTED', 'Database does not support exact sequence conditions', 'not-sent');
119
+ if (this.#capabilities.exactSequenceCondition !== true) throw new ClientError('RDB_UNSUPPORTED', 'Database does not support exact sequence conditions', 'not-sent');
119
120
  checkSequence(options.ifSequence);
120
121
  if (options.ifSequence < afterSequence) throw new RangeError('ifSequence must be at least afterSequence');
121
122
  }
@@ -130,8 +131,8 @@ class RemoteDatabase {
130
131
  async submit(prepared, options = {}) {
131
132
  this.#assertOpen();
132
133
  const request = validatePrepared(prepared);
133
- 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 });
134
- if (prepared.url !== this.#url || prepared.databaseId !== this.#id) throw new ClientError('FDB_REQUEST_IDENTITY', 'Saved request belongs to another database', 'not-sent');
134
+ if (this.#capabilities.atomicWrites !== true || this.#capabilities.replayWindow !== 64 || (request.ifSequence !== undefined && this.#capabilities.exactSequenceCondition !== true)) throw new ClientError('RDB_UNSUPPORTED', 'Database does not support this write contract', 'not-sent', { request: prepared });
135
+ if (prepared.url !== this.#url || prepared.databaseId !== this.#id) throw new ClientError('RDB_REQUEST_IDENTITY', 'Saved request belongs to another database', 'not-sent');
135
136
  try {
136
137
  const reply = await this.#request('/query', prepared.body, options, true, prepared, preparedQueryVersion(prepared));
137
138
  const result = confirmReply(reply, request, false, prepared);
@@ -143,7 +144,7 @@ class RemoteDatabase {
143
144
  if (error.outcome === 'rolled-back') this.#sequence = Math.max(this.#sequence, error.sequence);
144
145
  throw error;
145
146
  }
146
- throw new ClientError('FDB_PROTOCOL', 'Write response could not be confirmed; recover the saved request', 'unknown', { request: prepared });
147
+ throw new ClientError('RDB_PROTOCOL', 'Write response could not be confirmed; recover the saved request', 'unknown', { request: prepared });
147
148
  }
148
149
  }
149
150
  async recover(prepared, options = {}) {
@@ -157,22 +158,79 @@ class RemoteDatabase {
157
158
  if (!this.#sequenceKnown && this.#deferred && options.afterSequence === undefined) await this.info(options);
158
159
  return this.submit(this.prepareWrite(statements, options), options);
159
160
  }
161
+ async exportFile(options = {}) {
162
+ this.#assertOpen();
163
+ if (this.#transport) throw new ClientError('RDB_UNSUPPORTED', 'Export embedded databases through EmbeddedDatabase.exportFile', 'not-sent');
164
+ if (this.#cloud) throw new ClientError('RDB_UNSUPPORTED', 'Cloud databases do not export database files yet', 'not-sent');
165
+ if (!plainObject(options) || Object.keys(options).some(key => !['signal', 'timeoutMs'].includes(key))) throw new TypeError('export options take signal and timeoutMs');
166
+ const duration = timeout(options.timeoutMs ?? this.#timeout);
167
+ if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) throw new TypeError('signal must be an AbortSignal');
168
+ if (options.signal?.aborted) throw new ClientError('RDB_CANCELLED', 'Request cancelled before dispatch', 'not-sent');
169
+ if (this.#active.size >= 32) throw new ClientError('RDB_LIMIT', 'Client request limit reached', 'not-sent');
170
+ const controller = new AbortController();
171
+ const abort = () => controller.abort();
172
+ options.signal?.addEventListener('abort', abort, { once: true });
173
+ const timer = setTimeout(abort, duration);
174
+ this.#active.add(controller);
175
+ let open = true;
176
+ const finish = () => {
177
+ if (!open) return;
178
+ open = false;
179
+ clearTimeout(timer);
180
+ options.signal?.removeEventListener('abort', abort);
181
+ this.#active.delete(controller);
182
+ };
183
+ const credential = this.#token;
184
+ let status;
185
+ try {
186
+ const response = await this.#fetch(this.#url + '/file', {
187
+ method: 'GET',
188
+ headers: { authorization: `Bearer ${credential}`, accept: `${DATABASE_FILE_TYPE}, application/json`, ...(this.#id === undefined ? {} : { 'x-romedb-database-id': this.#id }) },
189
+ redirect: 'manual', signal: controller.signal,
190
+ });
191
+ // The deadline covers building the copy; the caller's signal still stops the download.
192
+ clearTimeout(timer);
193
+ status = response.status;
194
+ if (response.type === 'opaqueredirect' || (status >= 300 && status < 400)) throw new ClientError('RDB_PROTOCOL', 'Database URL redirected; redirects are never followed with a credential', 'read-failed', { status });
195
+ if (!response.ok) {
196
+ const retryAfterMs = retryAfter(response.headers.get('retry-after'));
197
+ const reply = await boundedJson(response, MAX_RESPONSE).catch(() => undefined);
198
+ const code = plainObject(reply?.error) && typeof reply.error.code === 'string' && /^RDB_[A-Z0-9_]{1,80}$/.test(reply.error.code) ? reply.error.code : httpCode(status);
199
+ throw new ClientError(code, safeMessage(reply, credential), 'read-failed', { status, ...(retryAfterMs === undefined ? {} : { retryAfterMs }) });
200
+ }
201
+ const size = Number(/^\d{1,16}$/.exec(response.headers.get('content-length') ?? '')?.[0] ?? NaN);
202
+ const exported = Number(/^\d{1,16}$/.exec(response.headers.get('x-romedb-sequence') ?? '')?.[0] ?? NaN);
203
+ const identity = response.headers.get('x-romedb-database-id');
204
+ if ((response.headers.get('content-type') ?? '').split(';')[0].trim().toLowerCase() !== DATABASE_FILE_TYPE || !Number.isSafeInteger(size) || !sequence(exported, true) || !response.body) {
205
+ throw new ClientError('RDB_PROTOCOL', 'Invalid database file response', 'read-failed', { status });
206
+ }
207
+ if (identity === null || (this.#id !== undefined && identity !== this.#id)) throw new ClientError('RDB_REQUEST_IDENTITY', 'Database file belongs to another database', 'read-failed', { status });
208
+ this.#id ??= identity;
209
+ this.#sequence = Math.max(this.#sequence, exported);
210
+ this.#sequenceKnown = true;
211
+ return { sequence: exported, size, body: countedBody(response.body, size, controller, finish) };
212
+ } catch (error) {
213
+ finish();
214
+ if (error instanceof ClientError) throw new ClientError(error.code, redactMessage(error.message, credential), error.outcome, { ...(error.status === undefined ? {} : { status: error.status }), ...(error.retryAfterMs === undefined ? {} : { retryAfterMs: error.retryAfterMs }) });
215
+ throw new ClientError(controller.signal.aborted ? 'RDB_CANCELLED' : status === undefined ? 'RDB_TRANSPORT' : 'RDB_PROTOCOL', 'Database file request failed', 'read-failed', { ...(status === undefined ? {} : { status }), cause: error });
216
+ }
217
+ }
160
218
  async close() {
161
219
  this.#closed = true;
162
220
  for (const controller of this.#active) controller.abort();
163
221
  this.#token = '';
164
222
  }
165
223
  #assertOpen() {
166
- if (this.#closed) throw new ClientError('FDB_CLOSED', 'Database client is closed', 'not-sent');
224
+ if (this.#closed) throw new ClientError('RDB_CLOSED', 'Database client is closed', 'not-sent');
167
225
  }
168
226
  #queryVersion() { return this.#cloud ? CLOUD_QUERY_VERSION : 2; }
169
227
  async #request(path, body, options, writing, prepared, queryVersion = 2) {
170
228
  this.#assertOpen();
171
229
  const duration = timeout(options.timeoutMs ?? this.#timeout);
172
230
  if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) throw new TypeError('signal must be an AbortSignal');
173
- if (options.signal?.aborted) throw new ClientError('FDB_CANCELLED', 'Request cancelled before dispatch', 'not-sent', prepared ? { request: prepared } : {});
231
+ if (options.signal?.aborted) throw new ClientError('RDB_CANCELLED', 'Request cancelled before dispatch', 'not-sent', prepared ? { request: prepared } : {});
174
232
  const bodyBytes = body === undefined ? 0 : byteLength(body);
175
- 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 } : {});
233
+ if (this.#active.size >= 32 || this.#inflightBytes + bodyBytes > 2_097_152) throw new ClientError('RDB_LIMIT', 'Client request limit reached', 'not-sent', prepared ? { request: prepared } : {});
176
234
  const controller = new AbortController();
177
235
  const abort = () => controller.abort();
178
236
  options.signal?.addEventListener('abort', abort, { once: true });
@@ -200,18 +258,18 @@ class RemoteDatabase {
200
258
  status = response.status;
201
259
  if (response.type === 'opaqueredirect' || (status >= 300 && status < 400)) {
202
260
  await response.body?.cancel().catch(() => {});
203
- throw new ClientError('FDB_PROTOCOL', 'Database URL redirected; redirects are never followed with a credential', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
261
+ throw new ClientError('RDB_PROTOCOL', 'Database URL redirected; redirects are never followed with a credential', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
204
262
  }
205
263
  const retryAfterMs = retryAfter(response.headers.get('retry-after'));
206
264
  const replyVersion = response.headers.get('x-romedb-query-version');
207
265
  if ((path === '/query' || path === '/read') && replyVersion !== null && replyVersion !== '2' && replyVersion !== String(queryVersion)) {
208
266
  await response.body?.cancel();
209
- throw new ClientError('FDB_PROTOCOL', 'Unsupported database query response version', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
267
+ throw new ClientError('RDB_PROTOCOL', 'Unsupported database query response version', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
210
268
  }
211
269
  const reply = await boundedJson(response, MAX_RESPONSE);
212
270
  if (plainObject(reply?.error) && typeof reply.error.message === 'string') reply.error.message = redactMessage(reply.error.message, credential);
213
271
  if (!response.ok && !(writing && reply?.requestId && reply?.sequence !== undefined)) {
214
- 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);
272
+ const code = plainObject(reply?.error) && typeof reply.error.code === 'string' && /^RDB_[A-Z0-9_]{1,80}$/.test(reply.error.code) ? reply.error.code : httpCode(status);
215
273
  throw new ClientError(code, safeMessage(reply, credential), writing ? 'unknown' : 'read-failed', { status, ...(retryAfterMs === undefined ? {} : { retryAfterMs }), ...(prepared ? { request: prepared } : {}) });
216
274
  }
217
275
  return reply;
@@ -227,7 +285,7 @@ class RemoteDatabase {
227
285
  ...(prepared ? { request: prepared } : {}),
228
286
  });
229
287
  }
230
- const code = controller.signal.aborted ? 'FDB_CANCELLED' : status === undefined ? 'FDB_TRANSPORT' : 'FDB_PROTOCOL';
288
+ const code = controller.signal.aborted ? 'RDB_CANCELLED' : status === undefined ? 'RDB_TRANSPORT' : 'RDB_PROTOCOL';
231
289
  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 });
232
290
  } finally {
233
291
  clearTimeout(timer);
@@ -297,9 +355,9 @@ function validatePrepared(prepared) {
297
355
  }
298
356
 
299
357
  function confirmReply(reply, request, reading, prepared) {
300
- 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 } : {});
358
+ const invalid = () => new ClientError('RDB_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 } : {});
301
359
  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();
302
- 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 });
360
+ if (plainObject(reply.error) && typeof reply.error.code === 'string' && /^RDB_[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 });
303
361
  if (!Array.isArray(reply.results) || reply.results.length !== request.statements.length || reply.error !== undefined || byteLength(JSON.stringify(reply.results)) > 262_144) throw invalid();
304
362
  try {
305
363
  const results = reply.results.map(result => {
@@ -334,11 +392,39 @@ async function boundedJson(response, limit) {
334
392
  } finally { await reader.cancel().catch(() => {}); }
335
393
  }
336
394
 
395
+ // Passes the download through and fails it unless exactly `size` bytes arrive.
396
+ function countedBody(body, size, controller, finish) {
397
+ const reader = body.getReader();
398
+ let received = 0;
399
+ const failure = (message, cause) => new ClientError(controller.signal.aborted ? 'RDB_CANCELLED' : 'RDB_TRANSPORT', message, 'read-failed', cause === undefined ? {} : { cause });
400
+ return new ReadableStream({
401
+ async pull(stream) {
402
+ let chunk;
403
+ try { chunk = await reader.read(); }
404
+ catch (error) { finish(); stream.error(failure('Database file download failed', error)); return; }
405
+ if (chunk.done) {
406
+ finish();
407
+ if (received === size) stream.close();
408
+ else stream.error(failure('Database file ended before its declared size'));
409
+ return;
410
+ }
411
+ received += chunk.value.byteLength;
412
+ if (received > size) {
413
+ finish(); controller.abort();
414
+ stream.error(new ClientError('RDB_PROTOCOL', 'Database file is larger than its declared size', 'read-failed'));
415
+ return;
416
+ }
417
+ stream.enqueue(chunk.value);
418
+ },
419
+ cancel(reason) { finish(); controller.abort(); return reader.cancel(reason).catch(() => {}); },
420
+ }, { highWaterMark: 0 });
421
+ }
422
+
337
423
  function loopbackHost(hostname) { return ['127.0.0.1', 'localhost', '[::1]'].includes(hostname); }
338
424
 
339
425
  function capabilities(url, info, local) {
340
426
  if (local) {
341
- 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');
427
+ 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('RDB_PROTOCOL', 'Unsupported database client contract', 'read-failed');
342
428
  return { ...info.capabilities, provenance: 'negotiated' };
343
429
  }
344
430
  return { provenance: 'documented', readVersion: 2, atomicWrites: true, replayWindow: 64, exactSequenceCondition: true, negotiatedLimits: false };
@@ -387,7 +473,7 @@ function pause(ms, signal) {
387
473
  signal.addEventListener('abort', stop, { once: true });
388
474
  });
389
475
  }
390
- function httpCode(status) { return status === 401 ? 'FDB_AUTH' : status === 403 ? 'FDB_FORBIDDEN' : status === 409 ? 'FDB_CONFLICT' : status === 429 || status === 413 ? 'FDB_LIMIT' : 'FDB_REMOTE'; }
476
+ function httpCode(status) { return status === 401 ? 'RDB_AUTH' : status === 403 ? 'RDB_FORBIDDEN' : status === 409 ? 'RDB_CONFLICT' : status === 429 || status === 413 ? 'RDB_LIMIT' : 'RDB_REMOTE'; }
391
477
  function redactMessage(message, token) { return (token ? message.replaceAll(token, '[redacted]') : message).slice(0, 2048); }
392
478
  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); }
393
479
 
package/client.d.ts CHANGED
@@ -25,6 +25,15 @@ export interface ConnectOptions {
25
25
  warm?: boolean;
26
26
  }
27
27
  export interface WarmResult { state: 'ready' | 'starting' | 'skipped'; }
28
+ /** A copy of the database at one sequence: a complete .romedb file that opens without a log beside it. */
29
+ export interface DatabaseFile {
30
+ /** Every write up to and including this sequence is in the file, and none after it. */
31
+ sequence: number;
32
+ /** File size in bytes; the body fails unless exactly this many arrive. */
33
+ size: number;
34
+ /** Read it to the end or cancel it, so the copy's storage is freed. */
35
+ body: ReadableStream<Uint8Array>;
36
+ }
28
37
  export type Outcome = 'not-sent' | 'read-failed' | 'unknown' | 'rolled-back';
29
38
  export class ClientError extends Error {
30
39
  readonly code: string;
@@ -34,7 +43,7 @@ export class ClientError extends Error {
34
43
  readonly sequence?: number;
35
44
  /** The server's Retry-After delay in milliseconds, on HTTP error replies that send one. */
36
45
  readonly retryAfterMs?: number;
37
- /** For FDB_TRANSPORT, FDB_PROTOCOL and FDB_CANCELLED without a reply: the fetch or parse error underneath. */
46
+ /** For RDB_TRANSPORT, RDB_PROTOCOL and RDB_CANCELLED without a reply: the fetch or parse error underneath. */
38
47
  readonly cause?: unknown;
39
48
  constructor(code: string, message: string, outcome: Outcome, details?: { request?: PreparedWrite; status?: number; sequence?: number; retryAfterMs?: number; cause?: unknown });
40
49
  }
@@ -60,6 +69,11 @@ export class RemoteDatabase implements Client {
60
69
  readonly deferred: boolean;
61
70
  /** Ask Cloud to start the database's container and open the database without running a query. */
62
71
  warm(options?: RequestOptions): Promise<WarmResult>;
72
+ /**
73
+ * Download the database as a .romedb file (romedb serve and serve --directory; not Cloud yet).
74
+ * timeoutMs covers building the copy, until the file starts arriving; signal also stops the download.
75
+ */
76
+ exportFile(options?: RequestOptions): Promise<DatabaseFile>;
63
77
  readonly sequence: number;
64
78
  readonly databaseId: string;
65
79
  info(options?: RequestOptions): Promise<DatabaseInfo>;
package/client.mjs CHANGED
@@ -12,6 +12,7 @@ const MAX_CLOUD_REQUEST = 1_048_576;
12
12
  const CLOUD_QUERY_VERSION = 3;
13
13
  const MAX_RESPONSE = 524_288;
14
14
  const MAX_SEQUENCE = Number.MAX_SAFE_INTEGER - 1;
15
+ const DATABASE_FILE_TYPE = 'application/vnd.romedb.database';
15
16
  const construction = Symbol('RemoteDatabase');
16
17
  const warmAtConnect = Symbol('warmAtConnect');
17
18
  const WARM_STATES = new Set(['ready', 'starting', 'skipped']);
@@ -79,7 +80,7 @@ class RemoteDatabase {
79
80
  async info(options = {}) {
80
81
  const info = await this.#request('', undefined, options, false);
81
82
  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
+ throw new ClientError('RDB_PROTOCOL', 'Invalid database details or changed database identity', 'read-failed');
83
84
  }
84
85
  if (this.#loopbackOrganization) this.#cloud = info.serviceVersion === undefined;
85
86
  const supported = capabilities(this.#url, info, !this.#cloud);
@@ -91,14 +92,14 @@ class RemoteDatabase {
91
92
  }
92
93
  async warm(options = {}) {
93
94
  this.#assertOpen();
94
- if (!this.#cloud) throw new ClientError('FDB_UNSUPPORTED', 'warm applies only to Cloud database URLs', 'not-sent');
95
+ if (!this.#cloud) throw new ClientError('RDB_UNSUPPORTED', 'warm applies only to Cloud database URLs', 'not-sent');
95
96
  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
+ if (!plainObject(reply) || !WARM_STATES.has(reply.state)) throw new ClientError('RDB_PROTOCOL', 'Invalid warm reply', 'read-failed');
97
98
  return { state: reply.state };
98
99
  }
99
100
  async read(statements, options = {}) {
100
101
  this.#assertOpen();
101
- if (this.#capabilities.readVersion !== 2) throw new ClientError('FDB_UNSUPPORTED', 'Database does not support fresh reads', 'not-sent');
102
+ if (this.#capabilities.readVersion !== 2) throw new ClientError('RDB_UNSUPPORTED', 'Database does not support fresh reads', 'not-sent');
102
103
  if (options.ifSequence !== undefined) throw new TypeError('fresh reads do not accept ifSequence');
103
104
  const afterSequence = options.afterSequence ?? this.#sequence;
104
105
  checkSequence(afterSequence);
@@ -115,12 +116,12 @@ class RemoteDatabase {
115
116
  }
116
117
  prepareWrite(statements, options = {}) {
117
118
  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');
119
+ if (!this.#sequenceKnown && this.#deferred && options.afterSequence === undefined) throw new ClientError('RDB_SEQUENCE_UNKNOWN', 'Read, call info() or pass afterSequence before preparing a write on a deferred connection', 'not-sent');
120
+ if (this.#capabilities.atomicWrites !== true || this.#capabilities.replayWindow !== 64) throw new ClientError('RDB_UNSUPPORTED', 'Database does not support atomic writes with recovery', 'not-sent');
120
121
  const afterSequence = options.afterSequence ?? this.#sequence;
121
122
  checkSequence(afterSequence);
122
123
  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
+ if (this.#capabilities.exactSequenceCondition !== true) throw new ClientError('RDB_UNSUPPORTED', 'Database does not support exact sequence conditions', 'not-sent');
124
125
  checkSequence(options.ifSequence);
125
126
  if (options.ifSequence < afterSequence) throw new RangeError('ifSequence must be at least afterSequence');
126
127
  }
@@ -135,8 +136,8 @@ class RemoteDatabase {
135
136
  async submit(prepared, options = {}) {
136
137
  this.#assertOpen();
137
138
  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');
139
+ if (this.#capabilities.atomicWrites !== true || this.#capabilities.replayWindow !== 64 || (request.ifSequence !== undefined && this.#capabilities.exactSequenceCondition !== true)) throw new ClientError('RDB_UNSUPPORTED', 'Database does not support this write contract', 'not-sent', { request: prepared });
140
+ if (prepared.url !== this.#url || prepared.databaseId !== this.#id) throw new ClientError('RDB_REQUEST_IDENTITY', 'Saved request belongs to another database', 'not-sent');
140
141
  try {
141
142
  const reply = await this.#request('/query', prepared.body, options, true, prepared, preparedQueryVersion(prepared));
142
143
  const result = confirmReply(reply, request, false, prepared);
@@ -148,7 +149,7 @@ class RemoteDatabase {
148
149
  if (error.outcome === 'rolled-back') this.#sequence = Math.max(this.#sequence, error.sequence);
149
150
  throw error;
150
151
  }
151
- throw new ClientError('FDB_PROTOCOL', 'Write response could not be confirmed; recover the saved request', 'unknown', { request: prepared });
152
+ throw new ClientError('RDB_PROTOCOL', 'Write response could not be confirmed; recover the saved request', 'unknown', { request: prepared });
152
153
  }
153
154
  }
154
155
  async recover(prepared, options = {}) {
@@ -162,22 +163,79 @@ class RemoteDatabase {
162
163
  if (!this.#sequenceKnown && this.#deferred && options.afterSequence === undefined) await this.info(options);
163
164
  return this.submit(this.prepareWrite(statements, options), options);
164
165
  }
166
+ async exportFile(options = {}) {
167
+ this.#assertOpen();
168
+ if (this.#transport) throw new ClientError('RDB_UNSUPPORTED', 'Export embedded databases through EmbeddedDatabase.exportFile', 'not-sent');
169
+ if (this.#cloud) throw new ClientError('RDB_UNSUPPORTED', 'Cloud databases do not export database files yet', 'not-sent');
170
+ if (!plainObject(options) || Object.keys(options).some(key => !['signal', 'timeoutMs'].includes(key))) throw new TypeError('export options take signal and timeoutMs');
171
+ const duration = timeout(options.timeoutMs ?? this.#timeout);
172
+ if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) throw new TypeError('signal must be an AbortSignal');
173
+ if (options.signal?.aborted) throw new ClientError('RDB_CANCELLED', 'Request cancelled before dispatch', 'not-sent');
174
+ if (this.#active.size >= 32) throw new ClientError('RDB_LIMIT', 'Client request limit reached', 'not-sent');
175
+ const controller = new AbortController();
176
+ const abort = () => controller.abort();
177
+ options.signal?.addEventListener('abort', abort, { once: true });
178
+ const timer = setTimeout(abort, duration);
179
+ this.#active.add(controller);
180
+ let open = true;
181
+ const finish = () => {
182
+ if (!open) return;
183
+ open = false;
184
+ clearTimeout(timer);
185
+ options.signal?.removeEventListener('abort', abort);
186
+ this.#active.delete(controller);
187
+ };
188
+ const credential = this.#token;
189
+ let status;
190
+ try {
191
+ const response = await this.#fetch(this.#url + '/file', {
192
+ method: 'GET',
193
+ headers: { authorization: `Bearer ${credential}`, accept: `${DATABASE_FILE_TYPE}, application/json`, ...(this.#id === undefined ? {} : { 'x-romedb-database-id': this.#id }) },
194
+ redirect: 'manual', signal: controller.signal,
195
+ });
196
+ // The deadline covers building the copy; the caller's signal still stops the download.
197
+ clearTimeout(timer);
198
+ status = response.status;
199
+ if (response.type === 'opaqueredirect' || (status >= 300 && status < 400)) throw new ClientError('RDB_PROTOCOL', 'Database URL redirected; redirects are never followed with a credential', 'read-failed', { status });
200
+ if (!response.ok) {
201
+ const retryAfterMs = retryAfter(response.headers.get('retry-after'));
202
+ const reply = await boundedJson(response, MAX_RESPONSE).catch(() => undefined);
203
+ const code = plainObject(reply?.error) && typeof reply.error.code === 'string' && /^RDB_[A-Z0-9_]{1,80}$/.test(reply.error.code) ? reply.error.code : httpCode(status);
204
+ throw new ClientError(code, safeMessage(reply, credential), 'read-failed', { status, ...(retryAfterMs === undefined ? {} : { retryAfterMs }) });
205
+ }
206
+ const size = Number(/^\d{1,16}$/.exec(response.headers.get('content-length') ?? '')?.[0] ?? NaN);
207
+ const exported = Number(/^\d{1,16}$/.exec(response.headers.get('x-romedb-sequence') ?? '')?.[0] ?? NaN);
208
+ const identity = response.headers.get('x-romedb-database-id');
209
+ if ((response.headers.get('content-type') ?? '').split(';')[0].trim().toLowerCase() !== DATABASE_FILE_TYPE || !Number.isSafeInteger(size) || !sequence(exported, true) || !response.body) {
210
+ throw new ClientError('RDB_PROTOCOL', 'Invalid database file response', 'read-failed', { status });
211
+ }
212
+ if (identity === null || (this.#id !== undefined && identity !== this.#id)) throw new ClientError('RDB_REQUEST_IDENTITY', 'Database file belongs to another database', 'read-failed', { status });
213
+ this.#id ??= identity;
214
+ this.#sequence = Math.max(this.#sequence, exported);
215
+ this.#sequenceKnown = true;
216
+ return { sequence: exported, size, body: countedBody(response.body, size, controller, finish) };
217
+ } catch (error) {
218
+ finish();
219
+ if (error instanceof ClientError) throw new ClientError(error.code, redactMessage(error.message, credential), error.outcome, { ...(error.status === undefined ? {} : { status: error.status }), ...(error.retryAfterMs === undefined ? {} : { retryAfterMs: error.retryAfterMs }) });
220
+ throw new ClientError(controller.signal.aborted ? 'RDB_CANCELLED' : status === undefined ? 'RDB_TRANSPORT' : 'RDB_PROTOCOL', 'Database file request failed', 'read-failed', { ...(status === undefined ? {} : { status }), cause: error });
221
+ }
222
+ }
165
223
  async close() {
166
224
  this.#closed = true;
167
225
  for (const controller of this.#active) controller.abort();
168
226
  this.#token = '';
169
227
  }
170
228
  #assertOpen() {
171
- if (this.#closed) throw new ClientError('FDB_CLOSED', 'Database client is closed', 'not-sent');
229
+ if (this.#closed) throw new ClientError('RDB_CLOSED', 'Database client is closed', 'not-sent');
172
230
  }
173
231
  #queryVersion() { return this.#cloud ? CLOUD_QUERY_VERSION : 2; }
174
232
  async #request(path, body, options, writing, prepared, queryVersion = 2) {
175
233
  this.#assertOpen();
176
234
  const duration = timeout(options.timeoutMs ?? this.#timeout);
177
235
  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 } : {});
236
+ if (options.signal?.aborted) throw new ClientError('RDB_CANCELLED', 'Request cancelled before dispatch', 'not-sent', prepared ? { request: prepared } : {});
179
237
  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 } : {});
238
+ if (this.#active.size >= 32 || this.#inflightBytes + bodyBytes > 2_097_152) throw new ClientError('RDB_LIMIT', 'Client request limit reached', 'not-sent', prepared ? { request: prepared } : {});
181
239
  const controller = new AbortController();
182
240
  const abort = () => controller.abort();
183
241
  options.signal?.addEventListener('abort', abort, { once: true });
@@ -205,18 +263,18 @@ class RemoteDatabase {
205
263
  status = response.status;
206
264
  if (response.type === 'opaqueredirect' || (status >= 300 && status < 400)) {
207
265
  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 } : {}) });
266
+ throw new ClientError('RDB_PROTOCOL', 'Database URL redirected; redirects are never followed with a credential', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
209
267
  }
210
268
  const retryAfterMs = retryAfter(response.headers.get('retry-after'));
211
269
  const replyVersion = response.headers.get('x-romedb-query-version');
212
270
  if ((path === '/query' || path === '/read') && replyVersion !== null && replyVersion !== '2' && replyVersion !== String(queryVersion)) {
213
271
  await response.body?.cancel();
214
- throw new ClientError('FDB_PROTOCOL', 'Unsupported database query response version', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
272
+ throw new ClientError('RDB_PROTOCOL', 'Unsupported database query response version', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
215
273
  }
216
274
  const reply = await boundedJson(response, MAX_RESPONSE);
217
275
  if (plainObject(reply?.error) && typeof reply.error.message === 'string') reply.error.message = redactMessage(reply.error.message, credential);
218
276
  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);
277
+ const code = plainObject(reply?.error) && typeof reply.error.code === 'string' && /^RDB_[A-Z0-9_]{1,80}$/.test(reply.error.code) ? reply.error.code : httpCode(status);
220
278
  throw new ClientError(code, safeMessage(reply, credential), writing ? 'unknown' : 'read-failed', { status, ...(retryAfterMs === undefined ? {} : { retryAfterMs }), ...(prepared ? { request: prepared } : {}) });
221
279
  }
222
280
  return reply;
@@ -232,7 +290,7 @@ class RemoteDatabase {
232
290
  ...(prepared ? { request: prepared } : {}),
233
291
  });
234
292
  }
235
- const code = controller.signal.aborted ? 'FDB_CANCELLED' : status === undefined ? 'FDB_TRANSPORT' : 'FDB_PROTOCOL';
293
+ const code = controller.signal.aborted ? 'RDB_CANCELLED' : status === undefined ? 'RDB_TRANSPORT' : 'RDB_PROTOCOL';
236
294
  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
295
  } finally {
238
296
  clearTimeout(timer);
@@ -302,9 +360,9 @@ function validatePrepared(prepared) {
302
360
  }
303
361
 
304
362
  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 } : {});
363
+ const invalid = () => new ClientError('RDB_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
364
  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 });
365
+ if (plainObject(reply.error) && typeof reply.error.code === 'string' && /^RDB_[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
366
  if (!Array.isArray(reply.results) || reply.results.length !== request.statements.length || reply.error !== undefined || byteLength(JSON.stringify(reply.results)) > 262_144) throw invalid();
309
367
  try {
310
368
  const results = reply.results.map(result => {
@@ -339,11 +397,39 @@ async function boundedJson(response, limit) {
339
397
  } finally { await reader.cancel().catch(() => {}); }
340
398
  }
341
399
 
400
+ // Passes the download through and fails it unless exactly `size` bytes arrive.
401
+ function countedBody(body, size, controller, finish) {
402
+ const reader = body.getReader();
403
+ let received = 0;
404
+ const failure = (message, cause) => new ClientError(controller.signal.aborted ? 'RDB_CANCELLED' : 'RDB_TRANSPORT', message, 'read-failed', cause === undefined ? {} : { cause });
405
+ return new ReadableStream({
406
+ async pull(stream) {
407
+ let chunk;
408
+ try { chunk = await reader.read(); }
409
+ catch (error) { finish(); stream.error(failure('Database file download failed', error)); return; }
410
+ if (chunk.done) {
411
+ finish();
412
+ if (received === size) stream.close();
413
+ else stream.error(failure('Database file ended before its declared size'));
414
+ return;
415
+ }
416
+ received += chunk.value.byteLength;
417
+ if (received > size) {
418
+ finish(); controller.abort();
419
+ stream.error(new ClientError('RDB_PROTOCOL', 'Database file is larger than its declared size', 'read-failed'));
420
+ return;
421
+ }
422
+ stream.enqueue(chunk.value);
423
+ },
424
+ cancel(reason) { finish(); controller.abort(); return reader.cancel(reason).catch(() => {}); },
425
+ }, { highWaterMark: 0 });
426
+ }
427
+
342
428
  function loopbackHost(hostname) { return ['127.0.0.1', 'localhost', '[::1]'].includes(hostname); }
343
429
 
344
430
  function capabilities(url, info, local) {
345
431
  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');
432
+ 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('RDB_PROTOCOL', 'Unsupported database client contract', 'read-failed');
347
433
  return { ...info.capabilities, provenance: 'negotiated' };
348
434
  }
349
435
  return { provenance: 'documented', readVersion: 2, atomicWrites: true, replayWindow: 64, exactSequenceCondition: true, negotiatedLimits: false };
@@ -392,7 +478,7 @@ function pause(ms, signal) {
392
478
  signal.addEventListener('abort', stop, { once: true });
393
479
  });
394
480
  }
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'; }
481
+ function httpCode(status) { return status === 401 ? 'RDB_AUTH' : status === 403 ? 'RDB_FORBIDDEN' : status === 409 ? 'RDB_CONFLICT' : status === 429 || status === 413 ? 'RDB_LIMIT' : 'RDB_REMOTE'; }
396
482
  function redactMessage(message, token) { return (token ? message.replaceAll(token, '[redacted]') : message).slice(0, 2048); }
397
483
  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
484