@romedb/node 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -10
- package/THIRD_PARTY_CRATE_NOTICES.md +4 -4
- package/client.cjs +1 -488
- package/client.d.ts +1 -105
- package/client.mjs +1 -502
- package/embedded-client.cjs +1 -1
- package/package.json +5 -4
- package/romedb.node +2 -2
- package/values.cjs +1 -220
- package/values.d.ts +1 -15
- package/values.mjs +1 -230
- package/client-internal.cjs +0 -2
- package/request-journal.cjs +0 -47
package/client.d.ts
CHANGED
|
@@ -1,105 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { Float64, Record, Vector, Value, PortableValue, encodeValue, decodeValue } from './values';
|
|
3
|
-
export interface Statement { sql: string; params?: { [key: string]: Value }; }
|
|
4
|
-
export interface RequestOptions { signal?: AbortSignal; timeoutMs?: number; }
|
|
5
|
-
/** What a copy of the database leaves out. */
|
|
6
|
-
export interface CopyOptions {
|
|
7
|
-
/**
|
|
8
|
-
* These tables keep their schema and indexes but no rows. Their rows are never read, so
|
|
9
|
-
* they cannot reach the file. Unknown names fail with RDB_VALIDATION. The copy also gets
|
|
10
|
-
* a new identity, because a receipt can hold row data.
|
|
11
|
-
*/
|
|
12
|
-
emptyTables?: readonly string[];
|
|
13
|
-
/** Leave out the database ID, the sequence and the receipts: the copy is a new database. */
|
|
14
|
-
newIdentity?: boolean;
|
|
15
|
-
}
|
|
16
|
-
export interface FileExportOptions extends RequestOptions, CopyOptions {}
|
|
17
|
-
export interface WorkLimits { rowsRead?: number; rowMutations?: number; vmSteps?: number; }
|
|
18
|
-
export interface ReadOptions extends RequestOptions { afterSequence?: number; workLimits?: WorkLimits; }
|
|
19
|
-
export interface WriteOptions extends ReadOptions { requestId?: string; ifSequence?: number; }
|
|
20
|
-
export type PreparedWrite =
|
|
21
|
-
| { readonly version: 1; readonly url: string; readonly databaseId: string; readonly body: string }
|
|
22
|
-
| { readonly version: 2; readonly url: string; readonly databaseId: string; readonly queryVersion: 2 | 3; readonly body: string };
|
|
23
|
-
export interface QueryResult { columns: string[]; rows: Value[][]; affected: bigint; }
|
|
24
|
-
export interface BatchResult { requestId: string; sequence: number; results: QueryResult[]; readVersion?: 2; executionWork?: unknown; }
|
|
25
|
-
export interface Capabilities { provenance: 'negotiated' | 'documented'; [key: string]: unknown; }
|
|
26
|
-
export interface DatabaseInfo {
|
|
27
|
-
id: string; sequence: number; capabilities: Capabilities;
|
|
28
|
-
/** RomeDB release of the engine, such as "2.7.0". RomeDB 2.7.0 and later report it. */
|
|
29
|
-
engineVersion?: string;
|
|
30
|
-
[key: string]: unknown;
|
|
31
|
-
}
|
|
32
|
-
export interface ConnectOptions {
|
|
33
|
-
url: string; token: string; timeoutMs?: number; fetch?: typeof globalThis.fetch;
|
|
34
|
-
/** Cloud database URLs skip the details request at connect by default; the first write without afterSequence fetches it. Pass false to request it at connect. */
|
|
35
|
-
deferInfo?: boolean;
|
|
36
|
-
/** Cloud database URLs send a background warm request at connect by default. Pass false to skip it. */
|
|
37
|
-
warm?: boolean;
|
|
38
|
-
}
|
|
39
|
-
export interface WarmResult { state: 'ready' | 'starting' | 'skipped'; }
|
|
40
|
-
/** A copy of the database at one sequence: a complete .romedb file that opens without a log beside it. */
|
|
41
|
-
export interface DatabaseFile {
|
|
42
|
-
/** Every write up to and including this sequence is in the file, and none after it. */
|
|
43
|
-
sequence: number;
|
|
44
|
-
/** File size in bytes; the body fails unless exactly this many arrive. */
|
|
45
|
-
size: number;
|
|
46
|
-
/** Read it to the end or cancel it, so the copy's storage is freed. */
|
|
47
|
-
body: ReadableStream<Uint8Array>;
|
|
48
|
-
}
|
|
49
|
-
export type Outcome = 'not-sent' | 'read-failed' | 'unknown' | 'rolled-back';
|
|
50
|
-
export class ClientError extends Error {
|
|
51
|
-
readonly code: string;
|
|
52
|
-
readonly outcome: Outcome;
|
|
53
|
-
readonly request?: PreparedWrite;
|
|
54
|
-
readonly status?: number;
|
|
55
|
-
readonly sequence?: number;
|
|
56
|
-
/** The server's Retry-After delay in milliseconds, on HTTP error replies that send one. */
|
|
57
|
-
readonly retryAfterMs?: number;
|
|
58
|
-
/** For RDB_TRANSPORT, RDB_PROTOCOL and RDB_CANCELLED without a reply: the fetch or parse error underneath. */
|
|
59
|
-
readonly cause?: unknown;
|
|
60
|
-
constructor(code: string, message: string, outcome: Outcome, details?: { request?: PreparedWrite; status?: number; sequence?: number; retryAfterMs?: number; cause?: unknown });
|
|
61
|
-
}
|
|
62
|
-
export interface Client {
|
|
63
|
-
readonly sequence: number;
|
|
64
|
-
readonly databaseId: string;
|
|
65
|
-
info(options?: RequestOptions): Promise<DatabaseInfo>;
|
|
66
|
-
read(statements: Statement[], options?: ReadOptions): Promise<BatchResult>;
|
|
67
|
-
prepareWrite(statements: Statement[], options?: WriteOptions): PreparedWrite;
|
|
68
|
-
submit(request: PreparedWrite, options?: RequestOptions): Promise<BatchResult>;
|
|
69
|
-
recover(request: PreparedWrite, options?: RequestOptions): Promise<BatchResult>;
|
|
70
|
-
writeAtomic(statements: Statement[], options?: WriteOptions): Promise<BatchResult>;
|
|
71
|
-
close(): Promise<void>;
|
|
72
|
-
}
|
|
73
|
-
export class RemoteDatabase implements Client {
|
|
74
|
-
private constructor();
|
|
75
|
-
readonly url: string;
|
|
76
|
-
/** True for a Cloud database. A loopback organization URL (romedb serve --directory) is not Cloud once info() has run. */
|
|
77
|
-
readonly cloud: boolean;
|
|
78
|
-
/** True once a reply has given the database's current sequence. */
|
|
79
|
-
readonly sequenceKnown: boolean;
|
|
80
|
-
/** True when connect() skipped the details request (the default for Cloud URLs). */
|
|
81
|
-
readonly deferred: boolean;
|
|
82
|
-
/** Ask Cloud to start the database's container and open the database without running a query. */
|
|
83
|
-
warm(options?: RequestOptions): Promise<WarmResult>;
|
|
84
|
-
/**
|
|
85
|
-
* Download the database as a .romedb file (romedb serve and serve --directory; not Cloud yet).
|
|
86
|
-
* timeoutMs covers building the copy, until the file starts arriving; signal also stops the download.
|
|
87
|
-
*/
|
|
88
|
-
exportFile(options?: FileExportOptions): Promise<DatabaseFile>;
|
|
89
|
-
readonly sequence: number;
|
|
90
|
-
readonly databaseId: string;
|
|
91
|
-
info(options?: RequestOptions): Promise<DatabaseInfo>;
|
|
92
|
-
read(statements: Statement[], options?: ReadOptions): Promise<BatchResult>;
|
|
93
|
-
prepareWrite(statements: Statement[], options?: WriteOptions): PreparedWrite;
|
|
94
|
-
submit(request: PreparedWrite, options?: RequestOptions): Promise<BatchResult>;
|
|
95
|
-
recover(request: PreparedWrite, options?: RequestOptions): Promise<BatchResult>;
|
|
96
|
-
writeAtomic(statements: Statement[], options?: WriteOptions): Promise<BatchResult>;
|
|
97
|
-
close(): Promise<void>;
|
|
98
|
-
}
|
|
99
|
-
export function connect(options: ConnectOptions): Promise<RemoteDatabase>;
|
|
100
|
-
export function normalizeUrl(url: string): string;
|
|
101
|
-
export function validatePrepared(request: unknown): { requestId: string; afterSequence: number; ifSequence?: number; statements: unknown[] };
|
|
102
|
-
/** Node.js only. Elsewhere it rejects; store the PreparedWrite yourself. */
|
|
103
|
-
export function saveRequest(path: string, request: PreparedWrite): Promise<void>;
|
|
104
|
-
/** Node.js only. Elsewhere it rejects. */
|
|
105
|
-
export function loadRequest(path: string): Promise<PreparedWrite>;
|
|
1
|
+
export * from '@romedb/client';
|
package/client.mjs
CHANGED
|
@@ -1,502 +1 @@
|
|
|
1
|
-
|
|
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, checkCopyOptions } = 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 DATABASE_FILE_TYPE = 'application/vnd.romedb.database';
|
|
16
|
-
const construction = Symbol('RemoteDatabase');
|
|
17
|
-
const warmAtConnect = Symbol('warmAtConnect');
|
|
18
|
-
const WARM_STATES = new Set(['ready', 'starting', 'skipped']);
|
|
19
|
-
const encoder = new TextEncoder();
|
|
20
|
-
|
|
21
|
-
class ClientError extends Error {
|
|
22
|
-
constructor(code, message, outcome, details = {}) {
|
|
23
|
-
const { cause, ...fields } = details;
|
|
24
|
-
super(message, cause === undefined ? undefined : { cause });
|
|
25
|
-
this.name = 'ClientError';
|
|
26
|
-
this.code = code;
|
|
27
|
-
this.outcome = outcome;
|
|
28
|
-
Object.assign(this, fields);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function connect(options) {
|
|
33
|
-
const db = new RemoteDatabase(options, construction);
|
|
34
|
-
db[warmAtConnect]();
|
|
35
|
-
if (db.deferred) return db;
|
|
36
|
-
try { await db.info(); return db; }
|
|
37
|
-
catch (error) { await db.close(); throw error; }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
class RemoteDatabase {
|
|
41
|
-
#url; #token; #fetch; #timeout; #sequence = 0; #id; #capabilities; #transport;
|
|
42
|
-
#closed = false; #active = new Set(); #inflightBytes = 0; #sequenceKnown = false; #deferred = false; #cloud = false; #warm = false; #loopbackOrganization = false;
|
|
43
|
-
constructor(options, key, transport) {
|
|
44
|
-
if (key !== construction) throw new TypeError('use connect()');
|
|
45
|
-
if (!plainObject(options)) throw new TypeError('connection options require an object');
|
|
46
|
-
this.#url = transport ? normalizeFileUrl(options.url) : normalizeUrl(options.url);
|
|
47
|
-
this.#transport = transport;
|
|
48
|
-
if (typeof options.token !== 'string' || !/^[\x21-\x7e]{1,4096}$/.test(options.token)) throw new TypeError('token requires a bounded bearer credential');
|
|
49
|
-
this.#token = options.token;
|
|
50
|
-
const fetch = options.fetch ?? globalThis.fetch;
|
|
51
|
-
if (typeof fetch !== 'function') throw new TypeError('fetch must be a function');
|
|
52
|
-
// Workers refuse fetch called on another object, so the default keeps globalThis.
|
|
53
|
-
this.#fetch = options.fetch ?? fetch.bind(globalThis);
|
|
54
|
-
this.#timeout = timeout(options.timeoutMs ?? 20_000);
|
|
55
|
-
if (options.deferInfo !== undefined && typeof options.deferInfo !== 'boolean') throw new TypeError('deferInfo requires a boolean');
|
|
56
|
-
if (options.warm !== undefined && typeof options.warm !== 'boolean') throw new TypeError('warm requires a boolean');
|
|
57
|
-
const cloudId = transport ? undefined : /\/databases\/([0-9a-f-]{36})$/.exec(this.#url)?.[1];
|
|
58
|
-
// A loopback organization URL is a local service directory or Cloud in development;
|
|
59
|
-
// the details reply tells which, so these connections always read details first.
|
|
60
|
-
this.#loopbackOrganization = cloudId !== undefined && loopbackHost(new URL(this.#url).hostname);
|
|
61
|
-
this.#cloud = cloudId !== undefined && !this.#loopbackOrganization;
|
|
62
|
-
if (cloudId === undefined && options.deferInfo === true) throw new TypeError('deferInfo applies only to Cloud database URLs');
|
|
63
|
-
if (cloudId === undefined && options.warm === true) throw new TypeError('warm applies only to Cloud database URLs');
|
|
64
|
-
// Cloud database URLs carry the database id and have documented capabilities,
|
|
65
|
-
// so the details request waits until a write needs a current sequence.
|
|
66
|
-
if (this.#cloud && options.deferInfo !== false) {
|
|
67
|
-
this.#id = cloudId; this.#capabilities = capabilities(this.#url, {}, false); this.#deferred = true;
|
|
68
|
-
}
|
|
69
|
-
this.#warm = this.#cloud && options.warm !== false;
|
|
70
|
-
}
|
|
71
|
-
[warmAtConnect]() {
|
|
72
|
-
if (this.#warm) this.warm().catch(() => {});
|
|
73
|
-
}
|
|
74
|
-
get deferred() { return this.#deferred; }
|
|
75
|
-
get url() { return this.#url; }
|
|
76
|
-
get cloud() { return this.#cloud; }
|
|
77
|
-
get sequence() { return this.#sequence; }
|
|
78
|
-
get sequenceKnown() { return this.#sequenceKnown; }
|
|
79
|
-
get databaseId() { return this.#id; }
|
|
80
|
-
async info(options = {}) {
|
|
81
|
-
const info = await this.#request('', undefined, options, false);
|
|
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)) {
|
|
83
|
-
throw new ClientError('RDB_PROTOCOL', 'Invalid database details or changed database identity', 'read-failed');
|
|
84
|
-
}
|
|
85
|
-
if (this.#loopbackOrganization) this.#cloud = info.serviceVersion === undefined;
|
|
86
|
-
const supported = capabilities(this.#url, info, !this.#cloud);
|
|
87
|
-
this.#id = info.id;
|
|
88
|
-
this.#sequence = Math.max(this.#sequence, info.sequence);
|
|
89
|
-
this.#sequenceKnown = true;
|
|
90
|
-
this.#capabilities = supported;
|
|
91
|
-
return { ...info, capabilities: supported };
|
|
92
|
-
}
|
|
93
|
-
async warm(options = {}) {
|
|
94
|
-
this.#assertOpen();
|
|
95
|
-
if (!this.#cloud) throw new ClientError('RDB_UNSUPPORTED', 'warm applies only to Cloud database URLs', 'not-sent');
|
|
96
|
-
const reply = await this.#request('/warm', '{}', options, false);
|
|
97
|
-
if (!plainObject(reply) || !WARM_STATES.has(reply.state)) throw new ClientError('RDB_PROTOCOL', 'Invalid warm reply', 'read-failed');
|
|
98
|
-
return { state: reply.state };
|
|
99
|
-
}
|
|
100
|
-
async read(statements, options = {}) {
|
|
101
|
-
this.#assertOpen();
|
|
102
|
-
if (this.#capabilities.readVersion !== 2) throw new ClientError('RDB_UNSUPPORTED', 'Database does not support fresh reads', 'not-sent');
|
|
103
|
-
if (options.ifSequence !== undefined) throw new TypeError('fresh reads do not accept ifSequence');
|
|
104
|
-
const afterSequence = options.afterSequence ?? this.#sequence;
|
|
105
|
-
checkSequence(afterSequence);
|
|
106
|
-
const workLimits = encodeWorkLimits(options.workLimits);
|
|
107
|
-
const queryVersion = this.#queryVersion();
|
|
108
|
-
const body = JSON.stringify({ readVersion: 2, requestId: crypto.randomUUID(), afterSequence, statements: encodeStatements(statements, queryVersion), ...(workLimits === undefined ? {} : { workLimits }) });
|
|
109
|
-
checkBodySize(body, queryVersion);
|
|
110
|
-
const request = JSON.parse(body);
|
|
111
|
-
const reply = await this.#request('/read', body, options, false, undefined, queryVersion);
|
|
112
|
-
const result = confirmReply(reply, request, true);
|
|
113
|
-
this.#sequence = Math.max(this.#sequence, result.sequence);
|
|
114
|
-
this.#sequenceKnown = true;
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
prepareWrite(statements, options = {}) {
|
|
118
|
-
this.#assertOpen();
|
|
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');
|
|
121
|
-
const afterSequence = options.afterSequence ?? this.#sequence;
|
|
122
|
-
checkSequence(afterSequence);
|
|
123
|
-
if (options.ifSequence !== undefined) {
|
|
124
|
-
if (this.#capabilities.exactSequenceCondition !== true) throw new ClientError('RDB_UNSUPPORTED', 'Database does not support exact sequence conditions', 'not-sent');
|
|
125
|
-
checkSequence(options.ifSequence);
|
|
126
|
-
if (options.ifSequence < afterSequence) throw new RangeError('ifSequence must be at least afterSequence');
|
|
127
|
-
}
|
|
128
|
-
const requestId = options.requestId ?? crypto.randomUUID();
|
|
129
|
-
if (typeof requestId !== 'string' || !/^[A-Za-z0-9_-]{1,80}$/.test(requestId)) throw new TypeError('requestId requires 1..80 letters, digits, underscores or hyphens');
|
|
130
|
-
const workLimits = encodeWorkLimits(options.workLimits);
|
|
131
|
-
const queryVersion = this.#queryVersion();
|
|
132
|
-
const body = JSON.stringify({ requestId, afterSequence, ...(options.ifSequence === undefined ? {} : { ifSequence: options.ifSequence }), statements: encodeStatements(statements, queryVersion), ...(workLimits === undefined ? {} : { workLimits }) });
|
|
133
|
-
checkBodySize(body, queryVersion);
|
|
134
|
-
return Object.freeze({ version: 2, url: this.#url, databaseId: this.#id, queryVersion, body });
|
|
135
|
-
}
|
|
136
|
-
async submit(prepared, options = {}) {
|
|
137
|
-
this.#assertOpen();
|
|
138
|
-
const request = validatePrepared(prepared);
|
|
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');
|
|
141
|
-
try {
|
|
142
|
-
const reply = await this.#request('/query', prepared.body, options, true, prepared, preparedQueryVersion(prepared));
|
|
143
|
-
const result = confirmReply(reply, request, false, prepared);
|
|
144
|
-
this.#sequence = Math.max(this.#sequence, result.sequence);
|
|
145
|
-
this.#sequenceKnown = true;
|
|
146
|
-
return result;
|
|
147
|
-
} catch (error) {
|
|
148
|
-
if (error instanceof ClientError) {
|
|
149
|
-
if (error.outcome === 'rolled-back') this.#sequence = Math.max(this.#sequence, error.sequence);
|
|
150
|
-
throw error;
|
|
151
|
-
}
|
|
152
|
-
throw new ClientError('RDB_PROTOCOL', 'Write response could not be confirmed; recover the saved request', 'unknown', { request: prepared });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
async recover(prepared, options = {}) {
|
|
156
|
-
try { return await this.submit(prepared, options); }
|
|
157
|
-
catch (error) {
|
|
158
|
-
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 });
|
|
159
|
-
throw error;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
async writeAtomic(statements, options = {}) {
|
|
163
|
-
if (!this.#sequenceKnown && this.#deferred && options.afterSequence === undefined) await this.info(options);
|
|
164
|
-
return this.submit(this.prepareWrite(statements, options), options);
|
|
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 (!plainObject(options) || Object.keys(options).some(key => !['signal', 'timeoutMs', 'emptyTables', 'newIdentity'].includes(key))) throw new TypeError('export options take signal, timeoutMs, emptyTables and newIdentity');
|
|
170
|
-
checkCopyOptions(options);
|
|
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' + fileQuery(options), {
|
|
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
|
-
}
|
|
223
|
-
async close() {
|
|
224
|
-
this.#closed = true;
|
|
225
|
-
for (const controller of this.#active) controller.abort();
|
|
226
|
-
this.#token = '';
|
|
227
|
-
}
|
|
228
|
-
#assertOpen() {
|
|
229
|
-
if (this.#closed) throw new ClientError('RDB_CLOSED', 'Database client is closed', 'not-sent');
|
|
230
|
-
}
|
|
231
|
-
#queryVersion() { return this.#cloud ? CLOUD_QUERY_VERSION : 2; }
|
|
232
|
-
async #request(path, body, options, writing, prepared, queryVersion = 2) {
|
|
233
|
-
this.#assertOpen();
|
|
234
|
-
const duration = timeout(options.timeoutMs ?? this.#timeout);
|
|
235
|
-
if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) throw new TypeError('signal must be an AbortSignal');
|
|
236
|
-
if (options.signal?.aborted) throw new ClientError('RDB_CANCELLED', 'Request cancelled before dispatch', 'not-sent', prepared ? { request: prepared } : {});
|
|
237
|
-
const bodyBytes = body === undefined ? 0 : byteLength(body);
|
|
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 } : {});
|
|
239
|
-
const controller = new AbortController();
|
|
240
|
-
const abort = () => controller.abort();
|
|
241
|
-
options.signal?.addEventListener('abort', abort, { once: true });
|
|
242
|
-
const timer = setTimeout(abort, duration);
|
|
243
|
-
this.#active.add(controller);
|
|
244
|
-
this.#inflightBytes += bodyBytes;
|
|
245
|
-
const credential = this.#token;
|
|
246
|
-
const fetch = this.#fetch;
|
|
247
|
-
let status;
|
|
248
|
-
try {
|
|
249
|
-
if (this.#transport) return await this.#transport(path, body, controller.signal, duration);
|
|
250
|
-
const send = () => fetch(this.#url + path, {
|
|
251
|
-
method: body === undefined ? 'GET' : 'POST',
|
|
252
|
-
headers: { authorization: `Bearer ${credential}`, accept: 'application/json', ...(body === undefined ? {} : { 'content-type': 'application/json' }), ...(this.#id === undefined ? {} : { 'x-romedb-database-id': prepared?.databaseId ?? this.#id }),
|
|
253
|
-
...(queryVersion === CLOUD_QUERY_VERSION && (path === '/query' || path === '/read') ? { 'x-romedb-query-version': String(queryVersion) } : {}) },
|
|
254
|
-
...(body === undefined ? {} : { body }),
|
|
255
|
-
// Workers have no redirect: 'error'; a redirect is refused below, never followed with the credential.
|
|
256
|
-
redirect: 'manual', signal: controller.signal,
|
|
257
|
-
});
|
|
258
|
-
let response = await send();
|
|
259
|
-
if (await queueRefused(response)) {
|
|
260
|
-
await pause(queueRetryDelay(response), controller.signal);
|
|
261
|
-
response = await send();
|
|
262
|
-
}
|
|
263
|
-
status = response.status;
|
|
264
|
-
if (response.type === 'opaqueredirect' || (status >= 300 && status < 400)) {
|
|
265
|
-
await response.body?.cancel().catch(() => {});
|
|
266
|
-
throw new ClientError('RDB_PROTOCOL', 'Database URL redirected; redirects are never followed with a credential', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
|
|
267
|
-
}
|
|
268
|
-
const retryAfterMs = retryAfter(response.headers.get('retry-after'));
|
|
269
|
-
const replyVersion = response.headers.get('x-romedb-query-version');
|
|
270
|
-
if ((path === '/query' || path === '/read') && replyVersion !== null && replyVersion !== '2' && replyVersion !== String(queryVersion)) {
|
|
271
|
-
await response.body?.cancel();
|
|
272
|
-
throw new ClientError('RDB_PROTOCOL', 'Unsupported database query response version', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
|
|
273
|
-
}
|
|
274
|
-
const reply = await boundedJson(response, MAX_RESPONSE);
|
|
275
|
-
if (plainObject(reply?.error) && typeof reply.error.message === 'string') reply.error.message = redactMessage(reply.error.message, credential);
|
|
276
|
-
if (!response.ok && !(writing && reply?.requestId && reply?.sequence !== undefined)) {
|
|
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);
|
|
278
|
-
throw new ClientError(code, safeMessage(reply, credential), writing ? 'unknown' : 'read-failed', { status, ...(retryAfterMs === undefined ? {} : { retryAfterMs }), ...(prepared ? { request: prepared } : {}) });
|
|
279
|
-
}
|
|
280
|
-
return reply;
|
|
281
|
-
} catch (error) {
|
|
282
|
-
if (error instanceof ClientError) {
|
|
283
|
-
const outcome = writing
|
|
284
|
-
? this.#transport && error.outcome === 'not-sent' ? 'not-sent' : 'unknown'
|
|
285
|
-
: error.outcome === 'not-sent' ? 'not-sent' : 'read-failed';
|
|
286
|
-
const message = this.#transport ? error.message.slice(0, 2048) : redactMessage(error.message, credential);
|
|
287
|
-
throw new ClientError(error.code, message, outcome, {
|
|
288
|
-
...(error.status === undefined ? {} : { status: error.status }),
|
|
289
|
-
...(error.retryAfterMs === undefined ? {} : { retryAfterMs: error.retryAfterMs }),
|
|
290
|
-
...(prepared ? { request: prepared } : {}),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
const code = controller.signal.aborted ? 'RDB_CANCELLED' : status === undefined ? 'RDB_TRANSPORT' : 'RDB_PROTOCOL';
|
|
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 });
|
|
295
|
-
} finally {
|
|
296
|
-
clearTimeout(timer);
|
|
297
|
-
options.signal?.removeEventListener('abort', abort);
|
|
298
|
-
this.#active.delete(controller);
|
|
299
|
-
this.#inflightBytes -= bodyBytes;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
require('./client-internal.cjs').createEmbeddedClient = async (options, transport) => {
|
|
305
|
-
const database = new RemoteDatabase({ ...options, token: 'embedded' }, construction, transport);
|
|
306
|
-
try { await database.info(); return database; }
|
|
307
|
-
catch (error) { await database.close(); throw error; }
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
function normalizeFileUrl(input) {
|
|
311
|
-
if (typeof input !== 'string' || input.length > 4096) throw new TypeError('file URL requires a bounded local path');
|
|
312
|
-
const url = new URL(input);
|
|
313
|
-
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');
|
|
314
|
-
return url.href;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function normalizeUrl(input) {
|
|
318
|
-
if (typeof input !== 'string' || input.length > 4096) throw new TypeError('url requires a bounded database URL');
|
|
319
|
-
// romedb:// is an alias for https://; the same checks apply after the rewrite.
|
|
320
|
-
const url = new URL(input.startsWith('romedb://') ? 'https://' + input.slice('romedb://'.length) : input);
|
|
321
|
-
const loopback = loopbackHost(url.hostname);
|
|
322
|
-
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');
|
|
323
|
-
let path = url.pathname.replace(/\/$/, '').replace(/\/(?:query|read)$/, '');
|
|
324
|
-
if (loopback && !path) path = '/v1/database';
|
|
325
|
-
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
|
|
326
|
-
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');
|
|
327
|
-
return url.origin + path;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function encodeStatements(statements, queryVersion = 2) {
|
|
331
|
-
if (!Array.isArray(statements) || statements.length < 1 || statements.length > 32) throw new RangeError('request requires 1..32 statements');
|
|
332
|
-
return statements.map(statement => {
|
|
333
|
-
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');
|
|
334
|
-
if (statement.params === undefined) return { sql: text(statement.sql) };
|
|
335
|
-
if (!plainObject(statement.params)) throw new TypeError('params require an object');
|
|
336
|
-
return { sql: text(statement.sql), params: Object.fromEntries(Object.entries(statement.params).map(([key, value]) => [text(key), encodeValue(value, 0, queryVersion === CLOUD_QUERY_VERSION)])) };
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function validatePrepared(prepared) {
|
|
341
|
-
const fields = prepared?.version === 1 ? 'body,databaseId,url,version' : 'body,databaseId,queryVersion,url,version';
|
|
342
|
-
if (!plainObject(prepared) || Object.keys(prepared).sort().join(',') !== fields || ![1, 2].includes(prepared.version) ||
|
|
343
|
-
(prepared.version === 2 && ![2, CLOUD_QUERY_VERSION].includes(prepared.queryVersion)) || typeof prepared.databaseId !== 'string' || !prepared.databaseId ||
|
|
344
|
-
(prepared.url?.startsWith('file:') ? normalizeFileUrl(prepared.url) : normalizeUrl(prepared.url)) !== prepared.url || typeof prepared.body !== 'string') throw new TypeError('invalid saved request');
|
|
345
|
-
checkBodySize(prepared.body, preparedQueryVersion(prepared));
|
|
346
|
-
const body = JSON.parse(prepared.body);
|
|
347
|
-
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');
|
|
348
|
-
encodeWorkLimits(body.workLimits);
|
|
349
|
-
checkSequence(body.afterSequence);
|
|
350
|
-
if (body.ifSequence !== undefined) {
|
|
351
|
-
checkSequence(body.ifSequence);
|
|
352
|
-
if (body.ifSequence < body.afterSequence) throw new RangeError('invalid saved sequence condition');
|
|
353
|
-
}
|
|
354
|
-
if (!Array.isArray(body.statements) || body.statements.length < 1 || body.statements.length > 32) throw new TypeError('invalid saved statements');
|
|
355
|
-
for (const statement of body.statements) {
|
|
356
|
-
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');
|
|
357
|
-
for (const value of Object.values(statement.params ?? {})) decodeValue(value);
|
|
358
|
-
}
|
|
359
|
-
return body;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function confirmReply(reply, request, reading, 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 } : {});
|
|
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();
|
|
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 });
|
|
366
|
-
if (!Array.isArray(reply.results) || reply.results.length !== request.statements.length || reply.error !== undefined || byteLength(JSON.stringify(reply.results)) > 262_144) throw invalid();
|
|
367
|
-
try {
|
|
368
|
-
const results = reply.results.map(result => {
|
|
369
|
-
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();
|
|
370
|
-
return { columns: result.columns, rows: result.rows.map(row => {
|
|
371
|
-
if (!Array.isArray(row) || row.length !== result.columns.length) throw invalid();
|
|
372
|
-
return row.map(value => decodeValue(value));
|
|
373
|
-
}), affected: BigInt(result.affected) };
|
|
374
|
-
});
|
|
375
|
-
return { requestId: reply.requestId, sequence: reply.sequence, results, ...(reading ? { readVersion: 2 } : {}), ...(reply.executionWork === undefined ? {} : { executionWork: reply.executionWork }) };
|
|
376
|
-
} catch { throw invalid(); }
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
async function boundedJson(response, limit) {
|
|
380
|
-
if (!/^application\/json(?:\s*;|$)/i.test(response.headers.get('content-type') ?? '')) throw new TypeError('response requires JSON');
|
|
381
|
-
if (!response.body) throw new TypeError('response body is missing');
|
|
382
|
-
const reader = response.body.getReader();
|
|
383
|
-
const chunks = [];
|
|
384
|
-
let length = 0;
|
|
385
|
-
try {
|
|
386
|
-
for (;;) {
|
|
387
|
-
const { done, value } = await reader.read();
|
|
388
|
-
if (done) break;
|
|
389
|
-
length += value.byteLength;
|
|
390
|
-
if (length > limit) throw new RangeError('response exceeds client byte limit');
|
|
391
|
-
chunks.push(value);
|
|
392
|
-
}
|
|
393
|
-
const bytes = new Uint8Array(length);
|
|
394
|
-
let offset = 0;
|
|
395
|
-
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
396
|
-
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
397
|
-
} finally { await reader.cancel().catch(() => {}); }
|
|
398
|
-
}
|
|
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
|
-
|
|
428
|
-
function loopbackHost(hostname) { return ['127.0.0.1', 'localhost', '[::1]'].includes(hostname); }
|
|
429
|
-
|
|
430
|
-
function capabilities(url, info, local) {
|
|
431
|
-
if (local) {
|
|
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');
|
|
433
|
-
return { ...info.capabilities, provenance: 'negotiated' };
|
|
434
|
-
}
|
|
435
|
-
return { provenance: 'documented', readVersion: 2, atomicWrites: true, replayWindow: 64, exactSequenceCondition: true, negotiatedLimits: false };
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function sequence(value, reply = false) { return Number.isSafeInteger(value) && value >= 0 && value <= (reply ? MAX_SEQUENCE + 1 : MAX_SEQUENCE); }
|
|
439
|
-
function encodeWorkLimits(value) {
|
|
440
|
-
if (value === undefined) return undefined;
|
|
441
|
-
const ceilings = { rowsRead: 1_000_000, rowMutations: 100_000, vmSteps: 10_000_000 };
|
|
442
|
-
if (!plainObject(value)) throw new TypeError('workLimits require an object');
|
|
443
|
-
for (const [name, limit] of Object.entries(value)) {
|
|
444
|
-
if (!Object.hasOwn(ceilings, name) || !Number.isSafeInteger(limit) || limit < 0 || limit > ceilings[name]) throw new RangeError('workLimits require nonnegative integers within the service ceilings');
|
|
445
|
-
}
|
|
446
|
-
return Object.fromEntries(Object.keys(ceilings).filter(key => Object.hasOwn(value, key)).map(key => [key, value[key]]));
|
|
447
|
-
}
|
|
448
|
-
function checkSequence(value) { if (!sequence(value)) throw new RangeError('sequence requires a nonnegative safe integer below Number.MAX_SAFE_INTEGER'); }
|
|
449
|
-
function timeout(value) { if (!Number.isInteger(value) || value < 1 || value > 120_000) throw new RangeError('timeoutMs must be 1..120000'); return value; }
|
|
450
|
-
function byteLength(text) { return encoder.encode(text).byteLength; }
|
|
451
|
-
function checkBodySize(body, queryVersion = 2) {
|
|
452
|
-
if (byteLength(body) > (queryVersion === CLOUD_QUERY_VERSION ? MAX_CLOUD_REQUEST : MAX_REQUEST))
|
|
453
|
-
throw new RangeError(queryVersion === CLOUD_QUERY_VERSION ? 'request exceeds 1 MiB' : 'request exceeds 64 KiB');
|
|
454
|
-
}
|
|
455
|
-
function preparedQueryVersion(prepared) { return prepared.version === 1 ? 2 : prepared.queryVersion; }
|
|
456
|
-
// Cloud refuses a queued request with 503 query_queue_* before dispatching it,
|
|
457
|
-
// so nothing ran and one retry is safe for reads and writes alike.
|
|
458
|
-
async function queueRefused(response) {
|
|
459
|
-
if (response.status !== 503) return false;
|
|
460
|
-
try { return /^query_queue_[a-z_]{1,40}$/.test((await response.clone().json())?.code ?? ''); } catch { return false; }
|
|
461
|
-
}
|
|
462
|
-
function queueRetryDelay(response) {
|
|
463
|
-
const header = response.headers.get('retry-after');
|
|
464
|
-
return header === null ? 0 : Math.min(retryAfter(header) ?? 1000, 2000);
|
|
465
|
-
}
|
|
466
|
-
// Retry-After holds delay seconds or an HTTP date.
|
|
467
|
-
function retryAfter(value) {
|
|
468
|
-
if (value === null) return undefined;
|
|
469
|
-
if (/^\s*\d{1,9}(?:\.\d+)?\s*$/.test(value)) return Math.round(Number(value) * 1000);
|
|
470
|
-
const date = /GMT\s*$/.test(value) ? Date.parse(value) : NaN;
|
|
471
|
-
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
472
|
-
}
|
|
473
|
-
function pause(ms, signal) {
|
|
474
|
-
return new Promise((resolve, reject) => {
|
|
475
|
-
if (signal.aborted) { reject(signal.reason); return; }
|
|
476
|
-
const timer = setTimeout(() => { signal.removeEventListener('abort', stop); resolve(); }, ms);
|
|
477
|
-
const stop = () => { clearTimeout(timer); reject(signal.reason); };
|
|
478
|
-
signal.addEventListener('abort', stop, { once: true });
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
function fileQuery({ emptyTables, newIdentity }) {
|
|
482
|
-
const parts = [];
|
|
483
|
-
if (emptyTables?.length) parts.push(`empty=${emptyTables.map(encodeURIComponent).join(',')}`);
|
|
484
|
-
if (newIdentity) parts.push('identity=new');
|
|
485
|
-
return parts.length ? `?${parts.join('&')}` : '';
|
|
486
|
-
}
|
|
487
|
-
function httpCode(status) { return status === 401 ? 'RDB_AUTH' : status === 403 ? 'RDB_FORBIDDEN' : status === 409 ? 'RDB_CONFLICT' : status === 429 || status === 413 ? 'RDB_LIMIT' : 'RDB_REMOTE'; }
|
|
488
|
-
function redactMessage(message, token) { return (token ? message.replaceAll(token, '[redacted]') : message).slice(0, 2048); }
|
|
489
|
-
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); }
|
|
490
|
-
|
|
491
|
-
async function saveRequest(path, request) { return require('./request-journal.cjs').saveRequest(path, request, validatePrepared); }
|
|
492
|
-
async function loadRequest(path) { return require('./request-journal.cjs').loadRequest(path, validatePrepared); }
|
|
493
|
-
|
|
494
|
-
module.exports = { connect, RemoteDatabase, ClientError, Float64, Record, Vector, encodeValue, decodeValue, normalizeUrl, validatePrepared, saveRequest, loadRequest };
|
|
495
|
-
})(commonjs, commonjs.exports, specifier => {
|
|
496
|
-
if (specifier === './values.cjs') return module0;
|
|
497
|
-
if (specifier === './client-internal.cjs') return {};
|
|
498
|
-
if (specifier === './request-journal.cjs') throw new TypeError("saveRequest and loadRequest need Node.js file access; store the PreparedWrite yourself");
|
|
499
|
-
throw new TypeError(`Cannot load ${specifier}`);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
export const { connect, RemoteDatabase, ClientError, Float64, Record, Vector, encodeValue, decodeValue, normalizeUrl, validatePrepared, saveRequest, loadRequest } = commonjs.exports;
|
|
1
|
+
export * from '@romedb/client';
|
package/embedded-client.cjs
CHANGED
|
@@ -8,7 +8,7 @@ const { pathToFileURL } = require('node:url');
|
|
|
8
8
|
const common = require('./client.cjs');
|
|
9
9
|
const { connect, RemoteDatabase, ClientError, Float64, Record, Vector, encodeValue, decodeValue, normalizeUrl, validatePrepared, saveRequest, loadRequest } = common;
|
|
10
10
|
const { plainObject, checkCopyOptions } = require('./values.cjs');
|
|
11
|
-
const { createEmbeddedClient } = require('
|
|
11
|
+
const { createEmbeddedClient } = require('@romedb/client/internal');
|
|
12
12
|
const native = require('./native.cjs');
|
|
13
13
|
const sessions = new Map();
|
|
14
14
|
const construction = Symbol('EmbeddedDatabase');
|