@riffdb/client 0.2.0 → 0.2.2
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 +3 -3
- package/dist/driver.d.ts +27 -1
- package/dist/driver.js +359 -45
- package/dist/index.d.ts +86 -3
- package/dist/index.js +521 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,8 +7,8 @@ compiler IDs, storage keys, or an authorization bypass.
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install --save-dev @riffdb/cli@0.
|
|
11
|
-
npm install @riffdb/client@0.
|
|
10
|
+
npm install --save-dev @riffdb/cli@0.2.2
|
|
11
|
+
npm install @riffdb/client@0.2.2
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Four-step application story
|
|
@@ -40,6 +40,6 @@ response must be resolved with the same idempotency key.
|
|
|
40
40
|
|
|
41
41
|
## Version compatibility
|
|
42
42
|
|
|
43
|
-
This is a 0.
|
|
43
|
+
This is a 0.2.x pre-1.0 package. It carries the exact durable-format statement
|
|
44
44
|
from `release/durable-format-manifest-v1.json`; downgrade is unsupported and a
|
|
45
45
|
breaking epoch requires symbolic export/reimport. Sealed and explicitly offline bundles retain vendored @riffdb/application, riffdb-application, riffdb.dev/application, and the source-contained Rust SDK.
|
package/dist/driver.d.ts
CHANGED
|
@@ -59,6 +59,18 @@ export interface ExactMoneyValue<Currency extends string = string> {
|
|
|
59
59
|
export declare function exactDecimal(value: string, precision: number, scale: number): ExactDecimalValue;
|
|
60
60
|
export declare function exactMoney<const Currency extends string>(currency: Currency, value: string): ExactMoneyValue<Currency>;
|
|
61
61
|
export interface DriverApplicationIdentity {
|
|
62
|
+
readonly applicationManifestHash?: string;
|
|
63
|
+
readonly operationCatalogHash?: string;
|
|
64
|
+
readonly contractLineage?: string;
|
|
65
|
+
readonly contractVersion?: bigint;
|
|
66
|
+
readonly contractBundleHash?: string;
|
|
67
|
+
readonly database?: string;
|
|
68
|
+
readonly role?: string;
|
|
69
|
+
readonly roleDefinitionHash?: string;
|
|
70
|
+
readonly remoteIdentityHash?: string;
|
|
71
|
+
readonly tenant?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface ResolvedDriverApplicationIdentity {
|
|
62
74
|
readonly applicationManifestHash: string;
|
|
63
75
|
readonly operationCatalogHash: string;
|
|
64
76
|
readonly contractLineage: string;
|
|
@@ -72,8 +84,18 @@ export interface DriverApplicationIdentity {
|
|
|
72
84
|
}
|
|
73
85
|
export interface DriverApplicationTransportOptions {
|
|
74
86
|
readonly socketPath: string;
|
|
75
|
-
readonly identity
|
|
87
|
+
readonly identity?: DriverApplicationIdentity;
|
|
88
|
+
readonly tenant?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface DriverConnectFromEnvOptions {
|
|
91
|
+
readonly socketPath?: string;
|
|
92
|
+
readonly identity?: Partial<DriverApplicationIdentity>;
|
|
93
|
+
readonly role?: string;
|
|
94
|
+
readonly database?: string;
|
|
76
95
|
readonly tenant?: string;
|
|
96
|
+
readonly prefix?: string;
|
|
97
|
+
readonly envPrefix?: string;
|
|
98
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
77
99
|
}
|
|
78
100
|
export interface DriverOperation {
|
|
79
101
|
readonly name: string;
|
|
@@ -163,9 +185,13 @@ export declare class DriverApplicationTransport {
|
|
|
163
185
|
private constructor();
|
|
164
186
|
/** Returns the bound tenant identifier when this session is tenant-scoped. */
|
|
165
187
|
get tenant(): string | undefined;
|
|
188
|
+
/** Returns the resolved application identity verified during driver handshake. */
|
|
189
|
+
get identity(): ResolvedDriverApplicationIdentity;
|
|
166
190
|
static connect(options: DriverApplicationTransportOptions): Promise<DriverApplicationTransport>;
|
|
191
|
+
static connectFromEnv(options?: DriverConnectFromEnvOptions): Promise<DriverApplicationTransport>;
|
|
167
192
|
invoke(operation: DriverOperation, input: Readonly<Record<string, DriverValue>>, options?: DriverInvokeOptions): Promise<DriverResult>;
|
|
168
193
|
batch(operation: DriverOperation, items: ReadonlyArray<Readonly<Record<string, DriverValue>>>, concurrency: number, checkpoint: number, options?: DriverInvokeOptions): Promise<DriverBatchResult>;
|
|
169
194
|
/** Stops new work, sends a clean local EOF, and rejects unfinished waits. */
|
|
170
195
|
shutdown(): Promise<void>;
|
|
171
196
|
}
|
|
197
|
+
export declare function resolveTransportOptionsFromEnv(options?: DriverConnectFromEnvOptions): DriverApplicationTransportOptions;
|
package/dist/driver.js
CHANGED
|
@@ -61,28 +61,62 @@ export class DriverApplicationError extends Error {
|
|
|
61
61
|
* surface. Those semantics remain owned by the Rust host.
|
|
62
62
|
*/
|
|
63
63
|
export class DriverApplicationTransport {
|
|
64
|
+
#socketPath;
|
|
64
65
|
#socket;
|
|
65
66
|
#identity;
|
|
67
|
+
#resolvedIdentity;
|
|
66
68
|
#tenant;
|
|
67
69
|
#requestPrefix;
|
|
68
70
|
#pending = new Map();
|
|
69
|
-
#
|
|
71
|
+
#inputChunks = [];
|
|
72
|
+
#inputLength = 0;
|
|
70
73
|
#nextRequest = 1;
|
|
71
74
|
#closed = false;
|
|
72
|
-
|
|
75
|
+
#disconnected = false;
|
|
76
|
+
#reconnecting;
|
|
77
|
+
constructor(socketPath, socket, identity, tenant) {
|
|
78
|
+
this.#socketPath = socketPath;
|
|
73
79
|
this.#socket = socket;
|
|
74
80
|
this.#identity = identity;
|
|
75
81
|
this.#tenant = tenant;
|
|
76
82
|
nextSession = nextSession === Number.MAX_SAFE_INTEGER ? 1 : nextSession + 1;
|
|
77
83
|
this.#requestPrefix = `ts.${process.pid}.${nextSession}`;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
this.#attachSocket(socket);
|
|
85
|
+
}
|
|
86
|
+
#attachSocket(socket) {
|
|
87
|
+
socket.on("data", (chunk) => {
|
|
88
|
+
if (this.#socket !== socket)
|
|
89
|
+
return;
|
|
90
|
+
this.#receive(chunk);
|
|
91
|
+
});
|
|
92
|
+
socket.on("error", () => {
|
|
93
|
+
if (this.#socket !== socket)
|
|
94
|
+
return;
|
|
95
|
+
this.#disconnected = true;
|
|
96
|
+
this.#inputChunks.length = 0;
|
|
97
|
+
this.#inputLength = 0;
|
|
98
|
+
this.#fail(new Error("RiffDB driver session failed"));
|
|
99
|
+
});
|
|
100
|
+
socket.on("close", () => {
|
|
101
|
+
if (this.#socket !== socket)
|
|
102
|
+
return;
|
|
103
|
+
this.#disconnected = true;
|
|
104
|
+
this.#inputChunks.length = 0;
|
|
105
|
+
this.#inputLength = 0;
|
|
106
|
+
this.#fail(new Error("RiffDB driver session closed"));
|
|
107
|
+
});
|
|
81
108
|
}
|
|
82
109
|
/** Returns the bound tenant identifier when this session is tenant-scoped. */
|
|
83
110
|
get tenant() {
|
|
84
111
|
return this.#tenant;
|
|
85
112
|
}
|
|
113
|
+
/** Returns the resolved application identity verified during driver handshake. */
|
|
114
|
+
get identity() {
|
|
115
|
+
if (this.#resolvedIdentity === undefined) {
|
|
116
|
+
throw new Error("RiffDB driver session has not completed handshake");
|
|
117
|
+
}
|
|
118
|
+
return this.#resolvedIdentity;
|
|
119
|
+
}
|
|
86
120
|
static async connect(options) {
|
|
87
121
|
const tenant = resolveTransportTenant(options);
|
|
88
122
|
validateTransportOptions(options);
|
|
@@ -92,7 +126,7 @@ export class DriverApplicationTransport {
|
|
|
92
126
|
socket.once("connect", () => { socket.off("error", onError); resolve(); });
|
|
93
127
|
socket.once("error", onError);
|
|
94
128
|
});
|
|
95
|
-
const transport = new DriverApplicationTransport(socket, options.identity, tenant);
|
|
129
|
+
const transport = new DriverApplicationTransport(options.socketPath, socket, options.identity ?? {}, tenant);
|
|
96
130
|
try {
|
|
97
131
|
await transport.#handshake();
|
|
98
132
|
return transport;
|
|
@@ -102,9 +136,18 @@ export class DriverApplicationTransport {
|
|
|
102
136
|
throw error;
|
|
103
137
|
}
|
|
104
138
|
}
|
|
139
|
+
static async connectFromEnv(options = {}) {
|
|
140
|
+
return DriverApplicationTransport.connect(resolveTransportOptionsFromEnv(options));
|
|
141
|
+
}
|
|
105
142
|
async invoke(operation, input, options = {}) {
|
|
106
143
|
validateOperation(operation);
|
|
107
144
|
validateDriverRecord(input);
|
|
145
|
+
const lowered = lowerOptions(options);
|
|
146
|
+
if (options.signal?.aborted)
|
|
147
|
+
throw new Error("RiffDB driver request aborted");
|
|
148
|
+
if (this.#disconnected || this.#socket.destroyed) {
|
|
149
|
+
await this.#ensureConnected();
|
|
150
|
+
}
|
|
108
151
|
const requestId = this.#requestId("invoke");
|
|
109
152
|
const response = await this.#request({
|
|
110
153
|
type: "invoke",
|
|
@@ -112,7 +155,7 @@ export class DriverApplicationTransport {
|
|
|
112
155
|
operation: operation.name,
|
|
113
156
|
input_schema_hash: operation.inputSchemaHash,
|
|
114
157
|
input,
|
|
115
|
-
options:
|
|
158
|
+
options: lowered,
|
|
116
159
|
}, requestId, options.signal);
|
|
117
160
|
return decodeResult(response);
|
|
118
161
|
}
|
|
@@ -127,6 +170,12 @@ export class DriverApplicationTransport {
|
|
|
127
170
|
}
|
|
128
171
|
for (const input of items)
|
|
129
172
|
validateDriverRecord(input);
|
|
173
|
+
const lowered = lowerOptions(options);
|
|
174
|
+
if (options.signal?.aborted)
|
|
175
|
+
throw new Error("RiffDB driver request aborted");
|
|
176
|
+
if (this.#disconnected || this.#socket.destroyed) {
|
|
177
|
+
await this.#ensureConnected();
|
|
178
|
+
}
|
|
130
179
|
const requestId = this.#requestId("batch");
|
|
131
180
|
const response = await this.#request({
|
|
132
181
|
type: "batch",
|
|
@@ -136,15 +185,16 @@ export class DriverApplicationTransport {
|
|
|
136
185
|
items,
|
|
137
186
|
concurrency,
|
|
138
187
|
checkpoint,
|
|
139
|
-
options:
|
|
188
|
+
options: lowered,
|
|
140
189
|
}, requestId, options.signal);
|
|
141
|
-
return decodeBatchResult(response);
|
|
190
|
+
return decodeBatchResult(response, items.length, checkpoint);
|
|
142
191
|
}
|
|
143
192
|
/** Stops new work, sends a clean local EOF, and rejects unfinished waits. */
|
|
144
193
|
async shutdown() {
|
|
145
194
|
if (this.#closed)
|
|
146
195
|
return;
|
|
147
196
|
this.#closed = true;
|
|
197
|
+
this.#disconnected = true;
|
|
148
198
|
this.#fail(new Error("RiffDB driver session closed"));
|
|
149
199
|
await new Promise((resolve) => {
|
|
150
200
|
if (this.#socket.destroyed) {
|
|
@@ -154,38 +204,121 @@ export class DriverApplicationTransport {
|
|
|
154
204
|
this.#socket.end(resolve);
|
|
155
205
|
});
|
|
156
206
|
}
|
|
207
|
+
async #ensureConnected() {
|
|
208
|
+
if (this.#closed)
|
|
209
|
+
throw new Error("RiffDB driver session closed");
|
|
210
|
+
if (!this.#disconnected && !this.#socket.destroyed)
|
|
211
|
+
return;
|
|
212
|
+
if (this.#reconnecting !== undefined) {
|
|
213
|
+
await this.#reconnecting;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const task = (async () => {
|
|
217
|
+
const socket = createConnection({ path: this.#socketPath });
|
|
218
|
+
await new Promise((resolve, reject) => {
|
|
219
|
+
const onError = () => reject(new Error("RiffDB driver session failed"));
|
|
220
|
+
socket.once("connect", () => { socket.off("error", onError); resolve(); });
|
|
221
|
+
socket.once("error", onError);
|
|
222
|
+
});
|
|
223
|
+
if (this.#closed) {
|
|
224
|
+
socket.destroy();
|
|
225
|
+
throw new Error("RiffDB driver session closed");
|
|
226
|
+
}
|
|
227
|
+
this.#socket = socket;
|
|
228
|
+
this.#inputChunks.length = 0;
|
|
229
|
+
this.#inputLength = 0;
|
|
230
|
+
this.#disconnected = false;
|
|
231
|
+
this.#attachSocket(socket);
|
|
232
|
+
try {
|
|
233
|
+
await this.#handshake();
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
this.#disconnected = true;
|
|
237
|
+
socket.destroy();
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
})();
|
|
241
|
+
this.#reconnecting = task;
|
|
242
|
+
try {
|
|
243
|
+
await task;
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
if (this.#reconnecting === task) {
|
|
247
|
+
this.#reconnecting = undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
157
251
|
async #handshake() {
|
|
158
252
|
const requestId = this.#requestId("handshake");
|
|
159
253
|
const identity = this.#identity;
|
|
254
|
+
const manifestHash = identity.applicationManifestHash ?? "";
|
|
255
|
+
const catalogHash = identity.operationCatalogHash ?? "";
|
|
256
|
+
const contractLineage = identity.contractLineage ?? "";
|
|
257
|
+
const contractVersion = identity.contractVersion ?? 0n;
|
|
258
|
+
const contractBundleHash = identity.contractBundleHash ?? "";
|
|
259
|
+
const database = identity.database ?? "";
|
|
260
|
+
const role = identity.role ?? "";
|
|
261
|
+
const roleDefinitionHash = identity.roleDefinitionHash ?? "";
|
|
262
|
+
const remoteIdentityHash = identity.remoteIdentityHash ?? "";
|
|
160
263
|
const response = await this.#request({
|
|
161
264
|
type: "handshake",
|
|
162
265
|
request_id: requestId,
|
|
163
266
|
protocol_version: DRIVER_PROTOCOL_VERSION,
|
|
164
|
-
application_manifest_hash:
|
|
165
|
-
operation_catalog_hash:
|
|
166
|
-
contract_lineage:
|
|
167
|
-
contract_version:
|
|
168
|
-
contract_bundle_hash:
|
|
267
|
+
application_manifest_hash: manifestHash,
|
|
268
|
+
operation_catalog_hash: catalogHash,
|
|
269
|
+
contract_lineage: contractLineage,
|
|
270
|
+
contract_version: contractVersion,
|
|
271
|
+
contract_bundle_hash: contractBundleHash,
|
|
169
272
|
value_registry_hash: DRIVER_VALUE_REGISTRY_HASH,
|
|
170
273
|
error_registry_hash: DRIVER_ERROR_REGISTRY_HASH,
|
|
171
|
-
database
|
|
172
|
-
role
|
|
173
|
-
role_definition_hash:
|
|
174
|
-
remote_identity_hash:
|
|
274
|
+
database,
|
|
275
|
+
role,
|
|
276
|
+
role_definition_hash: roleDefinitionHash,
|
|
277
|
+
remote_identity_hash: remoteIdentityHash,
|
|
175
278
|
}, requestId);
|
|
279
|
+
const resolvedContractVersion = requiredPositiveBigInt(response.contract_version);
|
|
176
280
|
if (response.type !== "handshake"
|
|
177
281
|
|| response.protocol_version !== DRIVER_PROTOCOL_VERSION
|
|
178
|
-
|| response.application_manifest_hash !==
|
|
179
|
-
|| response.
|
|
180
|
-
|| response.
|
|
181
|
-
||
|
|
182
|
-
|| response.
|
|
183
|
-
|| response.
|
|
184
|
-
|| response.
|
|
185
|
-
|| response.
|
|
186
|
-
|| response.
|
|
282
|
+
|| typeof response.application_manifest_hash !== "string"
|
|
283
|
+
|| !HASH.test(response.application_manifest_hash)
|
|
284
|
+
|| (manifestHash !== "" && response.application_manifest_hash !== manifestHash)
|
|
285
|
+
|| typeof response.operation_catalog_hash !== "string"
|
|
286
|
+
|| !HASH.test(response.operation_catalog_hash)
|
|
287
|
+
|| (catalogHash !== "" && response.operation_catalog_hash !== catalogHash)
|
|
288
|
+
|| typeof response.contract_lineage !== "string"
|
|
289
|
+
|| !SYMBOL.test(response.contract_lineage)
|
|
290
|
+
|| (contractLineage !== "" && response.contract_lineage !== contractLineage)
|
|
291
|
+
|| (contractVersion !== 0n && resolvedContractVersion !== contractVersion)
|
|
292
|
+
|| typeof response.contract_bundle_hash !== "string"
|
|
293
|
+
|| !HASH.test(response.contract_bundle_hash)
|
|
294
|
+
|| (contractBundleHash !== "" && response.contract_bundle_hash !== contractBundleHash)
|
|
295
|
+
|| typeof response.database !== "string"
|
|
296
|
+
|| !SYMBOL.test(response.database)
|
|
297
|
+
|| (database !== "" && response.database !== database)
|
|
298
|
+
|| typeof response.role !== "string"
|
|
299
|
+
|| !SYMBOL.test(response.role)
|
|
300
|
+
|| (role !== "" && response.role !== role)
|
|
301
|
+
|| typeof response.role_definition_hash !== "string"
|
|
302
|
+
|| !HASH.test(response.role_definition_hash)
|
|
303
|
+
|| (roleDefinitionHash !== "" && response.role_definition_hash !== roleDefinitionHash)
|
|
304
|
+
|| typeof response.remote_identity_hash !== "string"
|
|
305
|
+
|| !HASH.test(response.remote_identity_hash)
|
|
306
|
+
|| (remoteIdentityHash !== "" && response.remote_identity_hash !== remoteIdentityHash)) {
|
|
187
307
|
throw new Error("RiffDB driver identity mismatch");
|
|
188
308
|
}
|
|
309
|
+
this.#resolvedIdentity = {
|
|
310
|
+
applicationManifestHash: response.application_manifest_hash,
|
|
311
|
+
operationCatalogHash: response.operation_catalog_hash,
|
|
312
|
+
contractLineage: response.contract_lineage,
|
|
313
|
+
contractVersion: resolvedContractVersion,
|
|
314
|
+
contractBundleHash: response.contract_bundle_hash,
|
|
315
|
+
database: response.database,
|
|
316
|
+
role: response.role,
|
|
317
|
+
roleDefinitionHash: response.role_definition_hash,
|
|
318
|
+
remoteIdentityHash: response.remote_identity_hash,
|
|
319
|
+
...(this.#tenant === undefined ? {} : { tenant: this.#tenant }),
|
|
320
|
+
};
|
|
321
|
+
this.#identity = this.#resolvedIdentity;
|
|
189
322
|
}
|
|
190
323
|
async #request(request, requestId, signal) {
|
|
191
324
|
if (this.#closed || this.#socket.destroyed)
|
|
@@ -230,31 +363,40 @@ export class DriverApplicationTransport {
|
|
|
230
363
|
}
|
|
231
364
|
}
|
|
232
365
|
#receive(chunk) {
|
|
233
|
-
if (this.#closed)
|
|
366
|
+
if (this.#closed || chunk.length === 0)
|
|
234
367
|
return;
|
|
235
|
-
this.#
|
|
236
|
-
|
|
237
|
-
|
|
368
|
+
this.#inputChunks.push(chunk);
|
|
369
|
+
this.#inputLength += chunk.length;
|
|
370
|
+
while (this.#inputLength >= 4) {
|
|
371
|
+
const length = this.#peekFrameLength();
|
|
238
372
|
if (length < 2 || length > MAX_FRAME_BYTES) {
|
|
373
|
+
this.#closed = true;
|
|
374
|
+
this.#inputChunks.length = 0;
|
|
375
|
+
this.#inputLength = 0;
|
|
239
376
|
this.#fail(new Error("RiffDB driver returned an invalid frame"));
|
|
240
377
|
this.#socket.destroy();
|
|
241
378
|
return;
|
|
242
379
|
}
|
|
243
|
-
if (this.#
|
|
380
|
+
if (this.#inputLength < length + 4)
|
|
244
381
|
return;
|
|
245
|
-
const body = this.#
|
|
246
|
-
this.#input = this.#input.subarray(length + 4);
|
|
382
|
+
const body = this.#consumeFrame(length);
|
|
247
383
|
let response;
|
|
248
384
|
try {
|
|
249
385
|
response = exactObject(decodeJson(UTF8_DECODER.decode(body)));
|
|
250
386
|
}
|
|
251
387
|
catch {
|
|
388
|
+
this.#closed = true;
|
|
389
|
+
this.#inputChunks.length = 0;
|
|
390
|
+
this.#inputLength = 0;
|
|
252
391
|
this.#fail(new Error("RiffDB driver returned an invalid message"));
|
|
253
392
|
this.#socket.destroy();
|
|
254
393
|
return;
|
|
255
394
|
}
|
|
256
395
|
const requestId = response.request_id;
|
|
257
396
|
if (typeof requestId !== "string" || !REQUEST_ID.test(requestId)) {
|
|
397
|
+
this.#closed = true;
|
|
398
|
+
this.#inputChunks.length = 0;
|
|
399
|
+
this.#inputLength = 0;
|
|
258
400
|
this.#fail(new Error("RiffDB driver returned an invalid request identity"));
|
|
259
401
|
this.#socket.destroy();
|
|
260
402
|
return;
|
|
@@ -266,6 +408,80 @@ export class DriverApplicationTransport {
|
|
|
266
408
|
pending.resolve(response);
|
|
267
409
|
}
|
|
268
410
|
}
|
|
411
|
+
#peekFrameLength() {
|
|
412
|
+
const first = this.#inputChunks[0];
|
|
413
|
+
if (first.length >= 4) {
|
|
414
|
+
return first.readUInt32BE(0);
|
|
415
|
+
}
|
|
416
|
+
let b0 = 0;
|
|
417
|
+
let b1 = 0;
|
|
418
|
+
let b2 = 0;
|
|
419
|
+
let b3 = 0;
|
|
420
|
+
let idx = 0;
|
|
421
|
+
outer: for (const chunk of this.#inputChunks) {
|
|
422
|
+
for (let i = 0; i < chunk.length; i += 1) {
|
|
423
|
+
const byte = chunk[i];
|
|
424
|
+
if (idx === 0)
|
|
425
|
+
b0 = byte;
|
|
426
|
+
else if (idx === 1)
|
|
427
|
+
b1 = byte;
|
|
428
|
+
else if (idx === 2)
|
|
429
|
+
b2 = byte;
|
|
430
|
+
else {
|
|
431
|
+
b3 = byte;
|
|
432
|
+
break outer;
|
|
433
|
+
}
|
|
434
|
+
idx += 1;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3) >>> 0;
|
|
438
|
+
}
|
|
439
|
+
#consumeFrame(bodyLength) {
|
|
440
|
+
this.#skipBytes(4);
|
|
441
|
+
const first = this.#inputChunks[0];
|
|
442
|
+
if (first.length >= bodyLength) {
|
|
443
|
+
const body = first.subarray(0, bodyLength);
|
|
444
|
+
if (first.length === bodyLength) {
|
|
445
|
+
this.#inputChunks.shift();
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
this.#inputChunks[0] = first.subarray(bodyLength);
|
|
449
|
+
}
|
|
450
|
+
this.#inputLength -= bodyLength;
|
|
451
|
+
return body;
|
|
452
|
+
}
|
|
453
|
+
const body = Buffer.allocUnsafe(bodyLength);
|
|
454
|
+
let offset = 0;
|
|
455
|
+
while (offset < bodyLength) {
|
|
456
|
+
const chunk = this.#inputChunks[0];
|
|
457
|
+
const take = Math.min(chunk.length, bodyLength - offset);
|
|
458
|
+
chunk.copy(body, offset, 0, take);
|
|
459
|
+
offset += take;
|
|
460
|
+
if (take === chunk.length) {
|
|
461
|
+
this.#inputChunks.shift();
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
this.#inputChunks[0] = chunk.subarray(take);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
this.#inputLength -= bodyLength;
|
|
468
|
+
return body;
|
|
469
|
+
}
|
|
470
|
+
#skipBytes(count) {
|
|
471
|
+
let remaining = count;
|
|
472
|
+
while (remaining > 0) {
|
|
473
|
+
const chunk = this.#inputChunks[0];
|
|
474
|
+
if (chunk.length <= remaining) {
|
|
475
|
+
remaining -= chunk.length;
|
|
476
|
+
this.#inputChunks.shift();
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
this.#inputChunks[0] = chunk.subarray(remaining);
|
|
480
|
+
remaining = 0;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
this.#inputLength -= count;
|
|
484
|
+
}
|
|
269
485
|
#fail(error) {
|
|
270
486
|
for (const pending of this.#pending.values())
|
|
271
487
|
pending.reject(error);
|
|
@@ -675,20 +891,30 @@ function decodeResult(response) {
|
|
|
675
891
|
...(cursor === undefined ? {} : { cursor }),
|
|
676
892
|
};
|
|
677
893
|
}
|
|
678
|
-
function decodeBatchResult(response) {
|
|
894
|
+
function decodeBatchResult(response, expectedTotal, requestedCheckpoint) {
|
|
679
895
|
if (response.type !== "batch_result" || !Array.isArray(response.items)
|
|
680
896
|
|| response.items.length > MAX_COLLECTION_ITEMS) {
|
|
681
897
|
throw new Error("RiffDB driver returned an invalid batch result");
|
|
682
898
|
}
|
|
683
899
|
const checkpoint = boundedInteger(response.checkpoint, 0, MAX_COLLECTION_ITEMS);
|
|
684
900
|
const total = boundedInteger(response.total, 1, MAX_COLLECTION_ITEMS);
|
|
685
|
-
if (checkpoint > total
|
|
901
|
+
if (total !== expectedTotal || checkpoint < requestedCheckpoint || checkpoint > total
|
|
902
|
+
|| response.items.length !== total - requestedCheckpoint) {
|
|
686
903
|
throw new Error("RiffDB driver returned an invalid batch result");
|
|
904
|
+
}
|
|
905
|
+
const seenIndices = new Set();
|
|
687
906
|
const items = response.items.map((raw) => {
|
|
688
907
|
const item = exactObject(raw);
|
|
689
|
-
const index = boundedInteger(item.index,
|
|
908
|
+
const index = boundedInteger(item.index, requestedCheckpoint, total - 1);
|
|
909
|
+
if (seenIndices.has(index)) {
|
|
910
|
+
throw new Error("RiffDB driver returned an invalid batch item");
|
|
911
|
+
}
|
|
912
|
+
seenIndices.add(index);
|
|
690
913
|
const outcome = exactObject(item.outcome);
|
|
691
914
|
if (outcome.type === "result") {
|
|
915
|
+
if (index === checkpoint) {
|
|
916
|
+
throw new Error("RiffDB driver returned an invalid batch result");
|
|
917
|
+
}
|
|
692
918
|
const value = validateDriverValue(outcome.value, 0);
|
|
693
919
|
const commitSequence = optionalPositiveBigInt(outcome.commit_sequence);
|
|
694
920
|
const outcomeUri = optionalBoundedString(outcome.outcome_uri, 16_384);
|
|
@@ -704,8 +930,12 @@ function decodeBatchResult(response) {
|
|
|
704
930
|
},
|
|
705
931
|
};
|
|
706
932
|
}
|
|
707
|
-
if (outcome.type === "error")
|
|
933
|
+
if (outcome.type === "error") {
|
|
934
|
+
if (index < checkpoint) {
|
|
935
|
+
throw new Error("RiffDB driver returned an invalid batch result");
|
|
936
|
+
}
|
|
708
937
|
return { index, error: decodeError(outcome) };
|
|
938
|
+
}
|
|
709
939
|
throw new Error("RiffDB driver returned an invalid batch item");
|
|
710
940
|
});
|
|
711
941
|
return { items, checkpoint, total };
|
|
@@ -740,9 +970,11 @@ function decodeError(response) {
|
|
|
740
970
|
...(incidentId === undefined ? {} : { incidentId }),
|
|
741
971
|
});
|
|
742
972
|
}
|
|
973
|
+
const ENV_RESOLVED = Symbol("riffdb.envResolved");
|
|
743
974
|
function resolveTransportTenant(options) {
|
|
744
|
-
const
|
|
745
|
-
const
|
|
975
|
+
const allowEnvFallback = !(ENV_RESOLVED in options);
|
|
976
|
+
const envTenant = allowEnvFallback ? process.env.RIFFDB_TENANT : undefined;
|
|
977
|
+
const candidate = options.tenant ?? options.identity?.tenant ?? (envTenant && envTenant.length > 0 ? envTenant : undefined);
|
|
746
978
|
if (candidate === undefined)
|
|
747
979
|
return undefined;
|
|
748
980
|
if (typeof candidate !== "string" || candidate.length < 1 || candidate.length > 128 || !/^[\x21-\x7e]+$/.test(candidate)) {
|
|
@@ -750,21 +982,103 @@ function resolveTransportTenant(options) {
|
|
|
750
982
|
}
|
|
751
983
|
return candidate;
|
|
752
984
|
}
|
|
985
|
+
export function resolveTransportOptionsFromEnv(options = {}) {
|
|
986
|
+
if (options.prefix !== undefined &&
|
|
987
|
+
options.envPrefix !== undefined &&
|
|
988
|
+
options.prefix !== options.envPrefix) {
|
|
989
|
+
throw new Error("conflicting RiffDB environment prefixes");
|
|
990
|
+
}
|
|
991
|
+
const rawPrefix = options.prefix ?? options.envPrefix;
|
|
992
|
+
if (rawPrefix !== undefined &&
|
|
993
|
+
(typeof rawPrefix !== "string" ||
|
|
994
|
+
rawPrefix.trim().length === 0 ||
|
|
995
|
+
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(rawPrefix))) {
|
|
996
|
+
throw new Error("invalid RiffDB environment prefix");
|
|
997
|
+
}
|
|
998
|
+
const normalizedPrefix = rawPrefix === undefined
|
|
999
|
+
? "RIFFDB_"
|
|
1000
|
+
: rawPrefix.endsWith("_")
|
|
1001
|
+
? rawPrefix
|
|
1002
|
+
: `${rawPrefix}_`;
|
|
1003
|
+
const candidatePrefixes = normalizedPrefix === "RIFFDB_" || normalizedPrefix.endsWith("RIFFDB_")
|
|
1004
|
+
? [normalizedPrefix]
|
|
1005
|
+
: [`${normalizedPrefix}RIFFDB_`, normalizedPrefix];
|
|
1006
|
+
const envSource = options.env ?? process.env;
|
|
1007
|
+
const readEnv = (suffix) => {
|
|
1008
|
+
for (const candidatePrefix of candidatePrefixes) {
|
|
1009
|
+
const raw = envSource[`${candidatePrefix}${suffix}`];
|
|
1010
|
+
if (raw !== undefined && raw.trim().length > 0) {
|
|
1011
|
+
return raw.trim();
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return undefined;
|
|
1015
|
+
};
|
|
1016
|
+
const socketPath = options.socketPath ?? readEnv("DRIVER_SOCKET") ?? "";
|
|
1017
|
+
const base = options.identity ?? {};
|
|
1018
|
+
const contractLineage = base.contractLineage ?? readEnv("LINEAGE") ?? "";
|
|
1019
|
+
const rawVersion = base.contractVersion ?? readEnv("CONTRACT_VERSION");
|
|
1020
|
+
let contractVersion = 0n;
|
|
1021
|
+
if (typeof rawVersion === "bigint") {
|
|
1022
|
+
contractVersion = rawVersion;
|
|
1023
|
+
}
|
|
1024
|
+
else if (typeof rawVersion === "string") {
|
|
1025
|
+
if (!/^[1-9][0-9]*$/.test(rawVersion)) {
|
|
1026
|
+
throw new Error("invalid RiffDB driver identity");
|
|
1027
|
+
}
|
|
1028
|
+
contractVersion = BigInt(rawVersion);
|
|
1029
|
+
}
|
|
1030
|
+
const contractBundleHash = base.contractBundleHash ?? readEnv("BUNDLE_HASH") ?? "";
|
|
1031
|
+
const role = options.role ?? base.role ?? readEnv("ROLE") ?? "";
|
|
1032
|
+
const database = options.database ?? base.database ?? readEnv("DATABASE");
|
|
1033
|
+
const applicationManifestHash = base.applicationManifestHash ?? readEnv("MANIFEST_HASH");
|
|
1034
|
+
const operationCatalogHash = base.operationCatalogHash ?? readEnv("CATALOG_HASH");
|
|
1035
|
+
const roleDefinitionHash = base.roleDefinitionHash ?? readEnv("ROLE_HASH");
|
|
1036
|
+
const remoteIdentityHash = base.remoteIdentityHash ?? readEnv("REMOTE_HASH");
|
|
1037
|
+
const tenant = options.tenant ?? base.tenant ?? readEnv("TENANT");
|
|
1038
|
+
const identity = {
|
|
1039
|
+
contractLineage,
|
|
1040
|
+
contractVersion,
|
|
1041
|
+
contractBundleHash,
|
|
1042
|
+
role,
|
|
1043
|
+
...(database !== undefined && database.length > 0 ? { database } : {}),
|
|
1044
|
+
...(applicationManifestHash !== undefined && applicationManifestHash.length > 0 ? { applicationManifestHash } : {}),
|
|
1045
|
+
...(operationCatalogHash !== undefined && operationCatalogHash.length > 0 ? { operationCatalogHash } : {}),
|
|
1046
|
+
...(roleDefinitionHash !== undefined && roleDefinitionHash.length > 0 ? { roleDefinitionHash } : {}),
|
|
1047
|
+
...(remoteIdentityHash !== undefined && remoteIdentityHash.length > 0 ? { remoteIdentityHash } : {}),
|
|
1048
|
+
...(tenant !== undefined && tenant.length > 0 ? { tenant } : {}),
|
|
1049
|
+
};
|
|
1050
|
+
const resolved = {
|
|
1051
|
+
socketPath,
|
|
1052
|
+
identity,
|
|
1053
|
+
...(tenant !== undefined && tenant.length > 0 ? { tenant } : {}),
|
|
1054
|
+
};
|
|
1055
|
+
Object.defineProperty(resolved, ENV_RESOLVED, { value: true, enumerable: false });
|
|
1056
|
+
return resolved;
|
|
1057
|
+
}
|
|
753
1058
|
function validateTransportOptions(options) {
|
|
754
1059
|
if (!options.socketPath.startsWith("/") || options.socketPath.length > 4_096) {
|
|
755
1060
|
throw new Error("invalid RiffDB driver socket path");
|
|
756
1061
|
}
|
|
757
1062
|
const identity = options.identity;
|
|
1063
|
+
if (identity === undefined)
|
|
1064
|
+
return;
|
|
758
1065
|
for (const value of [
|
|
1066
|
+
identity.contractBundleHash,
|
|
759
1067
|
identity.applicationManifestHash, identity.operationCatalogHash,
|
|
760
|
-
identity.
|
|
1068
|
+
identity.roleDefinitionHash, identity.remoteIdentityHash,
|
|
761
1069
|
]) {
|
|
762
|
-
if (!HASH.test(value))
|
|
1070
|
+
if (value !== undefined && value !== "" && !HASH.test(value)) {
|
|
763
1071
|
throw new Error("invalid RiffDB driver identity");
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
for (const value of [identity.database, identity.contractLineage, identity.role]) {
|
|
1075
|
+
if (value !== undefined && value !== "" && !SYMBOL.test(value)) {
|
|
1076
|
+
throw new Error("invalid RiffDB driver identity");
|
|
1077
|
+
}
|
|
764
1078
|
}
|
|
765
|
-
if (
|
|
766
|
-
|
|
767
|
-
|| identity.contractVersion > 18446744073709551615n) {
|
|
1079
|
+
if (identity.contractVersion !== undefined
|
|
1080
|
+
&& identity.contractVersion !== 0n
|
|
1081
|
+
&& (identity.contractVersion < 1n || identity.contractVersion > 18446744073709551615n)) {
|
|
768
1082
|
throw new Error("invalid RiffDB driver identity");
|
|
769
1083
|
}
|
|
770
1084
|
}
|