@riffdb/client 0.2.0 → 0.2.1

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 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.1.0
11
- npm install @riffdb/client@0.1.0
10
+ npm install --save-dev @riffdb/cli@0.2.1
11
+ npm install @riffdb/client@0.2.1
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.1.x pre-1.0 package. It carries the exact durable-format statement
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;
@@ -75,6 +87,15 @@ export interface DriverApplicationTransportOptions {
75
87
  readonly identity: DriverApplicationIdentity;
76
88
  readonly tenant?: string;
77
89
  }
90
+ export interface DriverConnectFromEnvOptions {
91
+ readonly socketPath?: string;
92
+ readonly identity?: Partial<DriverApplicationIdentity>;
93
+ readonly role?: string;
94
+ readonly database?: string;
95
+ readonly tenant?: string;
96
+ readonly prefix?: string;
97
+ readonly envPrefix?: string;
98
+ }
78
99
  export interface DriverOperation {
79
100
  readonly name: string;
80
101
  readonly inputSchemaHash: string;
@@ -163,9 +184,13 @@ export declare class DriverApplicationTransport {
163
184
  private constructor();
164
185
  /** Returns the bound tenant identifier when this session is tenant-scoped. */
165
186
  get tenant(): string | undefined;
187
+ /** Returns the resolved application identity verified during driver handshake. */
188
+ get identity(): ResolvedDriverApplicationIdentity;
166
189
  static connect(options: DriverApplicationTransportOptions): Promise<DriverApplicationTransport>;
190
+ static connectFromEnv(options?: DriverConnectFromEnvOptions): Promise<DriverApplicationTransport>;
167
191
  invoke(operation: DriverOperation, input: Readonly<Record<string, DriverValue>>, options?: DriverInvokeOptions): Promise<DriverResult>;
168
192
  batch(operation: DriverOperation, items: ReadonlyArray<Readonly<Record<string, DriverValue>>>, concurrency: number, checkpoint: number, options?: DriverInvokeOptions): Promise<DriverBatchResult>;
169
193
  /** Stops new work, sends a clean local EOF, and rejects unfinished waits. */
170
194
  shutdown(): Promise<void>;
171
195
  }
196
+ export declare function resolveTransportOptionsFromEnv(options?: DriverConnectFromEnvOptions): DriverApplicationTransportOptions;
package/dist/driver.js CHANGED
@@ -63,10 +63,12 @@ export class DriverApplicationError extends Error {
63
63
  export class DriverApplicationTransport {
64
64
  #socket;
65
65
  #identity;
66
+ #resolvedIdentity;
66
67
  #tenant;
67
68
  #requestPrefix;
68
69
  #pending = new Map();
69
- #input = Buffer.alloc(0);
70
+ #inputChunks = [];
71
+ #inputLength = 0;
70
72
  #nextRequest = 1;
71
73
  #closed = false;
72
74
  constructor(socket, identity, tenant) {
@@ -83,6 +85,13 @@ export class DriverApplicationTransport {
83
85
  get tenant() {
84
86
  return this.#tenant;
85
87
  }
88
+ /** Returns the resolved application identity verified during driver handshake. */
89
+ get identity() {
90
+ if (this.#resolvedIdentity === undefined) {
91
+ throw new Error("RiffDB driver session has not completed handshake");
92
+ }
93
+ return this.#resolvedIdentity;
94
+ }
86
95
  static async connect(options) {
87
96
  const tenant = resolveTransportTenant(options);
88
97
  validateTransportOptions(options);
@@ -102,6 +111,9 @@ export class DriverApplicationTransport {
102
111
  throw error;
103
112
  }
104
113
  }
114
+ static async connectFromEnv(options = {}) {
115
+ return DriverApplicationTransport.connect(resolveTransportOptionsFromEnv(options));
116
+ }
105
117
  async invoke(operation, input, options = {}) {
106
118
  validateOperation(operation);
107
119
  validateDriverRecord(input);
@@ -138,7 +150,7 @@ export class DriverApplicationTransport {
138
150
  checkpoint,
139
151
  options: lowerOptions(options),
140
152
  }, requestId, options.signal);
141
- return decodeBatchResult(response);
153
+ return decodeBatchResult(response, items.length, checkpoint);
142
154
  }
143
155
  /** Stops new work, sends a clean local EOF, and rejects unfinished waits. */
144
156
  async shutdown() {
@@ -157,35 +169,63 @@ export class DriverApplicationTransport {
157
169
  async #handshake() {
158
170
  const requestId = this.#requestId("handshake");
159
171
  const identity = this.#identity;
172
+ const manifestHash = identity.applicationManifestHash ?? "";
173
+ const catalogHash = identity.operationCatalogHash ?? "";
174
+ const database = identity.database ?? "";
175
+ const roleDefinitionHash = identity.roleDefinitionHash ?? "";
176
+ const remoteIdentityHash = identity.remoteIdentityHash ?? "";
160
177
  const response = await this.#request({
161
178
  type: "handshake",
162
179
  request_id: requestId,
163
180
  protocol_version: DRIVER_PROTOCOL_VERSION,
164
- application_manifest_hash: identity.applicationManifestHash,
165
- operation_catalog_hash: identity.operationCatalogHash,
181
+ application_manifest_hash: manifestHash,
182
+ operation_catalog_hash: catalogHash,
166
183
  contract_lineage: identity.contractLineage,
167
184
  contract_version: identity.contractVersion,
168
185
  contract_bundle_hash: identity.contractBundleHash,
169
186
  value_registry_hash: DRIVER_VALUE_REGISTRY_HASH,
170
187
  error_registry_hash: DRIVER_ERROR_REGISTRY_HASH,
171
- database: identity.database,
188
+ database,
172
189
  role: identity.role,
173
- role_definition_hash: identity.roleDefinitionHash,
174
- remote_identity_hash: identity.remoteIdentityHash,
190
+ role_definition_hash: roleDefinitionHash,
191
+ remote_identity_hash: remoteIdentityHash,
175
192
  }, requestId);
176
193
  if (response.type !== "handshake"
177
194
  || response.protocol_version !== DRIVER_PROTOCOL_VERSION
178
- || response.application_manifest_hash !== identity.applicationManifestHash
179
- || response.operation_catalog_hash !== identity.operationCatalogHash
195
+ || typeof response.application_manifest_hash !== "string"
196
+ || !HASH.test(response.application_manifest_hash)
197
+ || (manifestHash !== "" && response.application_manifest_hash !== manifestHash)
198
+ || typeof response.operation_catalog_hash !== "string"
199
+ || !HASH.test(response.operation_catalog_hash)
200
+ || (catalogHash !== "" && response.operation_catalog_hash !== catalogHash)
180
201
  || response.contract_lineage !== identity.contractLineage
181
202
  || requiredPositiveBigInt(response.contract_version) !== identity.contractVersion
182
203
  || response.contract_bundle_hash !== identity.contractBundleHash
183
- || response.database !== identity.database
204
+ || typeof response.database !== "string"
205
+ || !SYMBOL.test(response.database)
206
+ || (database !== "" && response.database !== database)
184
207
  || response.role !== identity.role
185
- || response.role_definition_hash !== identity.roleDefinitionHash
186
- || response.remote_identity_hash !== identity.remoteIdentityHash) {
208
+ || typeof response.role_definition_hash !== "string"
209
+ || !HASH.test(response.role_definition_hash)
210
+ || (roleDefinitionHash !== "" && response.role_definition_hash !== roleDefinitionHash)
211
+ || typeof response.remote_identity_hash !== "string"
212
+ || !HASH.test(response.remote_identity_hash)
213
+ || (remoteIdentityHash !== "" && response.remote_identity_hash !== remoteIdentityHash)) {
187
214
  throw new Error("RiffDB driver identity mismatch");
188
215
  }
216
+ this.#resolvedIdentity = {
217
+ applicationManifestHash: response.application_manifest_hash,
218
+ operationCatalogHash: response.operation_catalog_hash,
219
+ contractLineage: identity.contractLineage,
220
+ contractVersion: identity.contractVersion,
221
+ contractBundleHash: identity.contractBundleHash,
222
+ database: response.database,
223
+ role: identity.role,
224
+ roleDefinitionHash: response.role_definition_hash,
225
+ remoteIdentityHash: response.remote_identity_hash,
226
+ ...(this.#tenant === undefined ? {} : { tenant: this.#tenant }),
227
+ };
228
+ this.#identity = this.#resolvedIdentity;
189
229
  }
190
230
  async #request(request, requestId, signal) {
191
231
  if (this.#closed || this.#socket.destroyed)
@@ -230,31 +270,37 @@ export class DriverApplicationTransport {
230
270
  }
231
271
  }
232
272
  #receive(chunk) {
233
- if (this.#closed)
273
+ if (this.#closed || chunk.length === 0)
234
274
  return;
235
- this.#input = Buffer.concat([this.#input, chunk]);
236
- while (this.#input.length >= 4) {
237
- const length = this.#input.readUInt32BE(0);
275
+ this.#inputChunks.push(chunk);
276
+ this.#inputLength += chunk.length;
277
+ while (this.#inputLength >= 4) {
278
+ const length = this.#peekFrameLength();
238
279
  if (length < 2 || length > MAX_FRAME_BYTES) {
280
+ this.#inputChunks.length = 0;
281
+ this.#inputLength = 0;
239
282
  this.#fail(new Error("RiffDB driver returned an invalid frame"));
240
283
  this.#socket.destroy();
241
284
  return;
242
285
  }
243
- if (this.#input.length < length + 4)
286
+ if (this.#inputLength < length + 4)
244
287
  return;
245
- const body = this.#input.subarray(4, length + 4);
246
- this.#input = this.#input.subarray(length + 4);
288
+ const body = this.#consumeFrame(length);
247
289
  let response;
248
290
  try {
249
291
  response = exactObject(decodeJson(UTF8_DECODER.decode(body)));
250
292
  }
251
293
  catch {
294
+ this.#inputChunks.length = 0;
295
+ this.#inputLength = 0;
252
296
  this.#fail(new Error("RiffDB driver returned an invalid message"));
253
297
  this.#socket.destroy();
254
298
  return;
255
299
  }
256
300
  const requestId = response.request_id;
257
301
  if (typeof requestId !== "string" || !REQUEST_ID.test(requestId)) {
302
+ this.#inputChunks.length = 0;
303
+ this.#inputLength = 0;
258
304
  this.#fail(new Error("RiffDB driver returned an invalid request identity"));
259
305
  this.#socket.destroy();
260
306
  return;
@@ -266,6 +312,80 @@ export class DriverApplicationTransport {
266
312
  pending.resolve(response);
267
313
  }
268
314
  }
315
+ #peekFrameLength() {
316
+ const first = this.#inputChunks[0];
317
+ if (first.length >= 4) {
318
+ return first.readUInt32BE(0);
319
+ }
320
+ let b0 = 0;
321
+ let b1 = 0;
322
+ let b2 = 0;
323
+ let b3 = 0;
324
+ let idx = 0;
325
+ outer: for (const chunk of this.#inputChunks) {
326
+ for (let i = 0; i < chunk.length; i += 1) {
327
+ const byte = chunk[i];
328
+ if (idx === 0)
329
+ b0 = byte;
330
+ else if (idx === 1)
331
+ b1 = byte;
332
+ else if (idx === 2)
333
+ b2 = byte;
334
+ else {
335
+ b3 = byte;
336
+ break outer;
337
+ }
338
+ idx += 1;
339
+ }
340
+ }
341
+ return ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3) >>> 0;
342
+ }
343
+ #consumeFrame(bodyLength) {
344
+ this.#skipBytes(4);
345
+ const first = this.#inputChunks[0];
346
+ if (first.length >= bodyLength) {
347
+ const body = first.subarray(0, bodyLength);
348
+ if (first.length === bodyLength) {
349
+ this.#inputChunks.shift();
350
+ }
351
+ else {
352
+ this.#inputChunks[0] = first.subarray(bodyLength);
353
+ }
354
+ this.#inputLength -= bodyLength;
355
+ return body;
356
+ }
357
+ const body = Buffer.allocUnsafe(bodyLength);
358
+ let offset = 0;
359
+ while (offset < bodyLength) {
360
+ const chunk = this.#inputChunks[0];
361
+ const take = Math.min(chunk.length, bodyLength - offset);
362
+ chunk.copy(body, offset, 0, take);
363
+ offset += take;
364
+ if (take === chunk.length) {
365
+ this.#inputChunks.shift();
366
+ }
367
+ else {
368
+ this.#inputChunks[0] = chunk.subarray(take);
369
+ }
370
+ }
371
+ this.#inputLength -= bodyLength;
372
+ return body;
373
+ }
374
+ #skipBytes(count) {
375
+ let remaining = count;
376
+ while (remaining > 0) {
377
+ const chunk = this.#inputChunks[0];
378
+ if (chunk.length <= remaining) {
379
+ remaining -= chunk.length;
380
+ this.#inputChunks.shift();
381
+ }
382
+ else {
383
+ this.#inputChunks[0] = chunk.subarray(remaining);
384
+ remaining = 0;
385
+ }
386
+ }
387
+ this.#inputLength -= count;
388
+ }
269
389
  #fail(error) {
270
390
  for (const pending of this.#pending.values())
271
391
  pending.reject(error);
@@ -675,20 +795,30 @@ function decodeResult(response) {
675
795
  ...(cursor === undefined ? {} : { cursor }),
676
796
  };
677
797
  }
678
- function decodeBatchResult(response) {
798
+ function decodeBatchResult(response, expectedTotal, requestedCheckpoint) {
679
799
  if (response.type !== "batch_result" || !Array.isArray(response.items)
680
800
  || response.items.length > MAX_COLLECTION_ITEMS) {
681
801
  throw new Error("RiffDB driver returned an invalid batch result");
682
802
  }
683
803
  const checkpoint = boundedInteger(response.checkpoint, 0, MAX_COLLECTION_ITEMS);
684
804
  const total = boundedInteger(response.total, 1, MAX_COLLECTION_ITEMS);
685
- if (checkpoint > total)
805
+ if (total !== expectedTotal || checkpoint < requestedCheckpoint || checkpoint > total
806
+ || response.items.length !== total - requestedCheckpoint) {
686
807
  throw new Error("RiffDB driver returned an invalid batch result");
808
+ }
809
+ const seenIndices = new Set();
687
810
  const items = response.items.map((raw) => {
688
811
  const item = exactObject(raw);
689
- const index = boundedInteger(item.index, 0, total - 1);
812
+ const index = boundedInteger(item.index, requestedCheckpoint, total - 1);
813
+ if (seenIndices.has(index)) {
814
+ throw new Error("RiffDB driver returned an invalid batch item");
815
+ }
816
+ seenIndices.add(index);
690
817
  const outcome = exactObject(item.outcome);
691
818
  if (outcome.type === "result") {
819
+ if (index === checkpoint) {
820
+ throw new Error("RiffDB driver returned an invalid batch result");
821
+ }
692
822
  const value = validateDriverValue(outcome.value, 0);
693
823
  const commitSequence = optionalPositiveBigInt(outcome.commit_sequence);
694
824
  const outcomeUri = optionalBoundedString(outcome.outcome_uri, 16_384);
@@ -704,8 +834,12 @@ function decodeBatchResult(response) {
704
834
  },
705
835
  };
706
836
  }
707
- if (outcome.type === "error")
837
+ if (outcome.type === "error") {
838
+ if (index < checkpoint) {
839
+ throw new Error("RiffDB driver returned an invalid batch result");
840
+ }
708
841
  return { index, error: decodeError(outcome) };
842
+ }
709
843
  throw new Error("RiffDB driver returned an invalid batch item");
710
844
  });
711
845
  return { items, checkpoint, total };
@@ -740,8 +874,10 @@ function decodeError(response) {
740
874
  ...(incidentId === undefined ? {} : { incidentId }),
741
875
  });
742
876
  }
877
+ const ENV_RESOLVED = Symbol("riffdb.envResolved");
743
878
  function resolveTransportTenant(options) {
744
- const envTenant = process.env.RIFFDB_TENANT;
879
+ const allowEnvFallback = !(ENV_RESOLVED in options);
880
+ const envTenant = allowEnvFallback ? process.env.RIFFDB_TENANT : undefined;
745
881
  const candidate = options.tenant ?? options.identity.tenant ?? (envTenant && envTenant.length > 0 ? envTenant : undefined);
746
882
  if (candidate === undefined)
747
883
  return undefined;
@@ -750,19 +886,95 @@ function resolveTransportTenant(options) {
750
886
  }
751
887
  return candidate;
752
888
  }
889
+ export function resolveTransportOptionsFromEnv(options = {}) {
890
+ if (options.prefix !== undefined &&
891
+ options.envPrefix !== undefined &&
892
+ options.prefix !== options.envPrefix) {
893
+ throw new Error("conflicting RiffDB environment prefixes");
894
+ }
895
+ const rawPrefix = options.prefix ?? options.envPrefix;
896
+ if (rawPrefix !== undefined &&
897
+ (typeof rawPrefix !== "string" ||
898
+ rawPrefix.trim().length === 0 ||
899
+ !/^[A-Za-z_][A-Za-z0-9_]*$/.test(rawPrefix))) {
900
+ throw new Error("invalid RiffDB environment prefix");
901
+ }
902
+ const normalizedPrefix = rawPrefix === undefined
903
+ ? "RIFFDB_"
904
+ : rawPrefix.endsWith("_")
905
+ ? rawPrefix
906
+ : `${rawPrefix}_`;
907
+ const candidatePrefixes = normalizedPrefix === "RIFFDB_" || normalizedPrefix.endsWith("RIFFDB_")
908
+ ? [normalizedPrefix]
909
+ : [`${normalizedPrefix}RIFFDB_`, normalizedPrefix];
910
+ const readEnv = (suffix) => {
911
+ for (const candidatePrefix of candidatePrefixes) {
912
+ const raw = process.env[`${candidatePrefix}${suffix}`];
913
+ if (raw !== undefined && raw.trim().length > 0) {
914
+ return raw.trim();
915
+ }
916
+ }
917
+ return undefined;
918
+ };
919
+ const socketPath = options.socketPath ?? readEnv("DRIVER_SOCKET") ?? "";
920
+ const base = options.identity ?? {};
921
+ const contractLineage = base.contractLineage ?? readEnv("LINEAGE") ?? "";
922
+ const rawVersion = base.contractVersion ?? readEnv("CONTRACT_VERSION");
923
+ let contractVersion = 0n;
924
+ if (typeof rawVersion === "bigint") {
925
+ contractVersion = rawVersion;
926
+ }
927
+ else if (typeof rawVersion === "string" && /^[0-9]+$/.test(rawVersion)) {
928
+ contractVersion = BigInt(rawVersion);
929
+ }
930
+ const contractBundleHash = base.contractBundleHash ?? readEnv("BUNDLE_HASH") ?? "";
931
+ const role = options.role ?? base.role ?? readEnv("ROLE") ?? "";
932
+ const database = options.database ?? base.database ?? readEnv("DATABASE");
933
+ const applicationManifestHash = base.applicationManifestHash ?? readEnv("MANIFEST_HASH");
934
+ const operationCatalogHash = base.operationCatalogHash ?? readEnv("CATALOG_HASH");
935
+ const roleDefinitionHash = base.roleDefinitionHash ?? readEnv("ROLE_HASH");
936
+ const remoteIdentityHash = base.remoteIdentityHash ?? readEnv("REMOTE_HASH");
937
+ const tenant = options.tenant ?? base.tenant ?? readEnv("TENANT");
938
+ const identity = {
939
+ contractLineage,
940
+ contractVersion,
941
+ contractBundleHash,
942
+ role,
943
+ ...(database !== undefined && database.length > 0 ? { database } : {}),
944
+ ...(applicationManifestHash !== undefined && applicationManifestHash.length > 0 ? { applicationManifestHash } : {}),
945
+ ...(operationCatalogHash !== undefined && operationCatalogHash.length > 0 ? { operationCatalogHash } : {}),
946
+ ...(roleDefinitionHash !== undefined && roleDefinitionHash.length > 0 ? { roleDefinitionHash } : {}),
947
+ ...(remoteIdentityHash !== undefined && remoteIdentityHash.length > 0 ? { remoteIdentityHash } : {}),
948
+ ...(tenant !== undefined && tenant.length > 0 ? { tenant } : {}),
949
+ };
950
+ const resolved = {
951
+ socketPath,
952
+ identity,
953
+ ...(tenant !== undefined && tenant.length > 0 ? { tenant } : {}),
954
+ };
955
+ Object.defineProperty(resolved, ENV_RESOLVED, { value: true, enumerable: false });
956
+ return resolved;
957
+ }
753
958
  function validateTransportOptions(options) {
754
959
  if (!options.socketPath.startsWith("/") || options.socketPath.length > 4_096) {
755
960
  throw new Error("invalid RiffDB driver socket path");
756
961
  }
757
962
  const identity = options.identity;
963
+ if (!HASH.test(identity.contractBundleHash)) {
964
+ throw new Error("invalid RiffDB driver identity");
965
+ }
758
966
  for (const value of [
759
967
  identity.applicationManifestHash, identity.operationCatalogHash,
760
- identity.contractBundleHash, identity.roleDefinitionHash, identity.remoteIdentityHash,
968
+ identity.roleDefinitionHash, identity.remoteIdentityHash,
761
969
  ]) {
762
- if (!HASH.test(value))
970
+ if (value !== undefined && value !== "" && !HASH.test(value)) {
763
971
  throw new Error("invalid RiffDB driver identity");
972
+ }
973
+ }
974
+ if (identity.database !== undefined && identity.database !== "" && !SYMBOL.test(identity.database)) {
975
+ throw new Error("invalid RiffDB driver identity");
764
976
  }
765
- if (!SYMBOL.test(identity.contractLineage) || !SYMBOL.test(identity.database)
977
+ if (!SYMBOL.test(identity.contractLineage)
766
978
  || !SYMBOL.test(identity.role) || identity.contractVersion < 1n
767
979
  || identity.contractVersion > 18446744073709551615n) {
768
980
  throw new Error("invalid RiffDB driver identity");
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { DriverApplicationTransport, type DriverApplicationError, type DriverCompactQueryResult, type DriverPackedQueryResult, type DriverOperation } from "./driver.js";
1
+ import { DriverApplicationTransport, type DriverApplicationError, type DriverApplicationTransportOptions, type DriverConnectFromEnvOptions, type DriverCompactQueryResult, type DriverPackedQueryResult, type DriverOperation } from "./driver.js";
2
2
  export type ApplicationValueSchema = {
3
3
  readonly kind: "bool" | "i64" | "u64" | "string" | "uuid" | "bytes" | "date" | "timestamp" | "cursor" | "limit";
4
4
  readonly maximumBytes?: number;
@@ -51,12 +51,52 @@ export interface ExactDecimalValue {
51
51
  readonly coefficientTwosComplement: Uint8Array;
52
52
  readonly scale: number;
53
53
  readonly precision: number;
54
+ toString?(): string;
55
+ toJSON?(): string;
54
56
  }
55
57
  /** One exact currency value accepted by generated `money<CURRENCY>` fields. */
56
58
  export interface ExactMoneyValue<Currency extends string = string> {
57
59
  readonly currency: Currency;
58
60
  readonly amount: ExactDecimalValue;
61
+ toString?(): string;
62
+ toJSON?(): {
63
+ readonly currency: Currency;
64
+ readonly amount: string;
65
+ };
59
66
  }
67
+ /** One exact UTC timestamp returned by generated query and command fields. */
68
+ export interface TimestampValue {
69
+ readonly seconds: bigint;
70
+ readonly nanos: number;
71
+ toISOString(): string;
72
+ toDate(): Date;
73
+ toJSON(): string;
74
+ toString(): string;
75
+ }
76
+ export type TimestampInput = TimestampValue | {
77
+ readonly seconds: bigint | number | string;
78
+ readonly nanos?: number;
79
+ } | Date | string;
80
+ export type IntegerInput = bigint | number | string;
81
+ export declare class RiffTimestamp implements TimestampValue {
82
+ readonly seconds: bigint;
83
+ readonly nanos: number;
84
+ constructor(seconds: bigint, nanos?: number);
85
+ static from(value: TimestampInput): TimestampValue;
86
+ static now(): TimestampValue;
87
+ toISOString(): string;
88
+ toDate(): Date;
89
+ toJSON(): string;
90
+ toString(): string;
91
+ }
92
+ export declare function createTimestampValue(seconds: bigint, nanos: number): TimestampValue;
93
+ export declare function formatTimestampIso(seconds: bigint, nanos: number): string;
94
+ export declare function formatExactDecimal(value: {
95
+ readonly coefficientTwosComplement: Uint8Array;
96
+ readonly scale: number;
97
+ }): string;
98
+ export declare function toJsonValue(value: unknown, key?: string): unknown;
99
+ export declare function attachRecordToJson<T extends Record<string, unknown>>(record: T): T;
60
100
  /**
61
101
  * Constructs an exact fixed-point value from decimal text without using a
62
102
  * JavaScript floating-point number.
@@ -198,8 +238,10 @@ export interface ContextualBatch<E> {
198
238
  readonly status: ReactiveConsumerStatus;
199
239
  }
200
240
  export interface ReactiveConsumerStatus {
241
+ readonly kind?: "exact" | "protected";
201
242
  readonly revision: bigint;
202
243
  readonly checkpoint: string;
244
+ readonly progressCursor?: string;
203
245
  readonly historyIncarnation: bigint;
204
246
  readonly liveLeases: number;
205
247
  readonly retries: number;
@@ -294,8 +336,11 @@ export declare class CliApplicationTransport {
294
336
  export declare class DriverGeneratedApplicationTransport {
295
337
  private readonly driver;
296
338
  constructor(driver: DriverApplicationTransport);
339
+ static connect(options: DriverApplicationTransportOptions): Promise<DriverGeneratedApplicationTransport>;
340
+ static connectFromEnv(options?: DriverConnectFromEnvOptions): Promise<DriverGeneratedApplicationTransport>;
297
341
  /** Returns the bound tenant identifier when this session is tenant-scoped. */
298
342
  get tenant(): string | undefined;
343
+ shutdown(): Promise<void>;
299
344
  executeNamedQuery<P, R>(request: NamedQueryRequest<P, R>, options?: QueryOptions): Promise<TypedQueryResult<R>>;
300
345
  executeCommand<I, R>(request: CommandRequest<I, R>, attemptBudget: number): Promise<TypedCommandResult<R>>;
301
346
  executeVectorInspection<P, R>(request: VectorInspectionRequest<P, R>, options?: VectorInspectionOptions): Promise<TypedVectorInspectionResult<R>>;
@@ -328,5 +373,5 @@ export declare class DriverGeneratedApplicationTransport {
328
373
  }): AsyncIterable<LiveQueryUpdate<T>>;
329
374
  private mutateDriverLease;
330
375
  }
331
- export { DRIVER_ERROR_REGISTRY_HASH, DRIVER_PROTOCOL_VERSION, DRIVER_VALUE_REGISTRY_HASH, DriverApplicationError, DriverApplicationTransport, } from "./driver.js";
332
- export type { DriverApplicationIdentity, DriverApplicationTransportOptions, DriverBatchItem, DriverBatchResult, DriverBatchSuccess, DriverDecimal, DriverErrorDetails, DriverInvokeOptions, DriverMoney, DriverOperation, DriverPackedColumn, DriverPackedQueryResult, DriverResult, DriverTimestamp, DriverValue, } from "./driver.js";
376
+ export { DRIVER_ERROR_REGISTRY_HASH, DRIVER_PROTOCOL_VERSION, DRIVER_VALUE_REGISTRY_HASH, DriverApplicationError, DriverApplicationTransport, resolveTransportOptionsFromEnv, } from "./driver.js";
377
+ export type { DriverApplicationIdentity, DriverApplicationTransportOptions, DriverConnectFromEnvOptions, DriverBatchItem, DriverBatchResult, DriverBatchSuccess, DriverDecimal, DriverErrorDetails, DriverInvokeOptions, DriverMoney, DriverOperation, DriverPackedColumn, DriverPackedQueryResult, DriverResult, DriverTimestamp, DriverValue, ResolvedDriverApplicationIdentity, } from "./driver.js";
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { promisify } from "node:util";
6
+ import { DriverApplicationTransport, } from "./driver.js";
6
7
  const executeFile = promisify(execFile);
7
8
  const MAX_OUTPUT_BYTES = 4_194_304;
8
9
  const HASH = /^[0-9a-f]{64}$/;
@@ -18,6 +19,253 @@ export class InputBudgetError extends Error {
18
19
  this.name = "InputBudgetError";
19
20
  }
20
21
  }
22
+ export class RiffTimestamp {
23
+ seconds;
24
+ nanos;
25
+ constructor(seconds, nanos = 0) {
26
+ if (typeof seconds !== "bigint" || seconds < -(1n << 63n) || seconds > (1n << 63n) - 1n) {
27
+ throw new Error("invalid RiffDB timestamp seconds");
28
+ }
29
+ if (!Number.isInteger(nanos) || nanos < 0 || nanos > 999_999_999) {
30
+ throw new Error("invalid RiffDB timestamp nanos");
31
+ }
32
+ this.seconds = seconds;
33
+ this.nanos = nanos;
34
+ }
35
+ static from(value) {
36
+ const parsed = coerceTimestampInput(value, "invalid RiffDB timestamp input");
37
+ return createTimestampValue(parsed.seconds, parsed.nanos);
38
+ }
39
+ static now() {
40
+ return RiffTimestamp.from(new Date());
41
+ }
42
+ toISOString() {
43
+ return formatTimestampIso(this.seconds, this.nanos);
44
+ }
45
+ toDate() {
46
+ return new Date(Number(this.seconds) * 1000 + Math.floor(this.nanos / 1_000_000));
47
+ }
48
+ toJSON() {
49
+ return this.toISOString();
50
+ }
51
+ toString() {
52
+ return this.toISOString();
53
+ }
54
+ }
55
+ export function createTimestampValue(seconds, nanos) {
56
+ const target = { seconds, nanos };
57
+ Object.defineProperties(target, {
58
+ toISOString: {
59
+ value() {
60
+ return formatTimestampIso(this.seconds, this.nanos);
61
+ },
62
+ enumerable: false,
63
+ configurable: true,
64
+ },
65
+ toDate: {
66
+ value() {
67
+ return new Date(Number(this.seconds) * 1000 + Math.floor(this.nanos / 1_000_000));
68
+ },
69
+ enumerable: false,
70
+ configurable: true,
71
+ },
72
+ toJSON: {
73
+ value() {
74
+ return formatTimestampIso(this.seconds, this.nanos);
75
+ },
76
+ enumerable: false,
77
+ configurable: true,
78
+ },
79
+ toString: {
80
+ value() {
81
+ return formatTimestampIso(this.seconds, this.nanos);
82
+ },
83
+ enumerable: false,
84
+ configurable: true,
85
+ },
86
+ });
87
+ return target;
88
+ }
89
+ function isGregorianLeapYear(year) {
90
+ return year % 4n === 0n && (year % 100n !== 0n || year % 400n === 0n);
91
+ }
92
+ function daysInGregorianMonth(year, month) {
93
+ switch (month) {
94
+ case 1:
95
+ case 3:
96
+ case 5:
97
+ case 7:
98
+ case 8:
99
+ case 10:
100
+ case 12:
101
+ return 31;
102
+ case 4:
103
+ case 6:
104
+ case 9:
105
+ case 11:
106
+ return 30;
107
+ case 2:
108
+ return isGregorianLeapYear(year) ? 29 : 28;
109
+ default:
110
+ return 0;
111
+ }
112
+ }
113
+ function daysFromCivil(year, month, day) {
114
+ const adjustedYear = month <= 2 ? year - 1n : year;
115
+ const era = (adjustedYear >= 0n ? adjustedYear : adjustedYear - 399n) / 400n;
116
+ const yoe = adjustedYear - era * 400n;
117
+ const m = BigInt(month);
118
+ const d = BigInt(day);
119
+ const doy = (153n * (m > 2n ? m - 3n : m + 9n) + 2n) / 5n + d - 1n;
120
+ const doe = yoe * 365n + yoe / 4n - yoe / 100n + doy;
121
+ return era * 146097n + doe - 719468n;
122
+ }
123
+ function civilFromDays(days) {
124
+ const z = days + 719468n;
125
+ const era = (z >= 0n ? z : z - 146096n) / 146097n;
126
+ const doe = z - era * 146097n;
127
+ const yoe = (doe - doe / 1460n + doe / 36524n - doe / 146096n) / 365n;
128
+ const y = yoe + era * 400n;
129
+ const doy = doe - (365n * yoe + yoe / 4n - yoe / 100n);
130
+ const mp = (5n * doy + 2n) / 153n;
131
+ const d = doy - (153n * mp + 2n) / 5n + 1n;
132
+ const m = mp < 10n ? mp + 3n : mp - 9n;
133
+ return {
134
+ year: m <= 2n ? y + 1n : y,
135
+ month: Number(m),
136
+ day: Number(d),
137
+ };
138
+ }
139
+ export function formatTimestampIso(seconds, nanos) {
140
+ const daySeconds = ((seconds % 86400n) + 86400n) % 86400n;
141
+ const days = (seconds - daySeconds) / 86400n;
142
+ const { year, month, day } = civilFromDays(days);
143
+ const hour = Number(daySeconds / 3600n);
144
+ const minute = Number((daySeconds % 3600n) / 60n);
145
+ const second = Number(daySeconds % 60n);
146
+ const yearPart = year < 0n
147
+ ? `-${(-year).toString().padStart(6, "0")}`
148
+ : year > 9999n
149
+ ? `+${year.toString().padStart(6, "0")}`
150
+ : year.toString().padStart(4, "0");
151
+ const datePart = `${yearPart}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:${String(second).padStart(2, "0")}`;
152
+ const fractionPart = nanos % 1_000_000 === 0
153
+ ? String(Math.floor(nanos / 1_000_000)).padStart(3, "0")
154
+ : String(nanos).padStart(9, "0").replace(/0+$/, "");
155
+ return `${datePart}.${fractionPart}Z`;
156
+ }
157
+ export function formatExactDecimal(value) {
158
+ let coefficient = decodeSignedTwosComplement(value.coefficientTwosComplement);
159
+ const negative = coefficient < 0n;
160
+ if (negative)
161
+ coefficient = -coefficient;
162
+ const scale = value.scale;
163
+ if (scale === 0) {
164
+ return `${negative ? "-" : ""}${coefficient.toString()}`;
165
+ }
166
+ const digits = coefficient.toString().padStart(scale + 1, "0");
167
+ const split = digits.length - scale;
168
+ return `${negative ? "-" : ""}${digits.slice(0, split)}.${digits.slice(split)}`;
169
+ }
170
+ function attachDecimalToJson(decimal) {
171
+ Object.defineProperties(decimal, {
172
+ toString: {
173
+ value() {
174
+ return formatExactDecimal(this);
175
+ },
176
+ enumerable: false,
177
+ configurable: true,
178
+ },
179
+ toJSON: {
180
+ value() {
181
+ return formatExactDecimal(this);
182
+ },
183
+ enumerable: false,
184
+ configurable: true,
185
+ },
186
+ });
187
+ return decimal;
188
+ }
189
+ function attachMoneyToJson(money) {
190
+ Object.defineProperties(money, {
191
+ toString: {
192
+ value() {
193
+ return `${this.currency} ${formatExactDecimal(this.amount)}`;
194
+ },
195
+ enumerable: false,
196
+ configurable: true,
197
+ },
198
+ toJSON: {
199
+ value() {
200
+ return { currency: this.currency, amount: formatExactDecimal(this.amount) };
201
+ },
202
+ enumerable: false,
203
+ configurable: true,
204
+ },
205
+ });
206
+ return money;
207
+ }
208
+ export function toJsonValue(value, key) {
209
+ if (value === null || value === undefined || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
210
+ return value;
211
+ }
212
+ if (typeof value === "bigint") {
213
+ if (key !== undefined && (key === "revision" || key.endsWith("_revision") || key.endsWith("Revision"))) {
214
+ return value.toString();
215
+ }
216
+ if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
217
+ return Number(value);
218
+ }
219
+ return value.toString();
220
+ }
221
+ if (value instanceof Date) {
222
+ return value.toISOString();
223
+ }
224
+ if (value instanceof Uint8Array) {
225
+ return Buffer.from(value).toString("base64");
226
+ }
227
+ if (Array.isArray(value)) {
228
+ return value.map((item) => toJsonValue(item));
229
+ }
230
+ if (typeof value === "object") {
231
+ const record = value;
232
+ if (typeof record.seconds === "bigint" && typeof record.nanos === "number") {
233
+ return formatTimestampIso(record.seconds, record.nanos);
234
+ }
235
+ if (record.coefficientTwosComplement instanceof Uint8Array && typeof record.scale === "number") {
236
+ return formatExactDecimal(record);
237
+ }
238
+ if (typeof record.currency === "string" && typeof record.amount === "object" && record.amount !== null) {
239
+ const amount = record.amount;
240
+ if (amount.coefficientTwosComplement instanceof Uint8Array && typeof amount.scale === "number") {
241
+ return {
242
+ currency: record.currency,
243
+ amount: formatExactDecimal(amount),
244
+ };
245
+ }
246
+ }
247
+ const output = {};
248
+ for (const [entryKey, entryValue] of Object.entries(record)) {
249
+ output[entryKey] = toJsonValue(entryValue, entryKey);
250
+ }
251
+ return output;
252
+ }
253
+ return value;
254
+ }
255
+ export function attachRecordToJson(record) {
256
+ Object.defineProperty(record, "toJSON", {
257
+ value() {
258
+ const output = {};
259
+ for (const [entryKey, entryValue] of Object.entries(this)) {
260
+ output[entryKey] = toJsonValue(entryValue, entryKey);
261
+ }
262
+ return output;
263
+ },
264
+ enumerable: false,
265
+ configurable: true,
266
+ });
267
+ return record;
268
+ }
21
269
  /**
22
270
  * Constructs an exact fixed-point value from decimal text without using a
23
271
  * JavaScript floating-point number.
@@ -43,17 +291,17 @@ export function exactDecimal(value, precision, scale) {
43
291
  const limit = 10n ** BigInt(precision);
44
292
  if (coefficient <= -limit || coefficient >= limit)
45
293
  throw new Error("exact decimal exceeds precision");
46
- return {
294
+ return attachDecimalToJson({
47
295
  coefficientTwosComplement: signedTwosComplement(coefficient),
48
296
  scale,
49
297
  precision,
50
- };
298
+ });
51
299
  }
52
300
  /** Constructs the exact precision-38, scale-2 value used by `money<CURRENCY>`. */
53
301
  export function exactMoney(currency, value) {
54
302
  if (!/^[A-Z]{3}$/.test(currency))
55
303
  throw new Error("invalid exact money currency");
56
- return { currency, amount: exactDecimal(value, 38, 2) };
304
+ return attachMoneyToJson({ currency, amount: exactDecimal(value, 38, 2) });
57
305
  }
58
306
  export class CliApplicationTransport {
59
307
  options;
@@ -389,10 +637,19 @@ export class DriverGeneratedApplicationTransport {
389
637
  constructor(driver) {
390
638
  this.driver = driver;
391
639
  }
640
+ static async connect(options) {
641
+ return new DriverGeneratedApplicationTransport(await DriverApplicationTransport.connect(options));
642
+ }
643
+ static async connectFromEnv(options = {}) {
644
+ return new DriverGeneratedApplicationTransport(await DriverApplicationTransport.connectFromEnv(options));
645
+ }
392
646
  /** Returns the bound tenant identifier when this session is tenant-scoped. */
393
647
  get tenant() {
394
648
  return this.driver.tenant;
395
649
  }
650
+ async shutdown() {
651
+ await this.driver.shutdown();
652
+ }
396
653
  async executeNamedQuery(request, options = {}) {
397
654
  validateIdentity(request);
398
655
  const operation = requireDriverOperation(request.driverOperation);
@@ -613,6 +870,42 @@ function decodeDriverMutationResult(value) {
613
870
  }
614
871
  function decodeDriverConsumerStatus(value) {
615
872
  const status = driverRecord(value);
873
+ if (status.kind !== undefined) {
874
+ const kind = driverEnum(status.kind);
875
+ if (kind === "protected") {
876
+ const historyIncarnation = driverPositiveU64(requiredDriverField(status, "history_incarnation"));
877
+ const rawCursor = requiredDriverField(status, "progress_cursor");
878
+ let progressCursor;
879
+ if (rawCursor.type === "bytes") {
880
+ if (rawCursor.value.length < 1 || rawCursor.value.length > 512) {
881
+ throw new Error("invalid RiffDB driver consumer status");
882
+ }
883
+ progressCursor = rawCursor.value;
884
+ }
885
+ else if (rawCursor.type === "string") {
886
+ progressCursor = expectBoundedString(rawCursor.value, 512);
887
+ if (progressCursor.length < 1) {
888
+ throw new Error("invalid RiffDB driver consumer status");
889
+ }
890
+ }
891
+ else {
892
+ throw new Error("invalid RiffDB driver consumer status");
893
+ }
894
+ return {
895
+ kind: "protected",
896
+ revision: 0n,
897
+ checkpoint: progressCursor,
898
+ progressCursor,
899
+ historyIncarnation,
900
+ liveLeases: 0,
901
+ retries: 0,
902
+ deadLetters: 0,
903
+ };
904
+ }
905
+ if (kind !== "exact") {
906
+ throw new Error("invalid RiffDB driver consumer status");
907
+ }
908
+ }
616
909
  return {
617
910
  revision: driverPositiveU64(requiredDriverField(status, "revision")),
618
911
  checkpoint: driverString(requiredDriverField(status, "checkpoint"), 256),
@@ -701,13 +994,13 @@ function decodeDriverLiveUpdate(value, applicationHead, responseCursor) {
701
994
  return {
702
995
  type: "terminal",
703
996
  reason,
704
- lastApplicationHead: driverPositiveU64(requiredDriverField(record, "application_head")),
997
+ lastApplicationHead: driverNonnegativeU64(requiredDriverField(record, "application_head")),
705
998
  historyIncarnation: driverPositiveU64(requiredDriverField(record, "history_incarnation")),
706
999
  };
707
1000
  }
708
1001
  const checkpoint = driverRecord(requiredDriverField(update, "checkpoint"));
709
1002
  const cursor = driverString(requiredDriverField(checkpoint, "cursor"), 16_384);
710
- const head = driverPositiveU64(requiredDriverField(checkpoint, "application_head"));
1003
+ const head = driverNonnegativeU64(requiredDriverField(checkpoint, "application_head"));
711
1004
  const history = driverPositiveU64(requiredDriverField(checkpoint, "history_incarnation"));
712
1005
  if (applicationHead !== head || responseCursor !== cursor) {
713
1006
  throw new Error("RiffDB driver live frontier is inconsistent");
@@ -774,6 +1067,7 @@ function decodeDriverDynamic(value) {
774
1067
  throw new Error("invalid RiffDB driver money");
775
1068
  return { currency: value.value.currency, amount: { coefficientTwosComplement: Uint8Array.from(Buffer.from(value.value.amount.coefficient, "base64")), scale: value.value.amount.scale, precision: value.value.amount.precision } };
776
1069
  }
1070
+ case "vector": return decodeVectorBits(value.value.component_bits, value.value.component_bits.length);
777
1071
  case "list": return value.value.map(decodeDriverDynamic);
778
1072
  case "record": return decodeDriverDynamicRecord(value.value);
779
1073
  }
@@ -805,6 +1099,14 @@ function driverPositiveU64(value) {
805
1099
  throw new Error("invalid RiffDB driver u64");
806
1100
  return parsed;
807
1101
  }
1102
+ function driverNonnegativeU64(value) {
1103
+ if (value.type !== "u64")
1104
+ throw new Error("invalid RiffDB driver u64");
1105
+ const parsed = BigInt(value.value);
1106
+ if (parsed < 0n || parsed > (1n << 64n) - 1n)
1107
+ throw new Error("invalid RiffDB driver u64");
1108
+ return parsed;
1109
+ }
808
1110
  function driverBoundedU32(value) {
809
1111
  if (value.type !== "u64")
810
1112
  throw new Error("invalid RiffDB driver u32");
@@ -899,14 +1201,14 @@ function encodeDriverValue(value, schema, budgetPath) {
899
1201
  if (typeof value !== "boolean")
900
1202
  throw new Error("invalid generated RiffDB bool input");
901
1203
  return { type: "bool", value };
902
- case "i64":
903
- if (typeof value !== "bigint" || value < -(1n << 63n) || value > (1n << 63n) - 1n)
904
- throw new Error("invalid generated RiffDB i64 input");
905
- return { type: "i64", value: value.toString() };
906
- case "u64":
907
- if (typeof value !== "bigint" || value < 0n || value > (1n << 64n) - 1n)
908
- throw new Error("invalid generated RiffDB u64 input");
909
- return { type: "u64", value: value.toString() };
1204
+ case "i64": {
1205
+ const parsed = coerceI64Input(value, "invalid generated RiffDB i64 input");
1206
+ return { type: "i64", value: parsed.toString() };
1207
+ }
1208
+ case "u64": {
1209
+ const parsed = coerceU64Input(value, "invalid generated RiffDB u64 input");
1210
+ return { type: "u64", value: parsed.toString() };
1211
+ }
910
1212
  case "string": {
911
1213
  const string = expectBoundedString(value, 262_144);
912
1214
  if (schema.maximumBytes !== undefined && new TextEncoder().encode(string).length > schema.maximumBytes && budgetPath !== undefined) {
@@ -932,10 +1234,8 @@ function encodeDriverValue(value, schema, budgetPath) {
932
1234
  throw new Error("invalid generated RiffDB date input");
933
1235
  return { type: "date", value: String(value) };
934
1236
  case "timestamp": {
935
- const timestamp = exactObject(value);
936
- if (typeof timestamp.seconds !== "bigint" || timestamp.seconds < -(1n << 63n) || timestamp.seconds > (1n << 63n) - 1n)
937
- throw new Error("invalid generated RiffDB timestamp input");
938
- return { type: "timestamp", value: { seconds: timestamp.seconds.toString(), nanos: boundedInteger(timestamp.nanos, 0, 999_999_999) } };
1237
+ const timestamp = coerceTimestampInput(value, "invalid generated RiffDB timestamp input");
1238
+ return { type: "timestamp", value: { seconds: timestamp.seconds.toString(), nanos: timestamp.nanos } };
939
1239
  }
940
1240
  case "decimal": return { type: "decimal", value: encodeDriverDecimal(value, schema) };
941
1241
  case "money": {
@@ -1120,7 +1420,7 @@ function decodeDriverValue(value, schema) {
1120
1420
  throw new Error("invalid RiffDB driver record result");
1121
1421
  output[field.name] = decodeDriverValue(fieldValue, field.schema);
1122
1422
  }
1123
- return output;
1423
+ return attachRecordToJson(output);
1124
1424
  }
1125
1425
  switch (schema.kind) {
1126
1426
  case "bool":
@@ -1167,7 +1467,7 @@ function decodeDriverValue(value, schema) {
1167
1467
  break;
1168
1468
  case "timestamp":
1169
1469
  if (value.type === "timestamp")
1170
- return { seconds: BigInt(value.value.seconds), nanos: boundedInteger(value.value.nanos, 0, 999_999_999) };
1470
+ return createTimestampValue(BigInt(value.value.seconds), boundedInteger(value.value.nanos, 0, 999_999_999));
1171
1471
  break;
1172
1472
  case "decimal":
1173
1473
  if (value.type === "decimal")
@@ -1175,7 +1475,7 @@ function decodeDriverValue(value, schema) {
1175
1475
  break;
1176
1476
  case "money":
1177
1477
  if (value.type === "money" && value.value.currency === schema.currency)
1178
- return { currency: value.value.currency, amount: decodeDriverDecimal(value.value.amount, schema) };
1478
+ return attachMoneyToJson({ currency: value.value.currency, amount: decodeDriverDecimal(value.value.amount, schema) });
1179
1479
  break;
1180
1480
  case "vector":
1181
1481
  if (value.type === "vector")
@@ -1196,7 +1496,7 @@ function decodeDriverDecimal(value, schema) {
1196
1496
  const coefficient = Uint8Array.from(Buffer.from(value.coefficient, "base64"));
1197
1497
  if (coefficient.byteLength < 1 || coefficient.byteLength > 16)
1198
1498
  throw new Error("RiffDB driver decimal coefficient is invalid");
1199
- return { coefficientTwosComplement: coefficient, precision: schema.precision, scale: schema.scale };
1499
+ return attachDecimalToJson({ coefficientTwosComplement: coefficient, precision: schema.precision, scale: schema.scale });
1200
1500
  }
1201
1501
  function decodeTypedCommandResult(result, request) {
1202
1502
  if (expectHash(result.plan_hash) !== request.planHash) {
@@ -1207,7 +1507,7 @@ function decodeTypedCommandResult(result, request) {
1207
1507
  if (schema === undefined)
1208
1508
  throw new Error("RiffDB application response has an unknown outcome");
1209
1509
  const payload = decodeWire(result.outcome, schema);
1210
- const outcome = { outcome: outcomeName, ...exactObject(payload) };
1510
+ const outcome = attachRecordToJson({ outcome: outcomeName, ...exactObject(payload) });
1211
1511
  const typed = {
1212
1512
  outcome,
1213
1513
  contractVersion: positiveNumber(result.contract_version),
@@ -1367,16 +1667,14 @@ function encodeValue(value, schema, budgetPath) {
1367
1667
  if (typeof value !== "boolean")
1368
1668
  throw new Error("invalid boolean input");
1369
1669
  return value;
1370
- case "i64":
1371
- if (typeof value !== "bigint" || value < -(1n << 63n) || value > (1n << 63n) - 1n) {
1372
- throw new Error("invalid i64 input");
1373
- }
1374
- return { $i64: value.toString() };
1375
- case "u64":
1376
- if (typeof value !== "bigint" || value < 0n || value > (1n << 64n) - 1n) {
1377
- throw new Error("invalid u64 input");
1378
- }
1379
- return { $u64: value.toString() };
1670
+ case "i64": {
1671
+ const parsed = coerceI64Input(value, "invalid i64 input");
1672
+ return { $i64: parsed.toString() };
1673
+ }
1674
+ case "u64": {
1675
+ const parsed = coerceU64Input(value, "invalid u64 input");
1676
+ return { $u64: parsed.toString() };
1677
+ }
1380
1678
  case "decimal": {
1381
1679
  const decimal = encodeDecimal(value);
1382
1680
  if (schema.precision === undefined || schema.scale === undefined
@@ -1416,14 +1714,8 @@ function encodeValue(value, schema, budgetPath) {
1416
1714
  }
1417
1715
  return { $date: value };
1418
1716
  case "timestamp": {
1419
- const input = exactObject(value);
1420
- const seconds = input.seconds;
1421
- const nanos = input.nanos;
1422
- if (typeof seconds !== "bigint" || seconds < -(1n << 63n) || seconds > (1n << 63n) - 1n
1423
- || !Number.isInteger(nanos) || nanos < 0 || nanos > 999_999_999) {
1424
- throw new Error("invalid timestamp input");
1425
- }
1426
- return { $timestamp: { seconds: seconds.toString(), nanos } };
1717
+ const timestamp = coerceTimestampInput(value, "invalid timestamp input");
1718
+ return { $timestamp: { seconds: timestamp.seconds.toString(), nanos: timestamp.nanos } };
1427
1719
  }
1428
1720
  case "vector": return { $vector: encodeVector(value, schema.dimension) };
1429
1721
  case "limit":
@@ -1500,7 +1792,7 @@ function normalizeQueryFields(value) {
1500
1792
  throw new Error("duplicate RiffDB query field");
1501
1793
  result[fieldName] = decodeTagged(entry.value);
1502
1794
  }
1503
- return result;
1795
+ return attachRecordToJson(result);
1504
1796
  });
1505
1797
  switch (field.cardinality) {
1506
1798
  case "one":
@@ -1533,13 +1825,13 @@ function decodeTagged(value) {
1533
1825
  case "uuid": return expectBoundedString(input.value, 262_144);
1534
1826
  case "enum": return expectSymbol(input.name);
1535
1827
  case "date": return Number(expectDecimal(input.days_since_unix_epoch));
1536
- case "timestamp": return { seconds: BigInt(expectDecimal(input.seconds)), nanos: nonnegativeNumber(input.nanos) };
1828
+ case "timestamp": return createTimestampValue(BigInt(expectDecimal(input.seconds)), nonnegativeNumber(input.nanos));
1537
1829
  case "bytes": return Uint8Array.from(Buffer.from(expectBoundedString(input.value, MAX_OUTPUT_BYTES), "base64"));
1538
1830
  case "decimal": return decodeDecimal(input);
1539
- case "money": return {
1831
+ case "money": return attachMoneyToJson({
1540
1832
  currency: expectCurrency(input.currency),
1541
1833
  amount: decodeDecimal(exactObject(input.amount)),
1542
- };
1834
+ });
1543
1835
  case "vector": return decodeVector(input.components, undefined);
1544
1836
  case "list":
1545
1837
  if (!Array.isArray(input.values))
@@ -1575,9 +1867,9 @@ function decodeDecimal(input) {
1575
1867
  if (coefficient.byteLength < 1 || coefficient.byteLength > 16)
1576
1868
  return fail();
1577
1869
  if (input.precision === undefined)
1578
- return { coefficientTwosComplement: coefficient, scale };
1870
+ return attachDecimalToJson({ coefficientTwosComplement: coefficient, scale });
1579
1871
  const precision = positiveNumber(input.precision);
1580
- return { coefficientTwosComplement: coefficient, scale, precision };
1872
+ return attachDecimalToJson({ coefficientTwosComplement: coefficient, scale, precision });
1581
1873
  }
1582
1874
  function decodePlain(value, schema) {
1583
1875
  if (schema.kind === "optional")
@@ -1596,7 +1888,7 @@ function decodePlain(value, schema) {
1596
1888
  return fail();
1597
1889
  for (const field of schema.fields)
1598
1890
  output[field.name] = decodePlain(input[field.name], field.schema);
1599
- return output;
1891
+ return attachRecordToJson(output);
1600
1892
  }
1601
1893
  switch (schema.kind) {
1602
1894
  case "bool":
@@ -1625,8 +1917,9 @@ function decodePlain(value, schema) {
1625
1917
  break;
1626
1918
  case "timestamp": {
1627
1919
  const input = exactObject(value);
1628
- if (typeof input.seconds === "bigint" && Number.isInteger(input.nanos))
1629
- return value;
1920
+ if (typeof input.seconds === "bigint" && Number.isInteger(input.nanos)) {
1921
+ return createTimestampValue(input.seconds, input.nanos);
1922
+ }
1630
1923
  break;
1631
1924
  }
1632
1925
  case "bytes":
@@ -1641,10 +1934,10 @@ function decodePlain(value, schema) {
1641
1934
  const currency = expectCurrency(input.currency);
1642
1935
  if (schema.currency !== undefined && currency !== schema.currency)
1643
1936
  return fail();
1644
- return {
1937
+ return attachMoneyToJson({
1645
1938
  currency,
1646
1939
  amount: normalizeDecodedDecimal(input.amount, schema.precision, schema.scale),
1647
- };
1940
+ });
1648
1941
  }
1649
1942
  case "vector": return decodeVector(value, schema.dimension);
1650
1943
  default: break;
@@ -1659,11 +1952,11 @@ function normalizeDecodedDecimal(value, expectedPrecision, expectedScale) {
1659
1952
  || (expectedScale !== undefined && encoded.scale !== expectedScale))
1660
1953
  return fail();
1661
1954
  const precision = expectedPrecision ?? encoded.precision;
1662
- return {
1955
+ return attachDecimalToJson({
1663
1956
  coefficientTwosComplement: input.coefficientTwosComplement,
1664
1957
  scale: encoded.scale,
1665
1958
  ...(precision === undefined ? {} : { precision }),
1666
- };
1959
+ });
1667
1960
  }
1668
1961
  function decodeWire(value, schema) {
1669
1962
  if (schema.kind !== "record")
@@ -1876,7 +2169,7 @@ function encodeVectorBits(value, dimension) {
1876
2169
  });
1877
2170
  }
1878
2171
  function decodeVectorBits(value, dimension) {
1879
- if (!Array.isArray(value) || value.length !== dimension)
2172
+ if (!Array.isArray(value) || dimension < 1 || dimension > 4_096 || value.length !== dimension)
1880
2173
  return fail();
1881
2174
  const float = new Float32Array(1);
1882
2175
  const bits = new Uint32Array(float.buffer);
@@ -1903,6 +2196,26 @@ function decodeVector(value, dimension) {
1903
2196
  }
1904
2197
  function decodeReactiveConsumerStatus(value) {
1905
2198
  const status = exactObject(value);
2199
+ if (status.kind !== undefined) {
2200
+ const kind = expectSymbol(status.kind);
2201
+ if (kind === "protected") {
2202
+ const progressCursor = expectBoundedString(status.progress_cursor, 512);
2203
+ if (progressCursor.length < 1)
2204
+ return fail();
2205
+ return {
2206
+ kind: "protected",
2207
+ revision: 0n,
2208
+ checkpoint: progressCursor,
2209
+ progressCursor,
2210
+ historyIncarnation: positiveBigInt(status.history_incarnation),
2211
+ liveLeases: 0,
2212
+ retries: 0,
2213
+ deadLetters: 0,
2214
+ };
2215
+ }
2216
+ if (kind !== "exact")
2217
+ return fail();
2218
+ }
1906
2219
  return {
1907
2220
  revision: positiveBigInt(status.revision),
1908
2221
  checkpoint: expectBoundedString(status.checkpoint, 64),
@@ -2031,6 +2344,122 @@ function signedTwosComplement(value) {
2031
2344
  }
2032
2345
  return fail();
2033
2346
  }
2347
+ function decodeSignedTwosComplement(bytes) {
2348
+ if (bytes.byteLength < 1 || bytes.byteLength > 16)
2349
+ return fail();
2350
+ let value = 0n;
2351
+ for (const byte of bytes) {
2352
+ value = (value << 8n) | BigInt(byte);
2353
+ }
2354
+ if ((bytes[0] & 0x80) !== 0) {
2355
+ value -= 1n << BigInt(bytes.byteLength * 8);
2356
+ }
2357
+ return value;
2358
+ }
2359
+ function coerceI64Input(value, errorMessage) {
2360
+ let parsed;
2361
+ if (typeof value === "bigint") {
2362
+ parsed = value;
2363
+ }
2364
+ else if (typeof value === "number") {
2365
+ if (!Number.isSafeInteger(value))
2366
+ throw new Error(errorMessage);
2367
+ parsed = BigInt(value);
2368
+ }
2369
+ else if (typeof value === "string" && /^-?(?:0|[1-9][0-9]*)$/.test(value)) {
2370
+ parsed = BigInt(value);
2371
+ }
2372
+ else {
2373
+ throw new Error(errorMessage);
2374
+ }
2375
+ if (parsed < -(1n << 63n) || parsed > (1n << 63n) - 1n) {
2376
+ throw new Error(errorMessage);
2377
+ }
2378
+ return parsed;
2379
+ }
2380
+ function coerceU64Input(value, errorMessage) {
2381
+ let parsed;
2382
+ if (typeof value === "bigint") {
2383
+ parsed = value;
2384
+ }
2385
+ else if (typeof value === "number") {
2386
+ if (!Number.isSafeInteger(value) || value < 0)
2387
+ throw new Error(errorMessage);
2388
+ parsed = BigInt(value);
2389
+ }
2390
+ else if (typeof value === "string" && /^(?:0|[1-9][0-9]*)$/.test(value)) {
2391
+ parsed = BigInt(value);
2392
+ }
2393
+ else {
2394
+ throw new Error(errorMessage);
2395
+ }
2396
+ if (parsed < 0n || parsed > (1n << 64n) - 1n) {
2397
+ throw new Error(errorMessage);
2398
+ }
2399
+ return parsed;
2400
+ }
2401
+ function coerceTimestampInput(value, errorMessage) {
2402
+ if (value instanceof Date) {
2403
+ const ms = value.getTime();
2404
+ if (!Number.isFinite(ms))
2405
+ throw new Error(errorMessage);
2406
+ const seconds = BigInt(Math.floor(ms / 1000));
2407
+ const nanos = (((ms % 1000) + 1000) % 1000) * 1_000_000;
2408
+ return { seconds, nanos };
2409
+ }
2410
+ if (typeof value === "string") {
2411
+ const match = /^([+-]?\d{4,19})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|([+-])(\d{2}):(\d{2}))$/.exec(value);
2412
+ if (match === null)
2413
+ throw new Error(errorMessage);
2414
+ const rawYear = match[1];
2415
+ if (/^-0+$/.test(rawYear))
2416
+ throw new Error(errorMessage);
2417
+ const year = BigInt(rawYear);
2418
+ const month = Number(match[2]);
2419
+ const day = Number(match[3]);
2420
+ const hour = Number(match[4]);
2421
+ const minute = Number(match[5]);
2422
+ const second = Number(match[6]);
2423
+ if (month < 1
2424
+ || month > 12
2425
+ || day < 1
2426
+ || day > daysInGregorianMonth(year, month)
2427
+ || hour > 23
2428
+ || minute > 59
2429
+ || second > 59) {
2430
+ throw new Error(errorMessage);
2431
+ }
2432
+ let offsetSeconds = 0n;
2433
+ if (match[8] !== "Z") {
2434
+ const offsetSign = match[9] === "-" ? -1n : 1n;
2435
+ const offsetHours = Number(match[10]);
2436
+ const offsetMinutes = Number(match[11]);
2437
+ if (offsetHours > 23 || offsetMinutes > 59) {
2438
+ throw new Error(errorMessage);
2439
+ }
2440
+ offsetSeconds = offsetSign * BigInt((offsetHours * 60 + offsetMinutes) * 60);
2441
+ }
2442
+ const seconds = daysFromCivil(year, month, day) * 86400n + BigInt(hour * 3600 + minute * 60 + second) - offsetSeconds;
2443
+ if (seconds < -(1n << 63n) || seconds > (1n << 63n) - 1n) {
2444
+ throw new Error(errorMessage);
2445
+ }
2446
+ const nanos = match[7] === undefined ? 0 : Number(match[7].padEnd(9, "0"));
2447
+ if (!Number.isInteger(nanos) || nanos < 0 || nanos > 999_999_999) {
2448
+ throw new Error(errorMessage);
2449
+ }
2450
+ return { seconds, nanos };
2451
+ }
2452
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
2453
+ const input = value;
2454
+ const seconds = coerceI64Input(input.seconds, errorMessage);
2455
+ const rawNanos = input.nanos === undefined ? 0 : input.nanos;
2456
+ if (typeof rawNanos !== "number" || !Number.isInteger(rawNanos) || rawNanos < 0 || rawNanos > 999_999_999) {
2457
+ throw new Error(errorMessage);
2458
+ }
2459
+ return { seconds, nanos: rawNanos };
2460
+ }
2461
+ throw new Error(errorMessage);
2462
+ }
2034
2463
  function positiveNumber(value) {
2035
2464
  const number = typeof value === "string" ? Number(expectDecimal(value)) : value;
2036
2465
  if (!Number.isSafeInteger(number) || number < 1)
@@ -2051,4 +2480,4 @@ function positiveBigInt(value) {
2051
2480
  function fail() {
2052
2481
  throw new Error("invalid RiffDB application response");
2053
2482
  }
2054
- export { DRIVER_ERROR_REGISTRY_HASH, DRIVER_PROTOCOL_VERSION, DRIVER_VALUE_REGISTRY_HASH, DriverApplicationError, DriverApplicationTransport, } from "./driver.js";
2483
+ export { DRIVER_ERROR_REGISTRY_HASH, DRIVER_PROTOCOL_VERSION, DRIVER_VALUE_REGISTRY_HASH, DriverApplicationError, DriverApplicationTransport, resolveTransportOptionsFromEnv, } from "./driver.js";
package/package.json CHANGED
@@ -1 +1 @@
1
- {"description":"First-party application runtime for the Rust-owned RiffDB driver host","engines":{"node":">=22"},"exports":{".":"./dist/index.js","./operator":"./dist/operator.js"},"files":["dist","README.md","conformance-manifest-v1.json"],"license":"MIT OR Apache-2.0","name":"@riffdb/client","repository":{"type":"git","url":"git+https://github.com/riffdb/riffdb.git"},"type":"module","types":"./dist/index.d.ts","version":"0.2.0"}
1
+ {"description":"First-party application runtime for the Rust-owned RiffDB driver host","engines":{"node":">=22"},"exports":{".":"./dist/index.js","./operator":"./dist/operator.js"},"files":["dist","README.md","conformance-manifest-v1.json"],"license":"MIT OR Apache-2.0","name":"@riffdb/client","repository":{"type":"git","url":"git+https://github.com/riffdb/riffdb.git"},"type":"module","types":"./dist/index.d.ts","version":"0.2.1"}