@spine-event-engine/storage 2.0.0-snapshot.2 → 2.0.0-snapshot.21

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.
Files changed (53) hide show
  1. package/README.md +47 -14
  2. package/REFERENCE.md +26 -18
  3. package/dist/index.d.ts +2 -4
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +0 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/internal/entity-commit.d.ts +28 -23
  8. package/dist/internal/entity-commit.d.ts.map +1 -1
  9. package/dist/internal/entity-commit.js +10 -1
  10. package/dist/internal/entity-commit.js.map +1 -1
  11. package/dist/internal/entity-history.d.ts +1 -1
  12. package/dist/internal/entity-history.d.ts.map +1 -1
  13. package/dist/memory/canonical-utf8.d.ts.map +1 -1
  14. package/dist/memory/canonical-utf8.js +13 -6
  15. package/dist/memory/canonical-utf8.js.map +1 -1
  16. package/dist/memory/in-memory-entity-commit.d.ts +38 -6
  17. package/dist/memory/in-memory-entity-commit.d.ts.map +1 -1
  18. package/dist/memory/in-memory-entity-commit.js +281 -68
  19. package/dist/memory/in-memory-entity-commit.js.map +1 -1
  20. package/dist/memory/in-memory-entity-history.d.ts +115 -1
  21. package/dist/memory/in-memory-entity-history.d.ts.map +1 -1
  22. package/dist/memory/in-memory-entity-history.js +186 -41
  23. package/dist/memory/in-memory-entity-history.js.map +1 -1
  24. package/dist/memory/in-memory-record-storage.d.ts +19 -0
  25. package/dist/memory/in-memory-record-storage.d.ts.map +1 -1
  26. package/dist/memory/in-memory-record-storage.js +25 -4
  27. package/dist/memory/in-memory-record-storage.js.map +1 -1
  28. package/dist/memory/tenant-records.d.ts +33 -7
  29. package/dist/memory/tenant-records.d.ts.map +1 -1
  30. package/dist/memory/tenant-records.js +297 -77
  31. package/dist/memory/tenant-records.js.map +1 -1
  32. package/dist/provider.d.ts +18 -0
  33. package/dist/provider.d.ts.map +1 -0
  34. package/dist/provider.js +23 -0
  35. package/dist/provider.js.map +1 -0
  36. package/dist/query/query-policy.d.ts +18 -14
  37. package/dist/query/query-policy.d.ts.map +1 -1
  38. package/dist/query/query-policy.js +55 -28
  39. package/dist/query/query-policy.js.map +1 -1
  40. package/dist/record/record-query.d.ts +12 -11
  41. package/dist/record/record-query.d.ts.map +1 -1
  42. package/dist/record/record-query.js +4 -1
  43. package/dist/record/record-query.js.map +1 -1
  44. package/dist/record/record-storage.d.ts +12 -5
  45. package/dist/record/record-storage.d.ts.map +1 -1
  46. package/dist/record/record-storage.js +14 -12
  47. package/dist/record/record-storage.js.map +1 -1
  48. package/dist/tsconfig.tsbuildinfo +1 -1
  49. package/package.json +6 -27
  50. package/dist/record/record-mask.d.ts +0 -11
  51. package/dist/record/record-mask.d.ts.map +0 -1
  52. package/dist/record/record-mask.js +0 -82
  53. package/dist/record/record-mask.js.map +0 -1
@@ -12,9 +12,11 @@
12
12
  * the License.
13
13
  */
14
14
  import { CanonicalUtf8 } from "./canonical-utf8.js";
15
- const tenantRecordsHost = globalThis;
16
15
  /**
17
- * Record slice owned by one tenant of an in-memory record storage.
16
+ * Record slice for one tenant of an in-memory record storage.
17
+ *
18
+ * @typeParam I Record identifier type.
19
+ * @typeParam R Stored Protobuf message type.
18
20
  */
19
21
  export class TenantRecords {
20
22
  #records = new Map();
@@ -86,22 +88,25 @@ export class TenantRecords {
86
88
  }
87
89
  }
88
90
  /**
89
- * Copies the materialized rows for a provider-owned staged commit.
91
+ * Captures one complete materialized slot for an Entity commit.
90
92
  *
91
- * @returns An independent snapshot of the current rows.
93
+ * @param id Identifies the affected slot.
94
+ * @returns The slot key and its prior entry, including materialized columns.
92
95
  */
93
- snapshot() {
94
- return tenantRecordsHost.structuredClone(this.#records);
96
+ capture(id) {
97
+ const key = StoredValues.key(id);
98
+ return { key, entry: this.#records.get(key) };
95
99
  }
96
100
  /**
97
- * Replaces all rows from a fully validated staged snapshot.
101
+ * Applies or restores one prepared slot without recomputing its key.
98
102
  *
99
- * @param snapshot Supplies the staged materialized rows.
103
+ * @param slot Supplies the prepared key and complete entry or prior absence.
100
104
  */
101
- replace(snapshot) {
102
- this.#records.clear();
103
- for (const [key, value] of snapshot)
104
- this.#records.set(key, value);
105
+ apply(slot) {
106
+ if (slot.entry === undefined)
107
+ this.#records.delete(slot.key);
108
+ else
109
+ this.#records.set(slot.key, slot.entry);
105
110
  }
106
111
  }
107
112
  /**
@@ -111,6 +116,11 @@ const StoredRecords = {
111
116
  // prettier-ignore
112
117
  /**
113
118
  * Determines whether two materialized records represent the same stored value.
119
+ * @typeParam I Record identifier type.
120
+ * @typeParam R Stored Protobuf message type.
121
+ * @param left Supplies the first materialized record, when present.
122
+ * @param right Supplies the second materialized record, when present.
123
+ * @returns Whether both records have the same stored value.
114
124
  */
115
125
  equal(left, right) {
116
126
  if (left === undefined || right === undefined)
@@ -124,7 +134,13 @@ const StoredRecords = {
124
134
  const TenantRecordQuery = {
125
135
  // prettier-ignore
126
136
  /**
127
- * Produces logical entries for one tenant-scoped query.
137
+ * Returns logical entries for one tenant-scoped query.
138
+ * @typeParam I Record identifier type.
139
+ * @typeParam R Stored Protobuf message type.
140
+ * @param entries Supplies this tenant's stored entries.
141
+ * @param spec Defines record identity and materialized columns.
142
+ * @param query Specifies filters, ordering, continuation, and windowing.
143
+ * @returns Matching logical record entries in query order.
128
144
  */
129
145
  entries(entries, spec, query) {
130
146
  const bounded = TenantRecordQuery.selectBounded(entries, spec, query);
@@ -143,38 +159,160 @@ const TenantRecordQuery = {
143
159
  }));
144
160
  },
145
161
  /**
146
- * Selects the finite ordered window without retaining every matching entry.
162
+ * Returns the finite ordered window without retaining every matching entry.
163
+ * @typeParam I Record identifier type.
164
+ * @typeParam R Stored Protobuf message type.
165
+ * @param entries Supplies this tenant's stored entries.
166
+ * @param spec Defines record identity and materialized columns.
167
+ * @param query Specifies filters, ordering, continuation, and windowing.
168
+ * @returns The selected window, or undefined when bounded selection does not apply.
147
169
  */
148
170
  selectBounded(entries, spec, query) {
149
- if (query.limit === undefined || !Number.isFinite(query.limit))
171
+ const windowSize = TenantRecordQuery.boundedWindowSize(query.limit, query.offset);
172
+ if (windowSize === undefined)
150
173
  return undefined;
151
174
  const offset = query.offset ?? 0;
152
- const windowSize = offset + query.limit;
153
- if (!Number.isInteger(offset) ||
154
- !Number.isInteger(query.limit) ||
155
- offset < 0 ||
156
- query.limit < 0) {
157
- return undefined;
158
- }
159
175
  if (windowSize === 0)
160
176
  return [];
161
177
  const orders = query.sort ?? [];
162
178
  const selected = [];
179
+ const filters = TenantRecordQuery.prepareFilters(query.filters);
180
+ const after = query.after === undefined ? undefined : TenantRecordQuery.prepareContinuation(query.after);
163
181
  for (const entry of entries) {
164
- if (!TenantRecordQuery.matches(spec, entry, query) ||
165
- (query.after !== undefined &&
166
- TenantRecordQuery.compareToContinuation(entry, orders, query.after) <= 0)) {
182
+ if (!TenantRecordQuery.matchesIds(spec, entry, query.ids) ||
183
+ !TenantRecordQuery.matchesPreparedFilters(entry.stored, filters))
184
+ continue;
185
+ const prepared = TenantRecordQuery.prepareEntry(entry, orders);
186
+ if (after !== undefined &&
187
+ TenantRecordQuery.comparePreparedAfter(prepared, orders, after) <= 0)
167
188
  continue;
168
- }
169
- TenantRecordQuery.insertSelected(selected, entry, windowSize, orders);
189
+ TenantRecordQuery.insertSelected(selected, prepared, windowSize, orders);
170
190
  }
171
- return selected.slice(offset);
191
+ return selected.slice(offset).map((candidate) => candidate.entry);
192
+ },
193
+ /**
194
+ * Determines the finite selection capacity of one bounded query.
195
+ * @param limit Maximum returned record count.
196
+ * @param offset Number of leading matches to skip.
197
+ * @returns Required capacity, or undefined for an unbounded query.
198
+ */
199
+ boundedWindowSize(limit, offset) {
200
+ if (limit === undefined || !Number.isFinite(limit))
201
+ return undefined;
202
+ const start = offset ?? 0;
203
+ if (!Number.isInteger(start) || !Number.isInteger(limit) || start < 0 || limit < 0)
204
+ return undefined;
205
+ return start + limit;
206
+ },
207
+ /**
208
+ * Prepares expected filter values once for a bounded query.
209
+ * @param filters Supplies the requested column filters.
210
+ * @returns Canonical expected values for each filter.
211
+ */
212
+ prepareFilters(filters) {
213
+ return filters?.map((filter) => ({
214
+ column: filter.column,
215
+ keys: (Array.isArray(filter.value) ? filter.value : [filter.value]).map((value) => StoredValues.key(value)),
216
+ }));
172
217
  },
173
218
  /**
174
- * Inserts an entry into a bounded, ordered top window.
219
+ * Matches materialized columns against filter keys prepared for this query.
220
+ * @typeParam I Record identifier type.
221
+ * @typeParam R Stored Protobuf message type.
222
+ * @param entry Supplies the materialized record.
223
+ * @param filters Supplies the prepared column filters, when present.
224
+ * @returns Whether every filter matches the record.
225
+ */
226
+ matchesPreparedFilters(entry, filters) {
227
+ if (filters === undefined || filters.length === 0)
228
+ return true;
229
+ return filters.every((filter) => filter.keys.includes(StoredValues.key(TenantRecordQuery.resolveValue(entry, filter.column))));
230
+ },
231
+ /**
232
+ * Normalizes an entry's identity and ordered fields once for this query.
233
+ * @typeParam I Record identifier type.
234
+ * @typeParam R Stored Protobuf message type.
235
+ * @param entry Supplies the matching materialized record.
236
+ * @param orders Specifies the requested sort fields.
237
+ * @returns The prepared comparison values and original entry.
238
+ */
239
+ prepareEntry(entry, orders) {
240
+ const id = StoredValues.normalize(entry.slotId);
241
+ return {
242
+ entry,
243
+ id,
244
+ values: orders.map((order) => order.field === "id"
245
+ ? id
246
+ : StoredValues.normalize(TenantRecordQuery.resolveValue(entry.stored, order.field))),
247
+ };
248
+ },
249
+ /**
250
+ * Normalizes the requested keyset position once for this query.
251
+ * @typeParam I Record identifier type.
252
+ * @param after Supplies the keyset position.
253
+ * @returns The prepared continuation values.
254
+ */
255
+ prepareContinuation(after) {
256
+ return {
257
+ id: StoredValues.normalize(after.id),
258
+ values: after.values.map((value) => StoredValues.normalize(value.value)),
259
+ };
260
+ },
261
+ /**
262
+ * Compares prepared entries by requested fields and storage identity.
263
+ * @typeParam I Record identifier type.
264
+ * @typeParam R Stored Protobuf message type.
265
+ * @param left Supplies the first prepared entry.
266
+ * @param right Supplies the second prepared entry.
267
+ * @param orders Specifies the requested sort fields.
268
+ * @returns A negative, zero, or positive comparison result.
269
+ */
270
+ comparePrepared(left, right, orders) {
271
+ for (let index = 0; index < orders.length; index += 1) {
272
+ const order = orders[index];
273
+ if (order === undefined)
274
+ throw new Error("Record query sort order is invalid.");
275
+ const comparison = order.field === "id"
276
+ ? StoredValues.compareIdentityNormalized(left.id, right.id)
277
+ : StoredValues.compareNormalized(left.values[index], right.values[index]);
278
+ if (comparison !== 0)
279
+ return order.direction === "desc" ? comparison * -1 : comparison;
280
+ }
281
+ return StoredValues.compareIdentityNormalized(left.id, right.id);
282
+ },
283
+ /**
284
+ * Compares one prepared entry to a normalized keyset continuation.
285
+ * @typeParam I Record identifier type.
286
+ * @typeParam R Stored Protobuf message type.
287
+ * @param entry Supplies the prepared entry.
288
+ * @param orders Specifies the requested sort fields.
289
+ * @param after Supplies the prepared continuation.
290
+ * @returns A negative, zero, or positive comparison result.
291
+ */
292
+ comparePreparedAfter(entry, orders, after) {
293
+ for (let index = 0; index < orders.length; index += 1) {
294
+ const order = orders[index];
295
+ if (order === undefined)
296
+ throw new Error("Record query continuation sort order is invalid.");
297
+ const comparison = order.field === "id"
298
+ ? StoredValues.compareIdentityNormalized(entry.id, after.values[index])
299
+ : StoredValues.compareNormalized(entry.values[index], after.values[index]);
300
+ if (comparison !== 0)
301
+ return order.direction === "desc" ? comparison * -1 : comparison;
302
+ }
303
+ return StoredValues.compareIdentityNormalized(entry.id, after.id);
304
+ },
305
+ /**
306
+ * Adds an entry to a bounded, ordered top window.
307
+ * @typeParam I Record identifier type.
308
+ * @typeParam R Stored Protobuf message type.
309
+ * @param selected Supplies the current prepared window to update.
310
+ * @param entry Supplies the prepared candidate entry.
311
+ * @param windowSize Sets the maximum number of entries to retain.
312
+ * @param orders Specifies the requested sort order.
175
313
  */
176
314
  insertSelected(selected, entry, windowSize, orders) {
177
- const comparison = (candidate) => TenantRecordQuery.compareEntries(candidate, entry, orders);
315
+ const comparison = (candidate) => TenantRecordQuery.comparePrepared(candidate, entry, orders);
178
316
  let low = 0;
179
317
  let high = selected.length;
180
318
  while (low < high) {
@@ -195,6 +333,11 @@ const TenantRecordQuery = {
195
333
  },
196
334
  /**
197
335
  * Applies offset and limit after filtering, ordering, and continuation.
336
+ * @typeParam T Record entry type.
337
+ * @param records Supplies ordered matching records.
338
+ * @param offset Sets the number of leading records to skip.
339
+ * @param limit Sets the maximum number of records to return.
340
+ * @returns The requested record window.
198
341
  */
199
342
  applyWindow(records, offset, limit) {
200
343
  const start = offset ?? 0;
@@ -202,6 +345,12 @@ const TenantRecordQuery = {
202
345
  },
203
346
  /**
204
347
  * Removes entries at or before a keyset continuation.
348
+ * @typeParam I Record identifier type.
349
+ * @typeParam R Stored Protobuf message type.
350
+ * @param records Supplies ordered matching records.
351
+ * @param orders Specifies the requested sort order.
352
+ * @param after Supplies the continuation position, when present.
353
+ * @returns Records after the continuation position.
205
354
  */
206
355
  continueAfter(records, orders, after) {
207
356
  return after === undefined
@@ -209,7 +358,13 @@ const TenantRecordQuery = {
209
358
  : records.filter((entry) => TenantRecordQuery.compareToContinuation(entry, orders, after) > 0);
210
359
  },
211
360
  /**
212
- * Orders entries by requested fields and storage-slot identity.
361
+ * Returns the comparison of entries by requested fields and storage-slot identity.
362
+ * @typeParam I Record identifier type.
363
+ * @typeParam R Stored Protobuf message type.
364
+ * @param left Supplies the first entry to compare.
365
+ * @param right Supplies the second entry to compare.
366
+ * @param orders Specifies the requested sort order.
367
+ * @returns A negative, zero, or positive comparison result.
213
368
  */
214
369
  compareEntries(left, right, orders) {
215
370
  for (const order of orders) {
@@ -222,7 +377,13 @@ const TenantRecordQuery = {
222
377
  return StoredValues.compareIdentity(left.slotId, right.slotId);
223
378
  },
224
379
  /**
225
- * Orders an entry relative to one keyset continuation.
380
+ * Returns the comparison of an entry with one keyset continuation.
381
+ * @typeParam I Record identifier type.
382
+ * @typeParam R Stored Protobuf message type.
383
+ * @param entry Supplies the entry to compare.
384
+ * @param orders Specifies the requested sort order.
385
+ * @param after Supplies the continuation position.
386
+ * @returns A negative, zero, or positive comparison result.
226
387
  */
227
388
  compareToContinuation(entry, orders, after) {
228
389
  for (let index = 0; index < orders.length; index += 1) {
@@ -239,6 +400,12 @@ const TenantRecordQuery = {
239
400
  },
240
401
  /**
241
402
  * Matches one entry against ID and column filters.
403
+ * @typeParam I Record identifier type.
404
+ * @typeParam R Stored Protobuf message type.
405
+ * @param spec Defines record identity and materialized columns.
406
+ * @param entry Supplies the candidate entry.
407
+ * @param query Specifies ID and column filters.
408
+ * @returns Whether the entry matches the query filters.
242
409
  */
243
410
  matches(spec, entry, query) {
244
411
  return (TenantRecordQuery.matchesIds(spec, entry, query.ids) &&
@@ -246,6 +413,11 @@ const TenantRecordQuery = {
246
413
  },
247
414
  /**
248
415
  * Matches materialized values against all requested column filters.
416
+ * @typeParam I Record identifier type.
417
+ * @typeParam R Stored Protobuf message type.
418
+ * @param entry Supplies the materialized record and columns.
419
+ * @param filters Specifies column filters, when present.
420
+ * @returns Whether every filter matches the entry.
249
421
  */
250
422
  matchesFilters(entry, filters) {
251
423
  if (filters === undefined || filters.length === 0)
@@ -258,6 +430,12 @@ const TenantRecordQuery = {
258
430
  },
259
431
  /**
260
432
  * Matches an entry's storage slot against requested logical IDs.
433
+ * @typeParam I Record identifier type.
434
+ * @typeParam R Stored Protobuf message type.
435
+ * @param spec Defines how record identifiers are copied.
436
+ * @param entry Supplies the candidate entry.
437
+ * @param ids Specifies requested identifiers, when present.
438
+ * @returns Whether the entry matches a requested identifier.
261
439
  */
262
440
  matchesIds(spec, entry, ids) {
263
441
  if (ids === undefined || ids.length === 0)
@@ -266,6 +444,11 @@ const TenantRecordQuery = {
266
444
  },
267
445
  /**
268
446
  * Resolves an ID, materialized column, or record path value.
447
+ * @typeParam I Record identifier type.
448
+ * @typeParam R Stored Protobuf message type.
449
+ * @param entry Supplies the materialized record.
450
+ * @param field Names the ID, column, or record path to read.
451
+ * @returns The resolved field value.
269
452
  */
270
453
  resolveValue(entry, field) {
271
454
  if (field === "id")
@@ -282,24 +465,35 @@ const StoredValues = {
282
465
  // prettier-ignore
283
466
  /**
284
467
  * Creates a canonical value key.
468
+ * @param value Supplies the value to encode.
469
+ * @returns A canonical key for the value.
285
470
  */
286
471
  key(value) {
287
- return StoredValues.encode(StoredValues.normalize(value));
472
+ return JSON.stringify(StoredValues.encoded(value));
288
473
  },
289
474
  /**
290
475
  * Compares values with the storage ordering rules.
476
+ * @param left Supplies the first value.
477
+ * @param right Supplies the second value.
478
+ * @returns A negative, zero, or positive comparison result.
291
479
  */
292
480
  compare(left, right) {
293
481
  return StoredValues.compareNormalized(StoredValues.normalize(left), StoredValues.normalize(right));
294
482
  },
295
483
  /**
296
484
  * Compares storage slot identities with canonical UTF-8 text ordering.
485
+ * @param left Supplies the first identity.
486
+ * @param right Supplies the second identity.
487
+ * @returns A negative, zero, or positive comparison result.
297
488
  */
298
489
  compareIdentity(left, right) {
299
490
  return StoredValues.compareIdentityNormalized(StoredValues.normalize(left), StoredValues.normalize(right));
300
491
  },
301
492
  /**
302
493
  * Reads a dot-separated path from an object value.
494
+ * @param value Supplies the value to inspect.
495
+ * @param path Names the dot-separated property path.
496
+ * @returns The value at the path, or undefined when absent.
303
497
  */
304
498
  readPath(value, path) {
305
499
  let current = value;
@@ -312,6 +506,9 @@ const StoredValues = {
312
506
  },
313
507
  /**
314
508
  * Compares normalized values of the same or distinct kinds.
509
+ * @param left Supplies the first normalized value.
510
+ * @param right Supplies the second normalized value.
511
+ * @returns A negative, zero, or positive comparison result.
315
512
  */
316
513
  compareNormalized(left, right) {
317
514
  const leftKind = StoredValues.kind(left);
@@ -340,6 +537,9 @@ const StoredValues = {
340
537
  },
341
538
  /**
342
539
  * Compares normalized storage identities, treating text as canonical UTF-8 bytes.
540
+ * @param left Supplies the first normalized identity.
541
+ * @param right Supplies the second normalized identity.
542
+ * @returns A negative, zero, or positive comparison result.
343
543
  */
344
544
  compareIdentityNormalized(left, right) {
345
545
  const leftKind = StoredValues.kind(left);
@@ -368,6 +568,9 @@ const StoredValues = {
368
568
  },
369
569
  /**
370
570
  * Compares numbers while placing NaN after other numbers.
571
+ * @param left Supplies the first number.
572
+ * @param right Supplies the second number.
573
+ * @returns A negative, zero, or positive comparison result.
371
574
  */
372
575
  compareNumbers(left, right) {
373
576
  if (Number.isNaN(left) || Number.isNaN(right))
@@ -376,6 +579,9 @@ const StoredValues = {
376
579
  },
377
580
  /**
378
581
  * Compares encoded bigint payloads numerically.
582
+ * @param left Supplies the first encoded bigint.
583
+ * @param right Supplies the second encoded bigint.
584
+ * @returns A negative, zero, or positive comparison result.
379
585
  */
380
586
  compareBigInts(left, right) {
381
587
  const l = BigInt(left);
@@ -384,6 +590,10 @@ const StoredValues = {
384
590
  },
385
591
  /**
386
592
  * Compares lists lexicographically.
593
+ * @typeParam T List element type.
594
+ * @param left Supplies the first list.
595
+ * @param right Supplies the second list.
596
+ * @returns A negative, zero, or positive comparison result.
387
597
  */
388
598
  compareLists(left, right) {
389
599
  for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
@@ -397,6 +607,9 @@ const StoredValues = {
397
607
  },
398
608
  /**
399
609
  * Compares normalized objects by keys and then values.
610
+ * @param left Supplies the first normalized object.
611
+ * @param right Supplies the second normalized object.
612
+ * @returns A negative, zero, or positive comparison result.
400
613
  */
401
614
  compareObjects(left, right) {
402
615
  const leftKeys = Object.keys(left);
@@ -412,12 +625,18 @@ const StoredValues = {
412
625
  },
413
626
  /**
414
627
  * Compares strings in code-unit order.
628
+ * @param left Supplies the first string.
629
+ * @param right Supplies the second string.
630
+ * @returns A negative, zero, or positive comparison result.
415
631
  */
416
632
  compareText(left, right) {
417
633
  return left < right ? -1 : left > right ? 1 : 0;
418
634
  },
419
635
  /**
420
636
  * Compares identity-value lists recursively.
637
+ * @param left Supplies the first normalized identity list.
638
+ * @param right Supplies the second normalized identity list.
639
+ * @returns A negative, zero, or positive comparison result.
421
640
  */
422
641
  compareIdentityLists(left, right) {
423
642
  for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
@@ -429,6 +648,9 @@ const StoredValues = {
429
648
  },
430
649
  /**
431
650
  * Compares object-shaped identities by canonical keys and values.
651
+ * @param left Supplies the first normalized identity object.
652
+ * @param right Supplies the second normalized identity object.
653
+ * @returns A negative, zero, or positive comparison result.
432
654
  */
433
655
  compareIdentityObjects(left, right) {
434
656
  const leftKeys = Object.keys(left);
@@ -445,6 +667,8 @@ const StoredValues = {
445
667
  },
446
668
  /**
447
669
  * Normalizes a value for canonical storage comparison.
670
+ * @param value Supplies the value to normalize.
671
+ * @returns The normalized value representation.
448
672
  */
449
673
  normalize(value) {
450
674
  if (typeof value === "bigint")
@@ -472,54 +696,39 @@ const StoredValues = {
472
696
  }, StoredValues.emptyObject());
473
697
  },
474
698
  /**
475
- * Encodes a normalized value without type collisions.
476
- */
477
- encode(value) {
478
- return JSON.stringify(StoredValues.encoded(value));
479
- },
480
- /**
481
- * Converts a normalized value to its tagged JSON representation.
699
+ * Converts a raw value to its tagged JSON representation.
700
+ * @param value Supplies the raw value.
701
+ * @returns The tagged JSON representation.
482
702
  */
483
703
  encoded(value) {
484
- const kind = StoredValues.kind(value);
485
- switch (kind) {
486
- case "undefined":
487
- return ["undefined"];
488
- case "null":
489
- return ["null"];
490
- case "boolean":
491
- if (typeof value !== "boolean")
492
- throw new Error("Normalized boolean value has an unexpected type.");
493
- return ["boolean", value];
494
- case "number":
495
- if (typeof value !== "number")
496
- throw new Error("Normalized number value has an unexpected type.");
497
- return ["number", String(value)];
498
- case "string":
499
- if (typeof value !== "string")
500
- throw new Error("Normalized string value has an unexpected type.");
501
- return ["string", value];
502
- case "bigint":
503
- return ["bigint", StoredValues.payload(value)];
504
- case "bytes":
505
- return ["bytes", StoredValues.payload(value)];
506
- case "array":
507
- return [
508
- "array",
509
- ...value.map((entry) => StoredValues.encoded(entry)),
510
- ];
511
- case "object":
512
- return [
513
- "object",
514
- ...Object.keys(value).map((key) => [
515
- key,
516
- StoredValues.encoded(value[key]),
517
- ]),
518
- ];
519
- }
704
+ if (value === undefined)
705
+ return ["undefined"];
706
+ if (value === null)
707
+ return ["null"];
708
+ if (typeof value === "boolean")
709
+ return ["boolean", value];
710
+ if (typeof value === "number")
711
+ return ["number", String(value)];
712
+ if (typeof value === "string")
713
+ return ["string", value];
714
+ if (typeof value === "bigint")
715
+ return ["bigint", value.toString()];
716
+ if (value instanceof Uint8Array)
717
+ return ["bytes", [...value]];
718
+ if (Array.isArray(value))
719
+ return ["array", ...value.map((entry) => StoredValues.encoded(entry))];
720
+ if (typeof value !== "object")
721
+ return ["undefined"];
722
+ // Evaluate fields in lexical order, then use native numeric-name enumeration.
723
+ const entries = Object.fromEntries(Object.keys(value)
724
+ .sort()
725
+ .map((key) => [key, StoredValues.encoded(Reflect.get(value, key))]));
726
+ return ["object", ...Object.entries(entries)];
520
727
  },
521
728
  /**
522
- * Identifies a normalized value kind.
729
+ * Returns the kind of a normalized value.
730
+ * @param value Supplies the normalized value.
731
+ * @returns The normalized value kind.
523
732
  */
524
733
  kind(value) {
525
734
  if (value === undefined)
@@ -539,6 +748,11 @@ const StoredValues = {
539
748
  },
540
749
  /**
541
750
  * Creates a frozen tagged normalized value.
751
+ * @typeParam K Tag kind.
752
+ * @typeParam P Payload type.
753
+ * @param kind Identifies the tag kind.
754
+ * @param payload Supplies the tag payload.
755
+ * @returns The frozen tagged value.
542
756
  */
543
757
  tagged(kind, payload) {
544
758
  const tagged = Object.create(null);
@@ -550,6 +764,8 @@ const StoredValues = {
550
764
  },
551
765
  /**
552
766
  * Reads a recognized normalized tag.
767
+ * @param value Supplies the object to inspect.
768
+ * @returns The recognized tag, or undefined for an ordinary object.
553
769
  */
554
770
  tag(value) {
555
771
  const tag = value[normalizedKind];
@@ -557,12 +773,16 @@ const StoredValues = {
557
773
  },
558
774
  /**
559
775
  * Creates an object with no prototype for normalized fields.
776
+ * @returns An empty object without a prototype.
560
777
  */
561
778
  emptyObject() {
562
779
  return Object.create(null);
563
780
  },
564
781
  /**
565
782
  * Reads a tagged normalized payload.
783
+ * @typeParam P Payload type.
784
+ * @param value Supplies the tagged value.
785
+ * @returns The tagged payload.
566
786
  */
567
787
  payload(value) {
568
788
  return value[normalizedPayload];