@riffdb/client 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +27 -0
- package/README.md +44 -2
- package/conformance-manifest-v1.json +15 -0
- package/dist/driver.d.ts +171 -0
- package/dist/driver.js +932 -0
- package/dist/index.d.ts +332 -0
- package/dist/index.js +2054 -0
- package/dist/operator.d.ts +65 -0
- package/dist/operator.js +313 -0
- package/package.json +1 -6
package/dist/driver.js
ADDED
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
import { createConnection } from "node:net";
|
|
2
|
+
const MAX_FRAME_BYTES = 8 * 1_024 * 1_024;
|
|
3
|
+
const MAX_ENCODED_BYTES_VALUE = 1_398_104;
|
|
4
|
+
const MAX_PENDING_REQUESTS = 256;
|
|
5
|
+
/** Decimal digits always inside `Number.MAX_SAFE_INTEGER` (9007199254740991). */
|
|
6
|
+
const SAFE_INTEGER_DIGITS = 15;
|
|
7
|
+
const MIN_SAFE_INTEGER_EXACT = BigInt(Number.MIN_SAFE_INTEGER);
|
|
8
|
+
const MAX_SAFE_INTEGER_EXACT = BigInt(Number.MAX_SAFE_INTEGER);
|
|
9
|
+
const MAX_COLLECTION_ITEMS = 4_096;
|
|
10
|
+
const MAX_VALUE_DEPTH = 32;
|
|
11
|
+
const HASH = /^[0-9a-f]{64}$/;
|
|
12
|
+
const SYMBOL = /^[A-Za-z0-9_.-]{1,256}$/;
|
|
13
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
14
|
+
const REQUEST_ID = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
15
|
+
const MAX_U64 = 18446744073709551615n;
|
|
16
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
17
|
+
let nextSession = 0;
|
|
18
|
+
/** Exact alpha driver protocol generation. */
|
|
19
|
+
export const DRIVER_PROTOCOL_VERSION = 4;
|
|
20
|
+
/** Exact tagged value registry compiled into `riffdb-driverd`. */
|
|
21
|
+
export const DRIVER_VALUE_REGISTRY_HASH = "8e1681ddf5e6a82e7fa646f9737128ad7e36f54f8b5846ac6e33e732125407e5";
|
|
22
|
+
/** Exact structured-error registry compiled into `riffdb-driverd`. */
|
|
23
|
+
export const DRIVER_ERROR_REGISTRY_HASH = "b94d685ecbc18f2369a2bfa1a53139d06100699c4ee41b31c86d6a7e17039850";
|
|
24
|
+
export function exactDecimal(value, precision, scale) {
|
|
25
|
+
if (!Number.isInteger(precision) || precision < 1 || precision > 38
|
|
26
|
+
|| !Number.isInteger(scale) || scale < 0 || scale > precision) {
|
|
27
|
+
throw new Error("invalid exact decimal type");
|
|
28
|
+
}
|
|
29
|
+
const match = /^(-?)([0-9]+)(?:\.([0-9]+))?$/.exec(value);
|
|
30
|
+
if (match === null || value.length > 128)
|
|
31
|
+
throw new Error("invalid exact decimal text");
|
|
32
|
+
const fractional = match[3] ?? "";
|
|
33
|
+
if (fractional.length > scale)
|
|
34
|
+
throw new Error("exact decimal exceeds scale");
|
|
35
|
+
let coefficient = BigInt(`${match[2]}${fractional.padEnd(scale, "0")}`);
|
|
36
|
+
if (match[1] === "-" && coefficient !== 0n)
|
|
37
|
+
coefficient = -coefficient;
|
|
38
|
+
const limit = 10n ** BigInt(precision);
|
|
39
|
+
if (coefficient <= -limit || coefficient >= limit)
|
|
40
|
+
throw new Error("exact decimal exceeds precision");
|
|
41
|
+
return { coefficientTwosComplement: signedTwosComplement(coefficient), scale, precision };
|
|
42
|
+
}
|
|
43
|
+
export function exactMoney(currency, value) {
|
|
44
|
+
if (!/^[A-Z]{3}$/.test(currency))
|
|
45
|
+
throw new Error("invalid exact money currency");
|
|
46
|
+
return { currency, amount: exactDecimal(value, 38, 2) };
|
|
47
|
+
}
|
|
48
|
+
/** Checked public error returned by the Rust driver host. */
|
|
49
|
+
export class DriverApplicationError extends Error {
|
|
50
|
+
details;
|
|
51
|
+
name = "DriverApplicationError";
|
|
52
|
+
constructor(details) {
|
|
53
|
+
super(`${details.code}: ${details.message}`);
|
|
54
|
+
this.details = details;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* One retained, bounded local session to `riffdb-driverd`.
|
|
59
|
+
*
|
|
60
|
+
* This class has no remote endpoint, credential, TLS, gRPC, or retry-status
|
|
61
|
+
* surface. Those semantics remain owned by the Rust host.
|
|
62
|
+
*/
|
|
63
|
+
export class DriverApplicationTransport {
|
|
64
|
+
#socket;
|
|
65
|
+
#identity;
|
|
66
|
+
#tenant;
|
|
67
|
+
#requestPrefix;
|
|
68
|
+
#pending = new Map();
|
|
69
|
+
#input = Buffer.alloc(0);
|
|
70
|
+
#nextRequest = 1;
|
|
71
|
+
#closed = false;
|
|
72
|
+
constructor(socket, identity, tenant) {
|
|
73
|
+
this.#socket = socket;
|
|
74
|
+
this.#identity = identity;
|
|
75
|
+
this.#tenant = tenant;
|
|
76
|
+
nextSession = nextSession === Number.MAX_SAFE_INTEGER ? 1 : nextSession + 1;
|
|
77
|
+
this.#requestPrefix = `ts.${process.pid}.${nextSession}`;
|
|
78
|
+
socket.on("data", (chunk) => this.#receive(chunk));
|
|
79
|
+
socket.on("error", () => this.#fail(new Error("RiffDB driver session failed")));
|
|
80
|
+
socket.on("close", () => this.#fail(new Error("RiffDB driver session closed")));
|
|
81
|
+
}
|
|
82
|
+
/** Returns the bound tenant identifier when this session is tenant-scoped. */
|
|
83
|
+
get tenant() {
|
|
84
|
+
return this.#tenant;
|
|
85
|
+
}
|
|
86
|
+
static async connect(options) {
|
|
87
|
+
const tenant = resolveTransportTenant(options);
|
|
88
|
+
validateTransportOptions(options);
|
|
89
|
+
const socket = createConnection({ path: options.socketPath });
|
|
90
|
+
await new Promise((resolve, reject) => {
|
|
91
|
+
const onError = () => reject(new Error("RiffDB driver session failed"));
|
|
92
|
+
socket.once("connect", () => { socket.off("error", onError); resolve(); });
|
|
93
|
+
socket.once("error", onError);
|
|
94
|
+
});
|
|
95
|
+
const transport = new DriverApplicationTransport(socket, options.identity, tenant);
|
|
96
|
+
try {
|
|
97
|
+
await transport.#handshake();
|
|
98
|
+
return transport;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
socket.destroy();
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async invoke(operation, input, options = {}) {
|
|
106
|
+
validateOperation(operation);
|
|
107
|
+
validateDriverRecord(input);
|
|
108
|
+
const requestId = this.#requestId("invoke");
|
|
109
|
+
const response = await this.#request({
|
|
110
|
+
type: "invoke",
|
|
111
|
+
request_id: requestId,
|
|
112
|
+
operation: operation.name,
|
|
113
|
+
input_schema_hash: operation.inputSchemaHash,
|
|
114
|
+
input,
|
|
115
|
+
options: lowerOptions(options),
|
|
116
|
+
}, requestId, options.signal);
|
|
117
|
+
return decodeResult(response);
|
|
118
|
+
}
|
|
119
|
+
async batch(operation, items, concurrency, checkpoint, options = {}) {
|
|
120
|
+
validateOperation(operation);
|
|
121
|
+
if (items.length < 1 || items.length > MAX_COLLECTION_ITEMS
|
|
122
|
+
|| !Number.isInteger(concurrency) || concurrency < 1 || concurrency > 384
|
|
123
|
+
|| !Number.isInteger(checkpoint) || checkpoint < 0 || checkpoint > items.length
|
|
124
|
+
|| options.readAfterCommit !== undefined || options.cursor !== undefined
|
|
125
|
+
|| options.queryConsistency !== undefined) {
|
|
126
|
+
throw new Error("invalid RiffDB driver batch bounds");
|
|
127
|
+
}
|
|
128
|
+
for (const input of items)
|
|
129
|
+
validateDriverRecord(input);
|
|
130
|
+
const requestId = this.#requestId("batch");
|
|
131
|
+
const response = await this.#request({
|
|
132
|
+
type: "batch",
|
|
133
|
+
request_id: requestId,
|
|
134
|
+
operation: operation.name,
|
|
135
|
+
input_schema_hash: operation.inputSchemaHash,
|
|
136
|
+
items,
|
|
137
|
+
concurrency,
|
|
138
|
+
checkpoint,
|
|
139
|
+
options: lowerOptions(options),
|
|
140
|
+
}, requestId, options.signal);
|
|
141
|
+
return decodeBatchResult(response);
|
|
142
|
+
}
|
|
143
|
+
/** Stops new work, sends a clean local EOF, and rejects unfinished waits. */
|
|
144
|
+
async shutdown() {
|
|
145
|
+
if (this.#closed)
|
|
146
|
+
return;
|
|
147
|
+
this.#closed = true;
|
|
148
|
+
this.#fail(new Error("RiffDB driver session closed"));
|
|
149
|
+
await new Promise((resolve) => {
|
|
150
|
+
if (this.#socket.destroyed) {
|
|
151
|
+
resolve();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.#socket.end(resolve);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async #handshake() {
|
|
158
|
+
const requestId = this.#requestId("handshake");
|
|
159
|
+
const identity = this.#identity;
|
|
160
|
+
const response = await this.#request({
|
|
161
|
+
type: "handshake",
|
|
162
|
+
request_id: requestId,
|
|
163
|
+
protocol_version: DRIVER_PROTOCOL_VERSION,
|
|
164
|
+
application_manifest_hash: identity.applicationManifestHash,
|
|
165
|
+
operation_catalog_hash: identity.operationCatalogHash,
|
|
166
|
+
contract_lineage: identity.contractLineage,
|
|
167
|
+
contract_version: identity.contractVersion,
|
|
168
|
+
contract_bundle_hash: identity.contractBundleHash,
|
|
169
|
+
value_registry_hash: DRIVER_VALUE_REGISTRY_HASH,
|
|
170
|
+
error_registry_hash: DRIVER_ERROR_REGISTRY_HASH,
|
|
171
|
+
database: identity.database,
|
|
172
|
+
role: identity.role,
|
|
173
|
+
role_definition_hash: identity.roleDefinitionHash,
|
|
174
|
+
remote_identity_hash: identity.remoteIdentityHash,
|
|
175
|
+
}, requestId);
|
|
176
|
+
if (response.type !== "handshake"
|
|
177
|
+
|| response.protocol_version !== DRIVER_PROTOCOL_VERSION
|
|
178
|
+
|| response.application_manifest_hash !== identity.applicationManifestHash
|
|
179
|
+
|| response.operation_catalog_hash !== identity.operationCatalogHash
|
|
180
|
+
|| response.contract_lineage !== identity.contractLineage
|
|
181
|
+
|| requiredPositiveBigInt(response.contract_version) !== identity.contractVersion
|
|
182
|
+
|| response.contract_bundle_hash !== identity.contractBundleHash
|
|
183
|
+
|| response.database !== identity.database
|
|
184
|
+
|| response.role !== identity.role
|
|
185
|
+
|| response.role_definition_hash !== identity.roleDefinitionHash
|
|
186
|
+
|| response.remote_identity_hash !== identity.remoteIdentityHash) {
|
|
187
|
+
throw new Error("RiffDB driver identity mismatch");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async #request(request, requestId, signal) {
|
|
191
|
+
if (this.#closed || this.#socket.destroyed)
|
|
192
|
+
throw new Error("RiffDB driver session closed");
|
|
193
|
+
if (this.#pending.size >= MAX_PENDING_REQUESTS)
|
|
194
|
+
throw new Error("RiffDB driver session is over capacity");
|
|
195
|
+
if (signal?.aborted === true)
|
|
196
|
+
throw new Error("RiffDB driver request was cancelled before submission");
|
|
197
|
+
const frame = encodeFrame(request);
|
|
198
|
+
let abort;
|
|
199
|
+
const response = new Promise((resolve, reject) => {
|
|
200
|
+
this.#pending.set(requestId, { resolve, reject });
|
|
201
|
+
if (signal !== undefined) {
|
|
202
|
+
abort = () => {
|
|
203
|
+
if (!this.#pending.has(requestId) || this.#closed)
|
|
204
|
+
return;
|
|
205
|
+
const cancelId = this.#requestId("cancel");
|
|
206
|
+
this.#socket.write(encodeFrame({
|
|
207
|
+
type: "cancel",
|
|
208
|
+
request_id: cancelId,
|
|
209
|
+
target_request_id: requestId,
|
|
210
|
+
}));
|
|
211
|
+
};
|
|
212
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
213
|
+
}
|
|
214
|
+
this.#socket.write(frame, (error) => {
|
|
215
|
+
if (error === null || error === undefined)
|
|
216
|
+
return;
|
|
217
|
+
this.#pending.delete(requestId);
|
|
218
|
+
reject(new Error("RiffDB driver session failed"));
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
try {
|
|
222
|
+
const value = await response;
|
|
223
|
+
if (value.type === "error")
|
|
224
|
+
throw decodeError(value);
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
if (signal !== undefined && abort !== undefined)
|
|
229
|
+
signal.removeEventListener("abort", abort);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
#receive(chunk) {
|
|
233
|
+
if (this.#closed)
|
|
234
|
+
return;
|
|
235
|
+
this.#input = Buffer.concat([this.#input, chunk]);
|
|
236
|
+
while (this.#input.length >= 4) {
|
|
237
|
+
const length = this.#input.readUInt32BE(0);
|
|
238
|
+
if (length < 2 || length > MAX_FRAME_BYTES) {
|
|
239
|
+
this.#fail(new Error("RiffDB driver returned an invalid frame"));
|
|
240
|
+
this.#socket.destroy();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (this.#input.length < length + 4)
|
|
244
|
+
return;
|
|
245
|
+
const body = this.#input.subarray(4, length + 4);
|
|
246
|
+
this.#input = this.#input.subarray(length + 4);
|
|
247
|
+
let response;
|
|
248
|
+
try {
|
|
249
|
+
response = exactObject(decodeJson(UTF8_DECODER.decode(body)));
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
this.#fail(new Error("RiffDB driver returned an invalid message"));
|
|
253
|
+
this.#socket.destroy();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const requestId = response.request_id;
|
|
257
|
+
if (typeof requestId !== "string" || !REQUEST_ID.test(requestId)) {
|
|
258
|
+
this.#fail(new Error("RiffDB driver returned an invalid request identity"));
|
|
259
|
+
this.#socket.destroy();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const pending = this.#pending.get(requestId);
|
|
263
|
+
if (pending === undefined)
|
|
264
|
+
continue;
|
|
265
|
+
this.#pending.delete(requestId);
|
|
266
|
+
pending.resolve(response);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
#fail(error) {
|
|
270
|
+
for (const pending of this.#pending.values())
|
|
271
|
+
pending.reject(error);
|
|
272
|
+
this.#pending.clear();
|
|
273
|
+
}
|
|
274
|
+
#requestId(kind) {
|
|
275
|
+
const value = `${this.#requestPrefix}.${kind}.${this.#nextRequest}`;
|
|
276
|
+
this.#nextRequest = this.#nextRequest === Number.MAX_SAFE_INTEGER ? 1 : this.#nextRequest + 1;
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function lowerOptions(options) {
|
|
281
|
+
const deadline = options.deadlineMillis ?? 30_000;
|
|
282
|
+
const attempts = options.maximumAttempts ?? 3;
|
|
283
|
+
if (!Number.isInteger(deadline) || deadline < 1 || deadline > 300_000
|
|
284
|
+
|| !Number.isInteger(attempts) || attempts < 1 || attempts > 10
|
|
285
|
+
|| (options.readAfterCommit !== undefined && options.readAfterCommit < 1n)
|
|
286
|
+
|| (options.cursor !== undefined && options.cursor.length > 16_384)
|
|
287
|
+
|| (options.queryConsistency !== undefined && options.queryConsistency !== "admissionHead")
|
|
288
|
+
|| (options.acceptPackedResult === true && options.acceptCompactResult !== true)) {
|
|
289
|
+
throw new Error("invalid RiffDB driver invocation options");
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
deadline_millis: deadline,
|
|
293
|
+
maximum_attempts: attempts,
|
|
294
|
+
read_after_commit: options.readAfterCommit ?? null,
|
|
295
|
+
cursor: options.cursor ?? null,
|
|
296
|
+
query_consistency: options.queryConsistency === "admissionHead" ? "admission_head" : null,
|
|
297
|
+
accept_compact_result: options.acceptCompactResult ?? false,
|
|
298
|
+
accept_packed_result: options.acceptPackedResult ?? false,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function encodeFrame(value) {
|
|
302
|
+
const body = Buffer.from(encodeFrameBody(value), "utf8");
|
|
303
|
+
if (body.length < 2 || body.length > MAX_FRAME_BYTES)
|
|
304
|
+
throw new Error("RiffDB driver request exceeds the frame bound");
|
|
305
|
+
const frame = Buffer.allocUnsafe(body.length + 4);
|
|
306
|
+
frame.writeUInt32BE(body.length, 0);
|
|
307
|
+
body.copy(frame, 4);
|
|
308
|
+
return frame;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Encodes the closed protocol without coercing u64 frontiers through the
|
|
312
|
+
* JavaScript `number` domain. Only integral JSON numbers are admitted.
|
|
313
|
+
*/
|
|
314
|
+
/** Deterministic UTF-16 code-unit ordering for protocol field names. */
|
|
315
|
+
function compareFieldNames(left, right) {
|
|
316
|
+
if (left < right)
|
|
317
|
+
return -1;
|
|
318
|
+
return left > right ? 1 : 0;
|
|
319
|
+
}
|
|
320
|
+
function encodeJson(value, depth = 0) {
|
|
321
|
+
if (depth > MAX_VALUE_DEPTH + 4)
|
|
322
|
+
throw new Error("RiffDB driver message exceeds the depth bound");
|
|
323
|
+
if (value === null)
|
|
324
|
+
return "null";
|
|
325
|
+
if (typeof value === "boolean")
|
|
326
|
+
return value ? "true" : "false";
|
|
327
|
+
if (typeof value === "string")
|
|
328
|
+
return JSON.stringify(value);
|
|
329
|
+
if (typeof value === "bigint") {
|
|
330
|
+
if (value < 0n || value > MAX_U64)
|
|
331
|
+
throw new Error("invalid RiffDB driver integer");
|
|
332
|
+
return value.toString();
|
|
333
|
+
}
|
|
334
|
+
if (typeof value === "number") {
|
|
335
|
+
if (!Number.isSafeInteger(value))
|
|
336
|
+
throw new Error("invalid RiffDB driver integer");
|
|
337
|
+
return value.toString();
|
|
338
|
+
}
|
|
339
|
+
if (Array.isArray(value)) {
|
|
340
|
+
if (value.length > MAX_COLLECTION_ITEMS)
|
|
341
|
+
throw new Error("RiffDB driver collection exceeds its bound");
|
|
342
|
+
const items = [];
|
|
343
|
+
for (const item of value)
|
|
344
|
+
items.push(encodeJson(item, depth + 1));
|
|
345
|
+
return `[${items.join(",")}]`;
|
|
346
|
+
}
|
|
347
|
+
if (typeof value === "object" && value !== null) {
|
|
348
|
+
// One pass instead of entries/filter/sort/map, and code-unit ordering
|
|
349
|
+
// instead of `localeCompare`: collation is locale-dependent, so the
|
|
350
|
+
// "closed protocol" encoding was not actually deterministic across hosts,
|
|
351
|
+
// and full Unicode collation is far more work than field names need.
|
|
352
|
+
const names = [];
|
|
353
|
+
const record = value;
|
|
354
|
+
for (const name of Object.keys(record)) {
|
|
355
|
+
if (record[name] !== undefined)
|
|
356
|
+
names.push(name);
|
|
357
|
+
}
|
|
358
|
+
if (names.length > MAX_COLLECTION_ITEMS)
|
|
359
|
+
throw new Error("RiffDB driver record exceeds its bound");
|
|
360
|
+
names.sort(compareFieldNames);
|
|
361
|
+
const encoded = [];
|
|
362
|
+
for (const name of names) {
|
|
363
|
+
encoded.push(`${JSON.stringify(name)}:${encodeJson(record[name], depth + 1)}`);
|
|
364
|
+
}
|
|
365
|
+
return `{${encoded.join(",")}}`;
|
|
366
|
+
}
|
|
367
|
+
throw new Error("invalid RiffDB driver message");
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Serializes one frame, preferring V8's C++ serializer.
|
|
371
|
+
*
|
|
372
|
+
* `JSON.stringify` throws on a bigint, and this protocol carries bigint in only
|
|
373
|
+
* two places: the handshake's `contract_version`, sent once per session, and an
|
|
374
|
+
* explicit read-after-commit fence, which is absent unless the caller asks for
|
|
375
|
+
* one. The ordinary per-operation frame therefore takes the fast path, and
|
|
376
|
+
* anything holding a bigint falls back to the exact encoder, which keeps the
|
|
377
|
+
* full u64 range.
|
|
378
|
+
*
|
|
379
|
+
* The fast path emits fields in insertion order rather than sorted order, and
|
|
380
|
+
* leaves the collection and depth bounds to `validate_request` on the host.
|
|
381
|
+
* Both are outbound frames this client constructs itself.
|
|
382
|
+
*/
|
|
383
|
+
function encodeFrameBody(value) {
|
|
384
|
+
try {
|
|
385
|
+
return JSON.stringify(value);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return encodeJson(value);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Parses integral JSON while preserving integers outside Number's exact range.
|
|
393
|
+
*
|
|
394
|
+
* This stays on the exact parser deliberately. A native `JSON.parse` fast path
|
|
395
|
+
* measures faster, but it turns a frontier above `Number.MAX_SAFE_INTEGER` into
|
|
396
|
+
* an imprecise double, which the envelope validators then reject; the exact
|
|
397
|
+
* parser instead returns those digits as a string that converts to an exact
|
|
398
|
+
* `BigInt`. Rejecting a legitimate large frontier is a functional regression,
|
|
399
|
+
* and `u64 frontiers remain exact across the JSON number protocol` covers it.
|
|
400
|
+
* Guarding the fast path needs a scan for bare integers of sixteen or more
|
|
401
|
+
* digits, and because application u64 values travel as twenty-digit strings the
|
|
402
|
+
* scan cannot be anchored on a literal, so it costs about 1.4us on a 4 KiB
|
|
403
|
+
* frame and gives most of the win back.
|
|
404
|
+
*/
|
|
405
|
+
function decodeJson(value) {
|
|
406
|
+
const parser = new ExactJsonParser(value);
|
|
407
|
+
return parser.parse();
|
|
408
|
+
}
|
|
409
|
+
class ExactJsonParser {
|
|
410
|
+
source;
|
|
411
|
+
#position = 0;
|
|
412
|
+
constructor(source) {
|
|
413
|
+
this.source = source;
|
|
414
|
+
}
|
|
415
|
+
parse() {
|
|
416
|
+
const value = this.#value(0);
|
|
417
|
+
this.#space();
|
|
418
|
+
if (this.#position !== this.source.length)
|
|
419
|
+
throw new Error("invalid RiffDB driver JSON");
|
|
420
|
+
return value;
|
|
421
|
+
}
|
|
422
|
+
#value(depth) {
|
|
423
|
+
if (depth > MAX_VALUE_DEPTH + 4)
|
|
424
|
+
throw new Error("RiffDB driver message exceeds the depth bound");
|
|
425
|
+
this.#space();
|
|
426
|
+
const token = this.source[this.#position];
|
|
427
|
+
if (token === '"')
|
|
428
|
+
return this.#string();
|
|
429
|
+
if (token === "{")
|
|
430
|
+
return this.#object(depth + 1);
|
|
431
|
+
if (token === "[")
|
|
432
|
+
return this.#array(depth + 1);
|
|
433
|
+
if (token === "t")
|
|
434
|
+
return this.#literal("true", true);
|
|
435
|
+
if (token === "f")
|
|
436
|
+
return this.#literal("false", false);
|
|
437
|
+
if (token === "n")
|
|
438
|
+
return this.#literal("null", null);
|
|
439
|
+
if (token === "-" || (token !== undefined && token >= "0" && token <= "9"))
|
|
440
|
+
return this.#integer();
|
|
441
|
+
throw new Error("invalid RiffDB driver JSON");
|
|
442
|
+
}
|
|
443
|
+
#object(depth) {
|
|
444
|
+
this.#position += 1;
|
|
445
|
+
const output = Object.create(null);
|
|
446
|
+
let count = 0;
|
|
447
|
+
this.#space();
|
|
448
|
+
if (this.source[this.#position] === "}") {
|
|
449
|
+
this.#position += 1;
|
|
450
|
+
return output;
|
|
451
|
+
}
|
|
452
|
+
for (;;) {
|
|
453
|
+
this.#space();
|
|
454
|
+
if (this.source[this.#position] !== '"')
|
|
455
|
+
throw new Error("invalid RiffDB driver JSON");
|
|
456
|
+
const name = this.#string();
|
|
457
|
+
if (Object.hasOwn(output, name))
|
|
458
|
+
throw new Error("duplicate RiffDB driver JSON field");
|
|
459
|
+
this.#space();
|
|
460
|
+
if (this.source[this.#position] !== ":")
|
|
461
|
+
throw new Error("invalid RiffDB driver JSON");
|
|
462
|
+
this.#position += 1;
|
|
463
|
+
output[name] = this.#value(depth);
|
|
464
|
+
count += 1;
|
|
465
|
+
if (count > MAX_COLLECTION_ITEMS)
|
|
466
|
+
throw new Error("RiffDB driver record exceeds its bound");
|
|
467
|
+
this.#space();
|
|
468
|
+
const separator = this.source[this.#position];
|
|
469
|
+
this.#position += 1;
|
|
470
|
+
if (separator === "}")
|
|
471
|
+
return output;
|
|
472
|
+
if (separator !== ",")
|
|
473
|
+
throw new Error("invalid RiffDB driver JSON");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
#array(depth) {
|
|
477
|
+
this.#position += 1;
|
|
478
|
+
const output = [];
|
|
479
|
+
this.#space();
|
|
480
|
+
if (this.source[this.#position] === "]") {
|
|
481
|
+
this.#position += 1;
|
|
482
|
+
return output;
|
|
483
|
+
}
|
|
484
|
+
for (;;) {
|
|
485
|
+
output.push(this.#value(depth));
|
|
486
|
+
if (output.length > MAX_COLLECTION_ITEMS)
|
|
487
|
+
throw new Error("RiffDB driver collection exceeds its bound");
|
|
488
|
+
this.#space();
|
|
489
|
+
const separator = this.source[this.#position];
|
|
490
|
+
this.#position += 1;
|
|
491
|
+
if (separator === "]")
|
|
492
|
+
return output;
|
|
493
|
+
if (separator !== ",")
|
|
494
|
+
throw new Error("invalid RiffDB driver JSON");
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
#string() {
|
|
498
|
+
// Scans by code unit and takes one slice. An unescaped body is already its
|
|
499
|
+
// own decoding, so the common case avoids per-character concatenation and
|
|
500
|
+
// the nested `JSON.parse`; escapes fall back to it for exact unescaping.
|
|
501
|
+
const source = this.source;
|
|
502
|
+
const start = this.#position;
|
|
503
|
+
let position = start + 1;
|
|
504
|
+
let escapes = false;
|
|
505
|
+
for (;;) {
|
|
506
|
+
if (position >= source.length)
|
|
507
|
+
throw new Error("invalid RiffDB driver JSON");
|
|
508
|
+
const code = source.charCodeAt(position);
|
|
509
|
+
if (code === 0x22)
|
|
510
|
+
break;
|
|
511
|
+
if (code === 0x5c) {
|
|
512
|
+
escapes = true;
|
|
513
|
+
position += 2;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (code < 0x20)
|
|
517
|
+
throw new Error("invalid RiffDB driver JSON");
|
|
518
|
+
position += 1;
|
|
519
|
+
}
|
|
520
|
+
this.#position = position + 1;
|
|
521
|
+
if (!escapes)
|
|
522
|
+
return source.slice(start + 1, position);
|
|
523
|
+
const decoded = JSON.parse(source.slice(start, this.#position));
|
|
524
|
+
if (typeof decoded !== "string")
|
|
525
|
+
throw new Error("invalid RiffDB driver JSON");
|
|
526
|
+
return decoded;
|
|
527
|
+
}
|
|
528
|
+
#integer() {
|
|
529
|
+
// Scans in place. The previous form sliced the whole remaining payload and
|
|
530
|
+
// ran a regex on the copy for every number, so a message cost O(n^2) in its
|
|
531
|
+
// own length, and it built three BigInts per number including the bounds.
|
|
532
|
+
const source = this.source;
|
|
533
|
+
const start = this.#position;
|
|
534
|
+
let position = start;
|
|
535
|
+
if (source.charCodeAt(position) === 0x2d)
|
|
536
|
+
position += 1;
|
|
537
|
+
const digits = position;
|
|
538
|
+
const first = source.charCodeAt(position);
|
|
539
|
+
if (first === 0x30) {
|
|
540
|
+
position += 1;
|
|
541
|
+
}
|
|
542
|
+
else if (first >= 0x31 && first <= 0x39) {
|
|
543
|
+
position += 1;
|
|
544
|
+
for (;;) {
|
|
545
|
+
const code = source.charCodeAt(position);
|
|
546
|
+
if (code >= 0x30 && code <= 0x39) {
|
|
547
|
+
position += 1;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
throw new Error("invalid RiffDB driver JSON");
|
|
555
|
+
}
|
|
556
|
+
this.#position = position;
|
|
557
|
+
const next = source.charCodeAt(position);
|
|
558
|
+
if (next === 0x2e || next === 0x65 || next === 0x45) {
|
|
559
|
+
throw new Error("non-integral RiffDB driver JSON number");
|
|
560
|
+
}
|
|
561
|
+
const text = source.slice(start, position);
|
|
562
|
+
// Any run this short is exactly representable, so the safe-integer bound
|
|
563
|
+
// needs no BigInt at all.
|
|
564
|
+
if (position - digits <= SAFE_INTEGER_DIGITS)
|
|
565
|
+
return Number(text);
|
|
566
|
+
const exact = BigInt(text);
|
|
567
|
+
if (exact >= MIN_SAFE_INTEGER_EXACT && exact <= MAX_SAFE_INTEGER_EXACT)
|
|
568
|
+
return Number(exact);
|
|
569
|
+
return text;
|
|
570
|
+
}
|
|
571
|
+
#literal(text, value) {
|
|
572
|
+
if (!this.source.startsWith(text, this.#position))
|
|
573
|
+
throw new Error("invalid RiffDB driver JSON");
|
|
574
|
+
this.#position += text.length;
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
#space() {
|
|
578
|
+
// RFC 8259 whitespace is exactly space, tab, LF and CR. The previous
|
|
579
|
+
// `/\s/u` test allocated a one-character string and ran a Unicode regex per
|
|
580
|
+
// position, and also admitted separators JSON does not allow.
|
|
581
|
+
const source = this.source;
|
|
582
|
+
let position = this.#position;
|
|
583
|
+
for (;;) {
|
|
584
|
+
const code = source.charCodeAt(position);
|
|
585
|
+
if (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) {
|
|
586
|
+
position += 1;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
this.#position = position;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function decodeResult(response) {
|
|
595
|
+
if (response.type === "packed_query_result") {
|
|
596
|
+
const outcome = boundedPattern(response.outcome, SYMBOL);
|
|
597
|
+
const resultName = boundedPattern(response.result_name, SYMBOL);
|
|
598
|
+
const entity = boundedPattern(response.entity, SYMBOL);
|
|
599
|
+
if (!Array.isArray(response.fields) || response.fields.length < 1
|
|
600
|
+
|| response.fields.length > MAX_COLLECTION_ITEMS
|
|
601
|
+
|| !Number.isInteger(response.row_count) || response.row_count < 0
|
|
602
|
+
|| response.row_count > MAX_COLLECTION_ITEMS
|
|
603
|
+
|| !Array.isArray(response.columns) || response.columns.length !== response.fields.length) {
|
|
604
|
+
throw new Error("RiffDB driver returned an invalid packed result");
|
|
605
|
+
}
|
|
606
|
+
const fields = response.fields.map((field) => boundedPattern(field, SYMBOL));
|
|
607
|
+
if (new Set(fields).size !== fields.length)
|
|
608
|
+
throw new Error("RiffDB driver returned an invalid packed result");
|
|
609
|
+
const rowCount = response.row_count;
|
|
610
|
+
const columns = response.columns.map((raw) => {
|
|
611
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
612
|
+
throw new Error("RiffDB driver returned an invalid packed result");
|
|
613
|
+
const column = raw;
|
|
614
|
+
if (typeof column.data !== "string" || !Array.isArray(column.offsets) || column.offsets.length !== rowCount + 1)
|
|
615
|
+
throw new Error("RiffDB driver returned an invalid packed result");
|
|
616
|
+
const data = Buffer.from(column.data, "base64");
|
|
617
|
+
if (data.toString("base64") !== column.data)
|
|
618
|
+
throw new Error("RiffDB driver returned an invalid packed result");
|
|
619
|
+
const offsets = column.offsets.map((offset) => {
|
|
620
|
+
if (!Number.isInteger(offset) || offset < 0 || offset > 0xffff_ffff)
|
|
621
|
+
throw new Error("RiffDB driver returned an invalid packed result");
|
|
622
|
+
return offset;
|
|
623
|
+
});
|
|
624
|
+
if (offsets[0] !== 0 || offsets.at(-1) !== data.length || offsets.some((offset, index) => index > 0 && offsets[index - 1] > offset))
|
|
625
|
+
throw new Error("RiffDB driver returned an invalid packed result");
|
|
626
|
+
return { data: new Uint8Array(data), offsets };
|
|
627
|
+
});
|
|
628
|
+
const head = optionalNonnegativeBigInt(response.application_head);
|
|
629
|
+
if (head === undefined)
|
|
630
|
+
throw new Error("RiffDB driver omitted the query frontier");
|
|
631
|
+
const cursor = optionalBoundedString(response.cursor, 16_384);
|
|
632
|
+
return { packed: { outcome, resultName, entity, fields, rowCount, columns }, applicationHead: head, replayed: false, ...(cursor === undefined ? {} : { cursor }) };
|
|
633
|
+
}
|
|
634
|
+
if (response.type === "compact_query_result") {
|
|
635
|
+
const outcome = boundedPattern(response.outcome, SYMBOL);
|
|
636
|
+
const resultName = boundedPattern(response.result_name, SYMBOL);
|
|
637
|
+
const entity = boundedPattern(response.entity, SYMBOL);
|
|
638
|
+
if (!Array.isArray(response.fields) || response.fields.length < 1
|
|
639
|
+
|| response.fields.length > MAX_COLLECTION_ITEMS
|
|
640
|
+
|| !Array.isArray(response.rows) || response.rows.length > MAX_COLLECTION_ITEMS) {
|
|
641
|
+
throw new Error("RiffDB driver returned an invalid compact result");
|
|
642
|
+
}
|
|
643
|
+
const fields = response.fields.map((field) => boundedPattern(field, SYMBOL));
|
|
644
|
+
if (new Set(fields).size !== fields.length) {
|
|
645
|
+
throw new Error("RiffDB driver returned an invalid compact result");
|
|
646
|
+
}
|
|
647
|
+
const rows = response.rows.map((raw) => {
|
|
648
|
+
if (!Array.isArray(raw) || raw.length !== fields.length) {
|
|
649
|
+
throw new Error("RiffDB driver returned an invalid compact result");
|
|
650
|
+
}
|
|
651
|
+
return raw.map((value) => validateDriverValue(value, 0));
|
|
652
|
+
});
|
|
653
|
+
const head = optionalNonnegativeBigInt(response.application_head);
|
|
654
|
+
if (head === undefined)
|
|
655
|
+
throw new Error("RiffDB driver omitted the query frontier");
|
|
656
|
+
const cursor = optionalBoundedString(response.cursor, 16_384);
|
|
657
|
+
return {
|
|
658
|
+
compact: { outcome, resultName, entity, fields, rows },
|
|
659
|
+
applicationHead: head,
|
|
660
|
+
replayed: false,
|
|
661
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
if (response.type !== "result")
|
|
665
|
+
throw new Error("RiffDB driver returned an invalid result");
|
|
666
|
+
const value = validateDriverValue(response.value, 0);
|
|
667
|
+
const head = optionalNonnegativeBigInt(response.application_head);
|
|
668
|
+
const cursor = optionalBoundedString(response.cursor, 16_384);
|
|
669
|
+
if (typeof response.replayed !== "boolean")
|
|
670
|
+
throw new Error("RiffDB driver returned an invalid result");
|
|
671
|
+
return {
|
|
672
|
+
value,
|
|
673
|
+
replayed: response.replayed,
|
|
674
|
+
...(head === undefined ? {} : { applicationHead: head }),
|
|
675
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function decodeBatchResult(response) {
|
|
679
|
+
if (response.type !== "batch_result" || !Array.isArray(response.items)
|
|
680
|
+
|| response.items.length > MAX_COLLECTION_ITEMS) {
|
|
681
|
+
throw new Error("RiffDB driver returned an invalid batch result");
|
|
682
|
+
}
|
|
683
|
+
const checkpoint = boundedInteger(response.checkpoint, 0, MAX_COLLECTION_ITEMS);
|
|
684
|
+
const total = boundedInteger(response.total, 1, MAX_COLLECTION_ITEMS);
|
|
685
|
+
if (checkpoint > total)
|
|
686
|
+
throw new Error("RiffDB driver returned an invalid batch result");
|
|
687
|
+
const items = response.items.map((raw) => {
|
|
688
|
+
const item = exactObject(raw);
|
|
689
|
+
const index = boundedInteger(item.index, 0, total - 1);
|
|
690
|
+
const outcome = exactObject(item.outcome);
|
|
691
|
+
if (outcome.type === "result") {
|
|
692
|
+
const value = validateDriverValue(outcome.value, 0);
|
|
693
|
+
const commitSequence = optionalPositiveBigInt(outcome.commit_sequence);
|
|
694
|
+
const outcomeUri = optionalBoundedString(outcome.outcome_uri, 16_384);
|
|
695
|
+
if (typeof outcome.replayed !== "boolean")
|
|
696
|
+
throw new Error("RiffDB driver returned an invalid batch item");
|
|
697
|
+
return {
|
|
698
|
+
index,
|
|
699
|
+
result: {
|
|
700
|
+
value,
|
|
701
|
+
replayed: outcome.replayed,
|
|
702
|
+
...(commitSequence === undefined ? {} : { commitSequence }),
|
|
703
|
+
...(outcomeUri === undefined ? {} : { outcomeUri }),
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
if (outcome.type === "error")
|
|
708
|
+
return { index, error: decodeError(outcome) };
|
|
709
|
+
throw new Error("RiffDB driver returned an invalid batch item");
|
|
710
|
+
});
|
|
711
|
+
return { items, checkpoint, total };
|
|
712
|
+
}
|
|
713
|
+
function decodeError(response) {
|
|
714
|
+
const code = boundedPattern(response.code, /^[A-Z0-9-]{1,64}$/);
|
|
715
|
+
const category = boundedPattern(response.category, /^[a-z_]{1,64}$/);
|
|
716
|
+
const message = boundedString(response.message, 4_096);
|
|
717
|
+
const retryability = boundedPattern(response.retryability ?? "not_retryable", /^[a-z_]{1,64}$/);
|
|
718
|
+
const recoveryAction = boundedPattern(response.recovery_action, /^[a-z_]{1,64}$/);
|
|
719
|
+
if (!Array.isArray(response.symbol_path) || response.symbol_path.length > 16) {
|
|
720
|
+
throw new Error("RiffDB driver returned invalid error context");
|
|
721
|
+
}
|
|
722
|
+
const symbolPath = response.symbol_path.map((value) => boundedPattern(value, SYMBOL));
|
|
723
|
+
const operation = optionalPattern(response.operation, SYMBOL);
|
|
724
|
+
const contractLineage = optionalPattern(response.contract_lineage, SYMBOL);
|
|
725
|
+
const contractVersion = optionalPositiveBigInt(response.contract_version);
|
|
726
|
+
if ((contractLineage === undefined) !== (contractVersion === undefined)) {
|
|
727
|
+
throw new Error("RiffDB driver returned invalid error context");
|
|
728
|
+
}
|
|
729
|
+
const traceId = optionalPattern(response.trace_id, REQUEST_ID);
|
|
730
|
+
const incidentId = optionalPattern(response.incident_id, REQUEST_ID);
|
|
731
|
+
if (typeof response.outcome_uncertain !== "boolean")
|
|
732
|
+
throw new Error("RiffDB driver returned invalid error context");
|
|
733
|
+
return new DriverApplicationError({
|
|
734
|
+
code, category, symbolPath, message, retryability, recoveryAction,
|
|
735
|
+
outcomeUncertain: response.outcome_uncertain,
|
|
736
|
+
...(operation === undefined ? {} : { operation }),
|
|
737
|
+
...(contractLineage === undefined ? {} : { contractLineage }),
|
|
738
|
+
...(contractVersion === undefined ? {} : { contractVersion }),
|
|
739
|
+
...(traceId === undefined ? {} : { traceId }),
|
|
740
|
+
...(incidentId === undefined ? {} : { incidentId }),
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
function resolveTransportTenant(options) {
|
|
744
|
+
const envTenant = process.env.RIFFDB_TENANT;
|
|
745
|
+
const candidate = options.tenant ?? options.identity.tenant ?? (envTenant && envTenant.length > 0 ? envTenant : undefined);
|
|
746
|
+
if (candidate === undefined)
|
|
747
|
+
return undefined;
|
|
748
|
+
if (typeof candidate !== "string" || candidate.length < 1 || candidate.length > 128 || !/^[\x21-\x7e]+$/.test(candidate)) {
|
|
749
|
+
throw new Error("invalid RiffDB tenant identifier");
|
|
750
|
+
}
|
|
751
|
+
return candidate;
|
|
752
|
+
}
|
|
753
|
+
function validateTransportOptions(options) {
|
|
754
|
+
if (!options.socketPath.startsWith("/") || options.socketPath.length > 4_096) {
|
|
755
|
+
throw new Error("invalid RiffDB driver socket path");
|
|
756
|
+
}
|
|
757
|
+
const identity = options.identity;
|
|
758
|
+
for (const value of [
|
|
759
|
+
identity.applicationManifestHash, identity.operationCatalogHash,
|
|
760
|
+
identity.contractBundleHash, identity.roleDefinitionHash, identity.remoteIdentityHash,
|
|
761
|
+
]) {
|
|
762
|
+
if (!HASH.test(value))
|
|
763
|
+
throw new Error("invalid RiffDB driver identity");
|
|
764
|
+
}
|
|
765
|
+
if (!SYMBOL.test(identity.contractLineage) || !SYMBOL.test(identity.database)
|
|
766
|
+
|| !SYMBOL.test(identity.role) || identity.contractVersion < 1n
|
|
767
|
+
|| identity.contractVersion > 18446744073709551615n) {
|
|
768
|
+
throw new Error("invalid RiffDB driver identity");
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function validateOperation(operation) {
|
|
772
|
+
if (!SYMBOL.test(operation.name) || !HASH.test(operation.inputSchemaHash)) {
|
|
773
|
+
throw new Error("invalid generated RiffDB operation");
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function validateDriverRecord(value, depth = 0) {
|
|
777
|
+
if (Object.keys(value).length > MAX_COLLECTION_ITEMS)
|
|
778
|
+
throw new Error("invalid generated RiffDB input");
|
|
779
|
+
for (const [name, field] of Object.entries(value)) {
|
|
780
|
+
if (!SYMBOL.test(name))
|
|
781
|
+
throw new Error("invalid generated RiffDB input");
|
|
782
|
+
validateDriverValue(field, depth);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function validateDriverValue(value, depth) {
|
|
786
|
+
if (depth > MAX_VALUE_DEPTH)
|
|
787
|
+
throw new Error("RiffDB driver value exceeds the depth bound");
|
|
788
|
+
const item = exactObject(value);
|
|
789
|
+
const type = item.type;
|
|
790
|
+
if (type === "null" && Object.keys(item).length === 1)
|
|
791
|
+
return item;
|
|
792
|
+
if (type === "bool" && typeof item.value === "boolean")
|
|
793
|
+
return item;
|
|
794
|
+
if (type === "i64") {
|
|
795
|
+
BigInt(boundedPattern(item.value, /^-?[0-9]{1,20}$/));
|
|
796
|
+
return item;
|
|
797
|
+
}
|
|
798
|
+
if (type === "u64") {
|
|
799
|
+
BigInt(boundedPattern(item.value, /^[0-9]{1,20}$/));
|
|
800
|
+
return item;
|
|
801
|
+
}
|
|
802
|
+
if (type === "string" && typeof item.value === "string" && item.value.length <= 262_144)
|
|
803
|
+
return item;
|
|
804
|
+
// Byte values use padded Base64 on this bounded JSON protocol. Their
|
|
805
|
+
// canonical decoded size is checked by the generated facade and service;
|
|
806
|
+
// the unchanged frame limit bounds the local representation.
|
|
807
|
+
if (type === "bytes" && typeof item.value === "string" && item.value.length <= MAX_ENCODED_BYTES_VALUE)
|
|
808
|
+
return item;
|
|
809
|
+
if (type === "uuid" && typeof item.value === "string" && UUID.test(item.value))
|
|
810
|
+
return item;
|
|
811
|
+
if (type === "enum" && typeof item.value === "string" && SYMBOL.test(item.value))
|
|
812
|
+
return item;
|
|
813
|
+
if (type === "date") {
|
|
814
|
+
boundedPattern(item.value, /^-?[0-9]{1,11}$/);
|
|
815
|
+
return item;
|
|
816
|
+
}
|
|
817
|
+
if (type === "timestamp") {
|
|
818
|
+
const timestamp = exactObject(item.value);
|
|
819
|
+
boundedPattern(timestamp.seconds, /^-?[0-9]{1,32}$/);
|
|
820
|
+
boundedInteger(timestamp.nanos, 0, 999_999_999);
|
|
821
|
+
return item;
|
|
822
|
+
}
|
|
823
|
+
if (type === "decimal") {
|
|
824
|
+
validateDecimal(item.value);
|
|
825
|
+
return item;
|
|
826
|
+
}
|
|
827
|
+
if (type === "money") {
|
|
828
|
+
const money = exactObject(item.value);
|
|
829
|
+
boundedPattern(money.currency, /^[A-Z]{3}$/);
|
|
830
|
+
validateDecimal(money.amount);
|
|
831
|
+
return item;
|
|
832
|
+
}
|
|
833
|
+
if (type === "vector") {
|
|
834
|
+
const vector = exactObject(item.value);
|
|
835
|
+
if (Object.keys(vector).length !== 1 || !Array.isArray(vector.component_bits) || vector.component_bits.length < 1
|
|
836
|
+
|| vector.component_bits.length > MAX_COLLECTION_ITEMS)
|
|
837
|
+
throw new Error("invalid RiffDB driver vector");
|
|
838
|
+
vector.component_bits.forEach((bits) => boundedInteger(bits, 0, 4_294_967_295));
|
|
839
|
+
return item;
|
|
840
|
+
}
|
|
841
|
+
if (type === "list" && Array.isArray(item.value) && item.value.length <= MAX_COLLECTION_ITEMS) {
|
|
842
|
+
item.value.forEach((entry) => validateDriverValue(entry, depth + 1));
|
|
843
|
+
return item;
|
|
844
|
+
}
|
|
845
|
+
if (type === "record") {
|
|
846
|
+
const record = exactObject(item.value);
|
|
847
|
+
validateDriverRecord(record, depth + 1);
|
|
848
|
+
return item;
|
|
849
|
+
}
|
|
850
|
+
throw new Error("RiffDB driver returned an invalid value");
|
|
851
|
+
}
|
|
852
|
+
function validateDecimal(value) {
|
|
853
|
+
const decimal = exactObject(value);
|
|
854
|
+
boundedString(decimal.coefficient, 1_366);
|
|
855
|
+
boundedInteger(decimal.scale, 0, 4_294_967_295);
|
|
856
|
+
if (decimal.precision !== undefined && decimal.precision !== null)
|
|
857
|
+
boundedInteger(decimal.precision, 1, 4_294_967_295);
|
|
858
|
+
}
|
|
859
|
+
function signedTwosComplement(value) {
|
|
860
|
+
let bytes = 1;
|
|
861
|
+
while (value < -(1n << BigInt(bytes * 8 - 1)) || value >= (1n << BigInt(bytes * 8 - 1)))
|
|
862
|
+
bytes += 1;
|
|
863
|
+
let encoded = value < 0n ? (1n << BigInt(bytes * 8)) + value : value;
|
|
864
|
+
const output = new Uint8Array(bytes);
|
|
865
|
+
for (let index = bytes - 1; index >= 0; index -= 1) {
|
|
866
|
+
output[index] = Number(encoded & 0xffn);
|
|
867
|
+
encoded >>= 8n;
|
|
868
|
+
}
|
|
869
|
+
return output;
|
|
870
|
+
}
|
|
871
|
+
function exactObject(value) {
|
|
872
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
873
|
+
throw new Error("invalid RiffDB driver message");
|
|
874
|
+
return value;
|
|
875
|
+
}
|
|
876
|
+
function boundedString(value, maximum) {
|
|
877
|
+
if (typeof value !== "string" || value.length < 1 || value.length > maximum)
|
|
878
|
+
throw new Error("invalid RiffDB driver message");
|
|
879
|
+
return value;
|
|
880
|
+
}
|
|
881
|
+
function boundedPattern(value, pattern) {
|
|
882
|
+
const text = boundedString(value, 4_096);
|
|
883
|
+
if (!pattern.test(text))
|
|
884
|
+
throw new Error("invalid RiffDB driver message");
|
|
885
|
+
return text;
|
|
886
|
+
}
|
|
887
|
+
function optionalPattern(value, pattern) {
|
|
888
|
+
return value === null || value === undefined ? undefined : boundedPattern(value, pattern);
|
|
889
|
+
}
|
|
890
|
+
function optionalBoundedString(value, maximum) {
|
|
891
|
+
return value === null || value === undefined ? undefined : boundedString(value, maximum);
|
|
892
|
+
}
|
|
893
|
+
function boundedInteger(value, minimum, maximum) {
|
|
894
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
895
|
+
throw new Error("invalid RiffDB driver message");
|
|
896
|
+
}
|
|
897
|
+
return value;
|
|
898
|
+
}
|
|
899
|
+
function optionalPositiveBigInt(value) {
|
|
900
|
+
if (value === null || value === undefined)
|
|
901
|
+
return undefined;
|
|
902
|
+
let parsed;
|
|
903
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0)
|
|
904
|
+
parsed = BigInt(value);
|
|
905
|
+
else if (typeof value === "string" && /^[1-9][0-9]{0,19}$/.test(value))
|
|
906
|
+
parsed = BigInt(value);
|
|
907
|
+
else
|
|
908
|
+
throw new Error("invalid RiffDB driver message");
|
|
909
|
+
if (parsed > MAX_U64)
|
|
910
|
+
throw new Error("invalid RiffDB driver message");
|
|
911
|
+
return parsed;
|
|
912
|
+
}
|
|
913
|
+
function optionalNonnegativeBigInt(value) {
|
|
914
|
+
if (value === null || value === undefined)
|
|
915
|
+
return undefined;
|
|
916
|
+
let parsed;
|
|
917
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
|
|
918
|
+
parsed = BigInt(value);
|
|
919
|
+
else if (typeof value === "string" && /^(?:0|[1-9][0-9]{0,19})$/.test(value))
|
|
920
|
+
parsed = BigInt(value);
|
|
921
|
+
else
|
|
922
|
+
throw new Error("invalid RiffDB driver message");
|
|
923
|
+
if (parsed > MAX_U64)
|
|
924
|
+
throw new Error("invalid RiffDB driver message");
|
|
925
|
+
return parsed;
|
|
926
|
+
}
|
|
927
|
+
function requiredPositiveBigInt(value) {
|
|
928
|
+
const parsed = optionalPositiveBigInt(value);
|
|
929
|
+
if (parsed === undefined)
|
|
930
|
+
throw new Error("invalid RiffDB driver message");
|
|
931
|
+
return parsed;
|
|
932
|
+
}
|