@romedb/node 0.2.13 → 0.2.15
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 +40 -10
- package/THIRD_PARTY_CRATE_NOTICES.md +4 -4
- package/client.cjs +1 -482
- package/client.d.ts +1 -93
- package/client.mjs +1 -496
- package/embedded-client.cjs +5 -4
- package/embedded-client.d.ts +2 -2
- package/embedded-worker.cjs +2 -1
- package/index.cjs +6 -1
- package/index.d.ts +7 -1
- package/package.json +5 -4
- package/romedb.node +2 -2
- package/values.cjs +1 -210
- package/values.d.ts +1 -15
- package/values.mjs +1 -220
- package/worker.cjs +1 -1
- package/client-internal.cjs +0 -2
- package/request-journal.cjs +0 -47
package/README.md
CHANGED
|
@@ -23,9 +23,12 @@ version, 0.2.12, differs from its asset only in `package.json` and this README.
|
|
|
23
23
|
project can also depend on the asset of a GitHub release by URL:
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
|
-
pnpm add https://github.com/romedb/romedb/releases/download/romedb-v0.2.
|
|
26
|
+
pnpm add https://github.com/romedb/romedb/releases/download/romedb-v0.2.15/romedb-node-0.2.15.tgz
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
From 0.2.15, `@romedb/node` depends on `@romedb/client` at the same version, so that
|
|
30
|
+
asset also needs npm or the `romedb-client` asset of the same release.
|
|
31
|
+
|
|
29
32
|
## A first database
|
|
30
33
|
|
|
31
34
|
```js
|
|
@@ -125,7 +128,7 @@ own connection.
|
|
|
125
128
|
| `boolean`, `null`, `string` | The same type |
|
|
126
129
|
| Array, plain object | Array, object |
|
|
127
130
|
| `Buffer`, `Uint8Array` | Binary |
|
|
128
|
-
| `Record(table, key)` | A record ID. The key is a string or a `bigint`. |
|
|
131
|
+
| `Record(table, key)` | A record ID. The key is a string or a `bigint`. `String(record)` gives `table:key`, and `record.equals(other)` compares the table and the key. |
|
|
129
132
|
| `Vector` | A vector of validated native-format bytes. See [typed vectors](#typed-vectors). |
|
|
130
133
|
|
|
131
134
|
RomeDB never makes a reference from a string. Unsupported JavaScript types,
|
|
@@ -702,8 +705,30 @@ it. Exports have no deadline unless you pass `timeoutMs`. `signal`, `timeoutMs`
|
|
|
702
705
|
`close()` cancel a running export. It then rejects with `RDB_CANCELLED`, unless it
|
|
703
706
|
finished first.
|
|
704
707
|
|
|
705
|
-
|
|
706
|
-
|
|
708
|
+
#### Leaving data out of a copy
|
|
709
|
+
|
|
710
|
+
A whole copy holds everything, including sessions and tokens. Use `emptyTables` when
|
|
711
|
+
someone else gets the file:
|
|
712
|
+
|
|
713
|
+
```js
|
|
714
|
+
await rql.exportFile('site.romedb', { emptyTables: ['cms_auth_sessions', 'cms_cache'] });
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
- The named tables keep their schema and indexes, but no rows.
|
|
718
|
+
- Their rows are never read, so they cannot reach the file. They are not in freed
|
|
719
|
+
pages, a log or a receipt.
|
|
720
|
+
- A name that is not a table fails with `RDB_VALIDATION`.
|
|
721
|
+
- An edge table must be emptied together with the tables it connects.
|
|
722
|
+
- Rows in other tables keep their references to the emptied rows.
|
|
723
|
+
- The copy also gets a new identity, because a receipt can hold row data.
|
|
724
|
+
|
|
725
|
+
Use `newIdentity: true` to copy every row into a new database, for example to
|
|
726
|
+
duplicate a site. The copy leaves out the database ID, the sequence and the receipts.
|
|
727
|
+
Its sequence starts at 0, and it gets a new ID the first time it opens.
|
|
728
|
+
|
|
729
|
+
The URL client has the streaming form, `rql.exportFile({ signal, timeoutMs, emptyTables,
|
|
730
|
+
newIdentity })`, for `romedb serve`, `serve --directory` and Cloud. On Cloud, set
|
|
731
|
+
`timeoutMs` high enough for the size of the database: a job builds the file first. See
|
|
707
732
|
[database files](https://github.com/romedb/romedb/blob/main/romedb/docs/service-protocol.md#database-files) for how the copy is
|
|
708
733
|
made.
|
|
709
734
|
|
|
@@ -790,8 +815,9 @@ Results use positional rows and keep duplicate columns. Integer results are `big
|
|
|
790
815
|
Number results are `Float64` instances with a `.value` number. They keep their exact
|
|
791
816
|
float type when you bind them again, integral floats and negative zero included.
|
|
792
817
|
|
|
793
|
-
This entry exports
|
|
794
|
-
|
|
818
|
+
This entry exports `Record`, `Vector` and the value codec of `@romedb/client`, which
|
|
819
|
+
`@romedb/node/embedded-client` also uses. The main `@romedb/node` entry has its own
|
|
820
|
+
`Record` and `Vector`, so do not pass those to this client. Encoded vectors and the dense float32, float64 and sparse-entry factories
|
|
795
821
|
work without native code. `null` is a present field. `undefined` and array holes are
|
|
796
822
|
refused, so no data disappears without an error.
|
|
797
823
|
|
|
@@ -833,9 +859,9 @@ From 0.2.12, `@romedb/node/client` and `@romedb/node/values` use only Web APIs:
|
|
|
833
859
|
`fetch`, `crypto.randomUUID`, `TextEncoder`, `AbortSignal`, `atob` and `btoa`. Node.js
|
|
834
860
|
loads the CommonJS files through the `node` export condition. Every other runtime and
|
|
835
861
|
bundler gets the ES modules `client.mjs` and `values.mjs`, for example Cloudflare
|
|
836
|
-
Workers, browsers and Deno with a bundler. They run the same code: `build-esm.cjs`
|
|
837
|
-
writes them from `client.cjs` and `values.cjs`, and the package tests
|
|
838
|
-
are out of date.
|
|
862
|
+
Workers, browsers and Deno with a bundler. They run the same code: `build-esm.cjs` in
|
|
863
|
+
`@romedb/client` writes them from `client.cjs` and `values.cjs`, and the package tests
|
|
864
|
+
fail when they are out of date.
|
|
839
865
|
|
|
840
866
|
Workers need no `nodejs_compat`. The client calls `fetch` without a `this` value, so
|
|
841
867
|
`globalThis.fetch` and your own `fetch` both work in Workers.
|
|
@@ -863,7 +889,11 @@ runtimes. There, store the `PreparedWrite` object yourself, for example in a Dur
|
|
|
863
889
|
Object.
|
|
864
890
|
|
|
865
891
|
The package also contains the native addon. So installing it for a Workers app also
|
|
866
|
-
downloads the addon, but the Workers bundle never includes it.
|
|
892
|
+
downloads the addon, but the Workers bundle never includes it. To skip that download,
|
|
893
|
+
install [`@romedb/client`](https://www.npmjs.com/package/@romedb/client). It holds this
|
|
894
|
+
code: `@romedb/node` depends on it, and `@romedb/node/client` and `@romedb/node/values`
|
|
895
|
+
pass it through. A project that uses both packages gets one set of value classes when
|
|
896
|
+
both are at the same version.
|
|
867
897
|
|
|
868
898
|
## Load errors
|
|
869
899
|
|
|
@@ -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: `
|
|
7
|
+
Target: `x86_64-unknown-linux-gnu`. Cargo.lock SHA-256: `600d6e7ecfe4d96e7ad02ea62ba679b7ae7e3c858b650adeb16693643bae6570`.
|
|
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: `092e973bbc52cf5defe48c0
|
|
|
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.
|
|
706
|
+
### romedb-node 0.2.15
|
|
707
707
|
- Workspace `LICENSE.md`: [source text b646f9ee8bcaf87e8de75153b9df7a2861c7ac445c87e741768b3c2bccf47bc5](#text-b646f9ee8bcaf87e8de75153b9df7a2861c7ac445c87e741768b3c2bccf47bc5)
|
|
708
708
|
|
|
709
|
-
### romedb 0.2.
|
|
709
|
+
### romedb 0.2.15
|
|
710
710
|
- Workspace `LICENSE.md`: [source text b646f9ee8bcaf87e8de75153b9df7a2861c7ac445c87e741768b3c2bccf47bc5](#text-b646f9ee8bcaf87e8de75153b9df7a2861c7ac445c87e741768b3c2bccf47bc5)
|
|
711
711
|
|
|
712
|
-
### romeql-parser 0.2.
|
|
712
|
+
### romeql-parser 0.2.15
|
|
713
713
|
- Workspace `LICENSE.md`: [source text b646f9ee8bcaf87e8de75153b9df7a2861c7ac445c87e741768b3c2bccf47bc5](#text-b646f9ee8bcaf87e8de75153b9df7a2861c7ac445c87e741768b3c2bccf47bc5)
|
|
714
714
|
|
|
715
715
|
### rquickjs-core 0.13.0
|
package/client.cjs
CHANGED
|
@@ -1,483 +1,2 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
|
|
3
|
-
const { Float64, Record, Vector, encodeValue, decodeValue, plainObject, text } = require('./values.cjs');
|
|
4
|
-
|
|
5
|
-
const MAX_REQUEST = 65_536;
|
|
6
|
-
const MAX_CLOUD_REQUEST = 1_048_576;
|
|
7
|
-
const CLOUD_QUERY_VERSION = 3;
|
|
8
|
-
const MAX_RESPONSE = 524_288;
|
|
9
|
-
const MAX_SEQUENCE = Number.MAX_SAFE_INTEGER - 1;
|
|
10
|
-
const DATABASE_FILE_TYPE = 'application/vnd.romedb.database';
|
|
11
|
-
const construction = Symbol('RemoteDatabase');
|
|
12
|
-
const warmAtConnect = Symbol('warmAtConnect');
|
|
13
|
-
const WARM_STATES = new Set(['ready', 'starting', 'skipped']);
|
|
14
|
-
const encoder = new TextEncoder();
|
|
15
|
-
|
|
16
|
-
class ClientError extends Error {
|
|
17
|
-
constructor(code, message, outcome, details = {}) {
|
|
18
|
-
const { cause, ...fields } = details;
|
|
19
|
-
super(message, cause === undefined ? undefined : { cause });
|
|
20
|
-
this.name = 'ClientError';
|
|
21
|
-
this.code = code;
|
|
22
|
-
this.outcome = outcome;
|
|
23
|
-
Object.assign(this, fields);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function connect(options) {
|
|
28
|
-
const db = new RemoteDatabase(options, construction);
|
|
29
|
-
db[warmAtConnect]();
|
|
30
|
-
if (db.deferred) return db;
|
|
31
|
-
try { await db.info(); return db; }
|
|
32
|
-
catch (error) { await db.close(); throw error; }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
class RemoteDatabase {
|
|
36
|
-
#url; #token; #fetch; #timeout; #sequence = 0; #id; #capabilities; #transport;
|
|
37
|
-
#closed = false; #active = new Set(); #inflightBytes = 0; #sequenceKnown = false; #deferred = false; #cloud = false; #warm = false; #loopbackOrganization = false;
|
|
38
|
-
constructor(options, key, transport) {
|
|
39
|
-
if (key !== construction) throw new TypeError('use connect()');
|
|
40
|
-
if (!plainObject(options)) throw new TypeError('connection options require an object');
|
|
41
|
-
this.#url = transport ? normalizeFileUrl(options.url) : normalizeUrl(options.url);
|
|
42
|
-
this.#transport = transport;
|
|
43
|
-
if (typeof options.token !== 'string' || !/^[\x21-\x7e]{1,4096}$/.test(options.token)) throw new TypeError('token requires a bounded bearer credential');
|
|
44
|
-
this.#token = options.token;
|
|
45
|
-
const fetch = options.fetch ?? globalThis.fetch;
|
|
46
|
-
if (typeof fetch !== 'function') throw new TypeError('fetch must be a function');
|
|
47
|
-
// Workers refuse fetch called on another object, so the default keeps globalThis.
|
|
48
|
-
this.#fetch = options.fetch ?? fetch.bind(globalThis);
|
|
49
|
-
this.#timeout = timeout(options.timeoutMs ?? 20_000);
|
|
50
|
-
if (options.deferInfo !== undefined && typeof options.deferInfo !== 'boolean') throw new TypeError('deferInfo requires a boolean');
|
|
51
|
-
if (options.warm !== undefined && typeof options.warm !== 'boolean') throw new TypeError('warm requires a boolean');
|
|
52
|
-
const cloudId = transport ? undefined : /\/databases\/([0-9a-f-]{36})$/.exec(this.#url)?.[1];
|
|
53
|
-
// A loopback organization URL is a local service directory or Cloud in development;
|
|
54
|
-
// the details reply tells which, so these connections always read details first.
|
|
55
|
-
this.#loopbackOrganization = cloudId !== undefined && loopbackHost(new URL(this.#url).hostname);
|
|
56
|
-
this.#cloud = cloudId !== undefined && !this.#loopbackOrganization;
|
|
57
|
-
if (cloudId === undefined && options.deferInfo === true) throw new TypeError('deferInfo applies only to Cloud database URLs');
|
|
58
|
-
if (cloudId === undefined && options.warm === true) throw new TypeError('warm applies only to Cloud database URLs');
|
|
59
|
-
// Cloud database URLs carry the database id and have documented capabilities,
|
|
60
|
-
// so the details request waits until a write needs a current sequence.
|
|
61
|
-
if (this.#cloud && options.deferInfo !== false) {
|
|
62
|
-
this.#id = cloudId; this.#capabilities = capabilities(this.#url, {}, false); this.#deferred = true;
|
|
63
|
-
}
|
|
64
|
-
this.#warm = this.#cloud && options.warm !== false;
|
|
65
|
-
}
|
|
66
|
-
[warmAtConnect]() {
|
|
67
|
-
if (this.#warm) this.warm().catch(() => {});
|
|
68
|
-
}
|
|
69
|
-
get deferred() { return this.#deferred; }
|
|
70
|
-
get url() { return this.#url; }
|
|
71
|
-
get cloud() { return this.#cloud; }
|
|
72
|
-
get sequence() { return this.#sequence; }
|
|
73
|
-
get sequenceKnown() { return this.#sequenceKnown; }
|
|
74
|
-
get databaseId() { return this.#id; }
|
|
75
|
-
async info(options = {}) {
|
|
76
|
-
const info = await this.#request('', undefined, options, false);
|
|
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)) {
|
|
78
|
-
throw new ClientError('RDB_PROTOCOL', 'Invalid database details or changed database identity', 'read-failed');
|
|
79
|
-
}
|
|
80
|
-
if (this.#loopbackOrganization) this.#cloud = info.serviceVersion === undefined;
|
|
81
|
-
const supported = capabilities(this.#url, info, !this.#cloud);
|
|
82
|
-
this.#id = info.id;
|
|
83
|
-
this.#sequence = Math.max(this.#sequence, info.sequence);
|
|
84
|
-
this.#sequenceKnown = true;
|
|
85
|
-
this.#capabilities = supported;
|
|
86
|
-
return { ...info, capabilities: supported };
|
|
87
|
-
}
|
|
88
|
-
async warm(options = {}) {
|
|
89
|
-
this.#assertOpen();
|
|
90
|
-
if (!this.#cloud) throw new ClientError('RDB_UNSUPPORTED', 'warm applies only to Cloud database URLs', 'not-sent');
|
|
91
|
-
const reply = await this.#request('/warm', '{}', options, false);
|
|
92
|
-
if (!plainObject(reply) || !WARM_STATES.has(reply.state)) throw new ClientError('RDB_PROTOCOL', 'Invalid warm reply', 'read-failed');
|
|
93
|
-
return { state: reply.state };
|
|
94
|
-
}
|
|
95
|
-
async read(statements, options = {}) {
|
|
96
|
-
this.#assertOpen();
|
|
97
|
-
if (this.#capabilities.readVersion !== 2) throw new ClientError('RDB_UNSUPPORTED', 'Database does not support fresh reads', 'not-sent');
|
|
98
|
-
if (options.ifSequence !== undefined) throw new TypeError('fresh reads do not accept ifSequence');
|
|
99
|
-
const afterSequence = options.afterSequence ?? this.#sequence;
|
|
100
|
-
checkSequence(afterSequence);
|
|
101
|
-
const workLimits = encodeWorkLimits(options.workLimits);
|
|
102
|
-
const queryVersion = this.#queryVersion();
|
|
103
|
-
const body = JSON.stringify({ readVersion: 2, requestId: crypto.randomUUID(), afterSequence, statements: encodeStatements(statements, queryVersion), ...(workLimits === undefined ? {} : { workLimits }) });
|
|
104
|
-
checkBodySize(body, queryVersion);
|
|
105
|
-
const request = JSON.parse(body);
|
|
106
|
-
const reply = await this.#request('/read', body, options, false, undefined, queryVersion);
|
|
107
|
-
const result = confirmReply(reply, request, true);
|
|
108
|
-
this.#sequence = Math.max(this.#sequence, result.sequence);
|
|
109
|
-
this.#sequenceKnown = true;
|
|
110
|
-
return result;
|
|
111
|
-
}
|
|
112
|
-
prepareWrite(statements, options = {}) {
|
|
113
|
-
this.#assertOpen();
|
|
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');
|
|
116
|
-
const afterSequence = options.afterSequence ?? this.#sequence;
|
|
117
|
-
checkSequence(afterSequence);
|
|
118
|
-
if (options.ifSequence !== undefined) {
|
|
119
|
-
if (this.#capabilities.exactSequenceCondition !== true) throw new ClientError('RDB_UNSUPPORTED', 'Database does not support exact sequence conditions', 'not-sent');
|
|
120
|
-
checkSequence(options.ifSequence);
|
|
121
|
-
if (options.ifSequence < afterSequence) throw new RangeError('ifSequence must be at least afterSequence');
|
|
122
|
-
}
|
|
123
|
-
const requestId = options.requestId ?? crypto.randomUUID();
|
|
124
|
-
if (typeof requestId !== 'string' || !/^[A-Za-z0-9_-]{1,80}$/.test(requestId)) throw new TypeError('requestId requires 1..80 letters, digits, underscores or hyphens');
|
|
125
|
-
const workLimits = encodeWorkLimits(options.workLimits);
|
|
126
|
-
const queryVersion = this.#queryVersion();
|
|
127
|
-
const body = JSON.stringify({ requestId, afterSequence, ...(options.ifSequence === undefined ? {} : { ifSequence: options.ifSequence }), statements: encodeStatements(statements, queryVersion), ...(workLimits === undefined ? {} : { workLimits }) });
|
|
128
|
-
checkBodySize(body, queryVersion);
|
|
129
|
-
return Object.freeze({ version: 2, url: this.#url, databaseId: this.#id, queryVersion, body });
|
|
130
|
-
}
|
|
131
|
-
async submit(prepared, options = {}) {
|
|
132
|
-
this.#assertOpen();
|
|
133
|
-
const request = validatePrepared(prepared);
|
|
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');
|
|
136
|
-
try {
|
|
137
|
-
const reply = await this.#request('/query', prepared.body, options, true, prepared, preparedQueryVersion(prepared));
|
|
138
|
-
const result = confirmReply(reply, request, false, prepared);
|
|
139
|
-
this.#sequence = Math.max(this.#sequence, result.sequence);
|
|
140
|
-
this.#sequenceKnown = true;
|
|
141
|
-
return result;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
if (error instanceof ClientError) {
|
|
144
|
-
if (error.outcome === 'rolled-back') this.#sequence = Math.max(this.#sequence, error.sequence);
|
|
145
|
-
throw error;
|
|
146
|
-
}
|
|
147
|
-
throw new ClientError('RDB_PROTOCOL', 'Write response could not be confirmed; recover the saved request', 'unknown', { request: prepared });
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
async recover(prepared, options = {}) {
|
|
151
|
-
try { return await this.submit(prepared, options); }
|
|
152
|
-
catch (error) {
|
|
153
|
-
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 });
|
|
154
|
-
throw error;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
async writeAtomic(statements, options = {}) {
|
|
158
|
-
if (!this.#sequenceKnown && this.#deferred && options.afterSequence === undefined) await this.info(options);
|
|
159
|
-
return this.submit(this.prepareWrite(statements, options), options);
|
|
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
|
-
}
|
|
218
|
-
async close() {
|
|
219
|
-
this.#closed = true;
|
|
220
|
-
for (const controller of this.#active) controller.abort();
|
|
221
|
-
this.#token = '';
|
|
222
|
-
}
|
|
223
|
-
#assertOpen() {
|
|
224
|
-
if (this.#closed) throw new ClientError('RDB_CLOSED', 'Database client is closed', 'not-sent');
|
|
225
|
-
}
|
|
226
|
-
#queryVersion() { return this.#cloud ? CLOUD_QUERY_VERSION : 2; }
|
|
227
|
-
async #request(path, body, options, writing, prepared, queryVersion = 2) {
|
|
228
|
-
this.#assertOpen();
|
|
229
|
-
const duration = timeout(options.timeoutMs ?? this.#timeout);
|
|
230
|
-
if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) throw new TypeError('signal must be an AbortSignal');
|
|
231
|
-
if (options.signal?.aborted) throw new ClientError('RDB_CANCELLED', 'Request cancelled before dispatch', 'not-sent', prepared ? { request: prepared } : {});
|
|
232
|
-
const bodyBytes = body === undefined ? 0 : byteLength(body);
|
|
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 } : {});
|
|
234
|
-
const controller = new AbortController();
|
|
235
|
-
const abort = () => controller.abort();
|
|
236
|
-
options.signal?.addEventListener('abort', abort, { once: true });
|
|
237
|
-
const timer = setTimeout(abort, duration);
|
|
238
|
-
this.#active.add(controller);
|
|
239
|
-
this.#inflightBytes += bodyBytes;
|
|
240
|
-
const credential = this.#token;
|
|
241
|
-
const fetch = this.#fetch;
|
|
242
|
-
let status;
|
|
243
|
-
try {
|
|
244
|
-
if (this.#transport) return await this.#transport(path, body, controller.signal, duration);
|
|
245
|
-
const send = () => fetch(this.#url + path, {
|
|
246
|
-
method: body === undefined ? 'GET' : 'POST',
|
|
247
|
-
headers: { authorization: `Bearer ${credential}`, accept: 'application/json', ...(body === undefined ? {} : { 'content-type': 'application/json' }), ...(this.#id === undefined ? {} : { 'x-romedb-database-id': prepared?.databaseId ?? this.#id }),
|
|
248
|
-
...(queryVersion === CLOUD_QUERY_VERSION && (path === '/query' || path === '/read') ? { 'x-romedb-query-version': String(queryVersion) } : {}) },
|
|
249
|
-
...(body === undefined ? {} : { body }),
|
|
250
|
-
// Workers have no redirect: 'error'; a redirect is refused below, never followed with the credential.
|
|
251
|
-
redirect: 'manual', signal: controller.signal,
|
|
252
|
-
});
|
|
253
|
-
let response = await send();
|
|
254
|
-
if (await queueRefused(response)) {
|
|
255
|
-
await pause(queueRetryDelay(response), controller.signal);
|
|
256
|
-
response = await send();
|
|
257
|
-
}
|
|
258
|
-
status = response.status;
|
|
259
|
-
if (response.type === 'opaqueredirect' || (status >= 300 && status < 400)) {
|
|
260
|
-
await response.body?.cancel().catch(() => {});
|
|
261
|
-
throw new ClientError('RDB_PROTOCOL', 'Database URL redirected; redirects are never followed with a credential', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
|
|
262
|
-
}
|
|
263
|
-
const retryAfterMs = retryAfter(response.headers.get('retry-after'));
|
|
264
|
-
const replyVersion = response.headers.get('x-romedb-query-version');
|
|
265
|
-
if ((path === '/query' || path === '/read') && replyVersion !== null && replyVersion !== '2' && replyVersion !== String(queryVersion)) {
|
|
266
|
-
await response.body?.cancel();
|
|
267
|
-
throw new ClientError('RDB_PROTOCOL', 'Unsupported database query response version', writing ? 'unknown' : 'read-failed', { status, ...(prepared ? { request: prepared } : {}) });
|
|
268
|
-
}
|
|
269
|
-
const reply = await boundedJson(response, MAX_RESPONSE);
|
|
270
|
-
if (plainObject(reply?.error) && typeof reply.error.message === 'string') reply.error.message = redactMessage(reply.error.message, credential);
|
|
271
|
-
if (!response.ok && !(writing && reply?.requestId && reply?.sequence !== undefined)) {
|
|
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);
|
|
273
|
-
throw new ClientError(code, safeMessage(reply, credential), writing ? 'unknown' : 'read-failed', { status, ...(retryAfterMs === undefined ? {} : { retryAfterMs }), ...(prepared ? { request: prepared } : {}) });
|
|
274
|
-
}
|
|
275
|
-
return reply;
|
|
276
|
-
} catch (error) {
|
|
277
|
-
if (error instanceof ClientError) {
|
|
278
|
-
const outcome = writing
|
|
279
|
-
? this.#transport && error.outcome === 'not-sent' ? 'not-sent' : 'unknown'
|
|
280
|
-
: error.outcome === 'not-sent' ? 'not-sent' : 'read-failed';
|
|
281
|
-
const message = this.#transport ? error.message.slice(0, 2048) : redactMessage(error.message, credential);
|
|
282
|
-
throw new ClientError(error.code, message, outcome, {
|
|
283
|
-
...(error.status === undefined ? {} : { status: error.status }),
|
|
284
|
-
...(error.retryAfterMs === undefined ? {} : { retryAfterMs: error.retryAfterMs }),
|
|
285
|
-
...(prepared ? { request: prepared } : {}),
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
const code = controller.signal.aborted ? 'RDB_CANCELLED' : status === undefined ? 'RDB_TRANSPORT' : 'RDB_PROTOCOL';
|
|
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 });
|
|
290
|
-
} finally {
|
|
291
|
-
clearTimeout(timer);
|
|
292
|
-
options.signal?.removeEventListener('abort', abort);
|
|
293
|
-
this.#active.delete(controller);
|
|
294
|
-
this.#inflightBytes -= bodyBytes;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
require('./client-internal.cjs').createEmbeddedClient = async (options, transport) => {
|
|
300
|
-
const database = new RemoteDatabase({ ...options, token: 'embedded' }, construction, transport);
|
|
301
|
-
try { await database.info(); return database; }
|
|
302
|
-
catch (error) { await database.close(); throw error; }
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
function normalizeFileUrl(input) {
|
|
306
|
-
if (typeof input !== 'string' || input.length > 4096) throw new TypeError('file URL requires a bounded local path');
|
|
307
|
-
const url = new URL(input);
|
|
308
|
-
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');
|
|
309
|
-
return url.href;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function normalizeUrl(input) {
|
|
313
|
-
if (typeof input !== 'string' || input.length > 4096) throw new TypeError('url requires a bounded database URL');
|
|
314
|
-
// romedb:// is an alias for https://; the same checks apply after the rewrite.
|
|
315
|
-
const url = new URL(input.startsWith('romedb://') ? 'https://' + input.slice('romedb://'.length) : input);
|
|
316
|
-
const loopback = loopbackHost(url.hostname);
|
|
317
|
-
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');
|
|
318
|
-
let path = url.pathname.replace(/\/$/, '').replace(/\/(?:query|read)$/, '');
|
|
319
|
-
if (loopback && !path) path = '/v1/database';
|
|
320
|
-
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
|
|
321
|
-
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');
|
|
322
|
-
return url.origin + path;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function encodeStatements(statements, queryVersion = 2) {
|
|
326
|
-
if (!Array.isArray(statements) || statements.length < 1 || statements.length > 32) throw new RangeError('request requires 1..32 statements');
|
|
327
|
-
return statements.map(statement => {
|
|
328
|
-
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');
|
|
329
|
-
if (statement.params === undefined) return { sql: text(statement.sql) };
|
|
330
|
-
if (!plainObject(statement.params)) throw new TypeError('params require an object');
|
|
331
|
-
return { sql: text(statement.sql), params: Object.fromEntries(Object.entries(statement.params).map(([key, value]) => [text(key), encodeValue(value, 0, queryVersion === CLOUD_QUERY_VERSION)])) };
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function validatePrepared(prepared) {
|
|
336
|
-
const fields = prepared?.version === 1 ? 'body,databaseId,url,version' : 'body,databaseId,queryVersion,url,version';
|
|
337
|
-
if (!plainObject(prepared) || Object.keys(prepared).sort().join(',') !== fields || ![1, 2].includes(prepared.version) ||
|
|
338
|
-
(prepared.version === 2 && ![2, CLOUD_QUERY_VERSION].includes(prepared.queryVersion)) || typeof prepared.databaseId !== 'string' || !prepared.databaseId ||
|
|
339
|
-
(prepared.url?.startsWith('file:') ? normalizeFileUrl(prepared.url) : normalizeUrl(prepared.url)) !== prepared.url || typeof prepared.body !== 'string') throw new TypeError('invalid saved request');
|
|
340
|
-
checkBodySize(prepared.body, preparedQueryVersion(prepared));
|
|
341
|
-
const body = JSON.parse(prepared.body);
|
|
342
|
-
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');
|
|
343
|
-
encodeWorkLimits(body.workLimits);
|
|
344
|
-
checkSequence(body.afterSequence);
|
|
345
|
-
if (body.ifSequence !== undefined) {
|
|
346
|
-
checkSequence(body.ifSequence);
|
|
347
|
-
if (body.ifSequence < body.afterSequence) throw new RangeError('invalid saved sequence condition');
|
|
348
|
-
}
|
|
349
|
-
if (!Array.isArray(body.statements) || body.statements.length < 1 || body.statements.length > 32) throw new TypeError('invalid saved statements');
|
|
350
|
-
for (const statement of body.statements) {
|
|
351
|
-
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');
|
|
352
|
-
for (const value of Object.values(statement.params ?? {})) decodeValue(value);
|
|
353
|
-
}
|
|
354
|
-
return body;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function confirmReply(reply, request, reading, 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 } : {});
|
|
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();
|
|
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 });
|
|
361
|
-
if (!Array.isArray(reply.results) || reply.results.length !== request.statements.length || reply.error !== undefined || byteLength(JSON.stringify(reply.results)) > 262_144) throw invalid();
|
|
362
|
-
try {
|
|
363
|
-
const results = reply.results.map(result => {
|
|
364
|
-
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();
|
|
365
|
-
return { columns: result.columns, rows: result.rows.map(row => {
|
|
366
|
-
if (!Array.isArray(row) || row.length !== result.columns.length) throw invalid();
|
|
367
|
-
return row.map(value => decodeValue(value));
|
|
368
|
-
}), affected: BigInt(result.affected) };
|
|
369
|
-
});
|
|
370
|
-
return { requestId: reply.requestId, sequence: reply.sequence, results, ...(reading ? { readVersion: 2 } : {}), ...(reply.executionWork === undefined ? {} : { executionWork: reply.executionWork }) };
|
|
371
|
-
} catch { throw invalid(); }
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async function boundedJson(response, limit) {
|
|
375
|
-
if (!/^application\/json(?:\s*;|$)/i.test(response.headers.get('content-type') ?? '')) throw new TypeError('response requires JSON');
|
|
376
|
-
if (!response.body) throw new TypeError('response body is missing');
|
|
377
|
-
const reader = response.body.getReader();
|
|
378
|
-
const chunks = [];
|
|
379
|
-
let length = 0;
|
|
380
|
-
try {
|
|
381
|
-
for (;;) {
|
|
382
|
-
const { done, value } = await reader.read();
|
|
383
|
-
if (done) break;
|
|
384
|
-
length += value.byteLength;
|
|
385
|
-
if (length > limit) throw new RangeError('response exceeds client byte limit');
|
|
386
|
-
chunks.push(value);
|
|
387
|
-
}
|
|
388
|
-
const bytes = new Uint8Array(length);
|
|
389
|
-
let offset = 0;
|
|
390
|
-
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
391
|
-
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
392
|
-
} finally { await reader.cancel().catch(() => {}); }
|
|
393
|
-
}
|
|
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
|
-
|
|
423
|
-
function loopbackHost(hostname) { return ['127.0.0.1', 'localhost', '[::1]'].includes(hostname); }
|
|
424
|
-
|
|
425
|
-
function capabilities(url, info, local) {
|
|
426
|
-
if (local) {
|
|
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');
|
|
428
|
-
return { ...info.capabilities, provenance: 'negotiated' };
|
|
429
|
-
}
|
|
430
|
-
return { provenance: 'documented', readVersion: 2, atomicWrites: true, replayWindow: 64, exactSequenceCondition: true, negotiatedLimits: false };
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function sequence(value, reply = false) { return Number.isSafeInteger(value) && value >= 0 && value <= (reply ? MAX_SEQUENCE + 1 : MAX_SEQUENCE); }
|
|
434
|
-
function encodeWorkLimits(value) {
|
|
435
|
-
if (value === undefined) return undefined;
|
|
436
|
-
const ceilings = { rowsRead: 1_000_000, rowMutations: 100_000, vmSteps: 10_000_000 };
|
|
437
|
-
if (!plainObject(value)) throw new TypeError('workLimits require an object');
|
|
438
|
-
for (const [name, limit] of Object.entries(value)) {
|
|
439
|
-
if (!Object.hasOwn(ceilings, name) || !Number.isSafeInteger(limit) || limit < 0 || limit > ceilings[name]) throw new RangeError('workLimits require nonnegative integers within the service ceilings');
|
|
440
|
-
}
|
|
441
|
-
return Object.fromEntries(Object.keys(ceilings).filter(key => Object.hasOwn(value, key)).map(key => [key, value[key]]));
|
|
442
|
-
}
|
|
443
|
-
function checkSequence(value) { if (!sequence(value)) throw new RangeError('sequence requires a nonnegative safe integer below Number.MAX_SAFE_INTEGER'); }
|
|
444
|
-
function timeout(value) { if (!Number.isInteger(value) || value < 1 || value > 120_000) throw new RangeError('timeoutMs must be 1..120000'); return value; }
|
|
445
|
-
function byteLength(text) { return encoder.encode(text).byteLength; }
|
|
446
|
-
function checkBodySize(body, queryVersion = 2) {
|
|
447
|
-
if (byteLength(body) > (queryVersion === CLOUD_QUERY_VERSION ? MAX_CLOUD_REQUEST : MAX_REQUEST))
|
|
448
|
-
throw new RangeError(queryVersion === CLOUD_QUERY_VERSION ? 'request exceeds 1 MiB' : 'request exceeds 64 KiB');
|
|
449
|
-
}
|
|
450
|
-
function preparedQueryVersion(prepared) { return prepared.version === 1 ? 2 : prepared.queryVersion; }
|
|
451
|
-
// Cloud refuses a queued request with 503 query_queue_* before dispatching it,
|
|
452
|
-
// so nothing ran and one retry is safe for reads and writes alike.
|
|
453
|
-
async function queueRefused(response) {
|
|
454
|
-
if (response.status !== 503) return false;
|
|
455
|
-
try { return /^query_queue_[a-z_]{1,40}$/.test((await response.clone().json())?.code ?? ''); } catch { return false; }
|
|
456
|
-
}
|
|
457
|
-
function queueRetryDelay(response) {
|
|
458
|
-
const header = response.headers.get('retry-after');
|
|
459
|
-
return header === null ? 0 : Math.min(retryAfter(header) ?? 1000, 2000);
|
|
460
|
-
}
|
|
461
|
-
// Retry-After holds delay seconds or an HTTP date.
|
|
462
|
-
function retryAfter(value) {
|
|
463
|
-
if (value === null) return undefined;
|
|
464
|
-
if (/^\s*\d{1,9}(?:\.\d+)?\s*$/.test(value)) return Math.round(Number(value) * 1000);
|
|
465
|
-
const date = /GMT\s*$/.test(value) ? Date.parse(value) : NaN;
|
|
466
|
-
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
467
|
-
}
|
|
468
|
-
function pause(ms, signal) {
|
|
469
|
-
return new Promise((resolve, reject) => {
|
|
470
|
-
if (signal.aborted) { reject(signal.reason); return; }
|
|
471
|
-
const timer = setTimeout(() => { signal.removeEventListener('abort', stop); resolve(); }, ms);
|
|
472
|
-
const stop = () => { clearTimeout(timer); reject(signal.reason); };
|
|
473
|
-
signal.addEventListener('abort', stop, { once: true });
|
|
474
|
-
});
|
|
475
|
-
}
|
|
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'; }
|
|
477
|
-
function redactMessage(message, token) { return (token ? message.replaceAll(token, '[redacted]') : message).slice(0, 2048); }
|
|
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); }
|
|
479
|
-
|
|
480
|
-
async function saveRequest(path, request) { return require('./request-journal.cjs').saveRequest(path, request, validatePrepared); }
|
|
481
|
-
async function loadRequest(path) { return require('./request-journal.cjs').loadRequest(path, validatePrepared); }
|
|
482
|
-
|
|
483
|
-
module.exports = { connect, RemoteDatabase, ClientError, Float64, Record, Vector, encodeValue, decodeValue, normalizeUrl, validatePrepared, saveRequest, loadRequest };
|
|
2
|
+
module.exports = require('@romedb/client');
|