@spine-event-engine/storage-datastore 2.0.0-snapshot.16 → 2.0.0-snapshot.18

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.
@@ -27,6 +27,9 @@ const casRetryDelayMs = 100;
27
27
  const wrappedReadOptions = { wrapNumbers: true };
28
28
  /**
29
29
  * Private flat Datastore-entity codec for one record storage handle.
30
+ *
31
+ * @typeParam I Identifier type encoded in Datastore keys.
32
+ * @typeParam R Generated record message type encoded as payload bytes.
30
33
  */
31
34
  class FlatEntityCodec {
32
35
  recordSpec;
@@ -36,6 +39,18 @@ class FlatEntityCodec {
36
39
  #namespace;
37
40
  #idColumn;
38
41
  #columnMapping;
42
+ /**
43
+ * Initializes the Datastore kind, tenant namespace, ID codec, and column mapping.
44
+ *
45
+ * @param context Storage tenant context.
46
+ * @param recordSpec Generated record and column contract.
47
+ * @param client Datastore client supplying the default namespace.
48
+ * @param maxClientSideScan Maximum local materialization budget.
49
+ * @param group Optional storage group used in the physical kind.
50
+ * @param kind Optional explicit physical kind.
51
+ * @param namespaceConverter Converts tenant IDs to Datastore namespaces.
52
+ * @param stringifiers Converts message-valued IDs and columns.
53
+ */
39
54
  constructor(context, recordSpec, client, maxClientSideScan, group, kind, namespaceConverter, stringifiers) {
40
55
  this.recordSpec = recordSpec;
41
56
  this.maxClientSideScan = maxClientSideScan;
@@ -54,12 +69,34 @@ class FlatEntityCodec {
54
69
  this.#idColumn = new DatastoreIdColumn(recordSpec.idType, stringifiers);
55
70
  this.#columnMapping = new DatastoreColumnMapping(stringifiers);
56
71
  }
72
+ /**
73
+ * Builds a namespaced Datastore key for a record identifier.
74
+ *
75
+ * @param client Datastore client that creates keys.
76
+ * @param id Typed record identifier.
77
+ * @returns Physical key for this kind and namespace.
78
+ */
57
79
  key(client, id) {
58
80
  return client.key({
59
- path: [this.#kind, this.#idColumn.value(id)],
81
+ path: [this.#kind, this.idName(id)],
60
82
  ...(this.#namespace === undefined ? {} : { namespace: this.#namespace }),
61
83
  });
62
84
  }
85
+ /**
86
+ * Returns the canonical Datastore key name for a logical record ID.
87
+ * @param id Typed record identifier.
88
+ * @returns Key name shared by equivalent logical IDs.
89
+ */
90
+ idName(id) {
91
+ return this.#idColumn.value(id);
92
+ }
93
+ /**
94
+ * Encodes record bytes and declared materialized columns as entity properties.
95
+ *
96
+ * @param record Generated record to serialize.
97
+ * @param columns Materialized column values by declared name.
98
+ * @returns Datastore property map including the binary payload.
99
+ */
63
100
  encode(record, columns) {
64
101
  const data = {
65
102
  [payloadProperty]: Buffer.from(toBinary(this.recordSpec.recordType, record, { writeUnknownFields: false })),
@@ -69,6 +106,13 @@ class FlatEntityCodec {
69
106
  }
70
107
  return data;
71
108
  }
109
+ /**
110
+ * Decodes a record ID from an entity's physical key.
111
+ *
112
+ * @param entity Raw Datastore entity.
113
+ * @param client Client whose key symbol identifies the entity key.
114
+ * @returns Typed record identifier.
115
+ */
72
116
  id(entity, client) {
73
117
  const key = datastoreKey(entity, client.KEY);
74
118
  const name = datastoreKeyName(key);
@@ -77,36 +121,85 @@ class FlatEntityCodec {
77
121
  }
78
122
  return this.#idColumn.read(name);
79
123
  }
124
+ /**
125
+ * Reads declared column values from a decoded entity record.
126
+ *
127
+ * @param entity Raw Datastore entity.
128
+ * @returns Declared column values by name.
129
+ */
80
130
  columns(entity) {
81
131
  return new Map(this.recordSpec.columns.map((column) => [column.name, column.valueIn(this.decode(entity))]));
82
132
  }
133
+ /**
134
+ * Marks the binary record payload as excluded from Datastore indexes.
135
+ *
136
+ * @param data Entity property map being saved.
137
+ * @returns Payload property name to leave unindexed.
138
+ */
83
139
  unindexedProperties(data) {
84
140
  void data;
85
141
  return [payloadProperty];
86
142
  }
143
+ /**
144
+ * Creates a provider query for this handle's kind and namespace.
145
+ *
146
+ * @param client Datastore client that creates queries.
147
+ * @returns Query scoped to this record family.
148
+ */
87
149
  createQuery(client) {
88
150
  return this.#namespace === undefined
89
151
  ? client.createQuery(this.#kind)
90
152
  : client.createQuery(this.#namespace, this.#kind);
91
153
  }
154
+ /**
155
+ * Validates a declared column and returns its Datastore property name.
156
+ *
157
+ * @param column Requested record column name.
158
+ * @returns Datastore property name for the column.
159
+ */
92
160
  columnProperty(column) {
93
161
  if (!this.recordSpec.columns.some((candidate) => candidate.name === column)) {
94
162
  throw new Error(`Datastore record column "${column}" is not declared.`);
95
163
  }
96
164
  return column;
97
165
  }
166
+ /**
167
+ * Returns a row limit with one sentinel beyond the local scan budget.
168
+ *
169
+ * @returns Provider row limit used to detect budget overflow.
170
+ */
98
171
  queryLimit() {
99
172
  return this.maxClientSideScan + 1;
100
173
  }
174
+ /**
175
+ * Returns the tenant namespace selected for this handle.
176
+ *
177
+ * @returns Datastore namespace, if tenant scoping requires one.
178
+ */
101
179
  namespace() {
102
180
  return this.#namespace;
103
181
  }
182
+ /**
183
+ * Converts a declared record column value to its provider representation.
184
+ *
185
+ * @param column Declared record column name.
186
+ * @param value Runtime column value.
187
+ * @returns Datastore-compatible column value.
188
+ */
104
189
  columnValue(column, value) {
105
190
  const declared = this.recordSpec.columns.find((candidate) => candidate.name === column);
106
191
  if (declared === undefined)
107
192
  throw new Error(`Datastore record column "${column}" is not declared.`);
108
193
  return ColumnMappings.value(this.#columnMapping, declared.type, value);
109
194
  }
195
+ /**
196
+ * Captures ordered values and the physical key from the final page entity.
197
+ *
198
+ * @param entity Last raw Datastore entity in a page.
199
+ * @param order Provider ordering used for the page.
200
+ * @param keySymbol Client symbol for the physical entity key.
201
+ * @returns Stable keyset continuation for the next page.
202
+ */
110
203
  pageCursor(entity, order, keySymbol) {
111
204
  const key = datastoreKey(entity, keySymbol);
112
205
  const record = this.decode(entity);
@@ -122,6 +215,12 @@ class FlatEntityCodec {
122
215
  throw new Error("Datastore provider page continuation is malformed.");
123
216
  return { values, key };
124
217
  }
218
+ /**
219
+ * Decodes a generated record from an entity's binary payload.
220
+ *
221
+ * @param entity Raw Datastore entity containing the payload.
222
+ * @returns Generated record without unknown fields.
223
+ */
125
224
  decode(entity) {
126
225
  const payload = entity[payloadProperty];
127
226
  if (!(payload instanceof Uint8Array)) {
@@ -169,6 +268,9 @@ function cursorJson(_key, value) {
169
268
  }
170
269
  /**
171
270
  * Provides one Datastore-backed storage handle for a record family.
271
+ *
272
+ * @typeParam I Record identifier type used by this handle.
273
+ * @typeParam R Generated record message type stored by this handle.
172
274
  */
173
275
  export class DatastoreRecordStorage extends RecordStorage {
174
276
  client;
@@ -340,7 +442,7 @@ export class DatastoreRecordStorage extends RecordStorage {
340
442
  queryCapabilities() {
341
443
  return {
342
444
  comparisons: ["equal", "greaterThan", "lessThan", "greaterOrEqual", "lessOrEqual"],
343
- features: ["order", "mask", "limit"],
445
+ features: ["order", "limit"],
344
446
  };
345
447
  }
346
448
  /**
@@ -349,12 +451,79 @@ export class DatastoreRecordStorage extends RecordStorage {
349
451
  * @returns The candidate storage entries.
350
452
  */
351
453
  async queryPlanRecordEntries(plan) {
352
- if (plan.predicate !== undefined && DatastoreQueryPushdown.legal(plan) === undefined)
454
+ const legal = DatastoreQueryPushdown.legal(plan);
455
+ const largeIds = plan.exhaustive && plan.predicate !== undefined
456
+ ? DatastoreQueryPushdown.largeIdSet(plan.predicate, plan.order)
457
+ : undefined;
458
+ if (plan.predicate !== undefined && legal === undefined && largeIds === undefined)
353
459
  throw new TypeError("Datastore normalized query has an illegal predicate or inequality ordering.");
354
460
  for (const order of plan.order ?? [])
355
461
  this.#codec.columnProperty(order.column);
356
- const query = this.#codec.createQuery(this.client);
357
- DatastoreQueryPushdown.plan(query, plan, (id) => this.#codec.key(this.client, id), (column) => this.#codec.columnProperty(column), (column, value) => this.#codec.columnValue(column, value));
462
+ if (plan.exhaustive) {
463
+ if (largeIds !== undefined)
464
+ return this.largeIdPlanEntries(largeIds);
465
+ return this.exhaustivePlanEntries(plan, true);
466
+ }
467
+ return this.boundedPlanEntries(plan);
468
+ }
469
+ /**
470
+ * Reads a mandatory large ID set through provider-legal key batches.
471
+ *
472
+ * @param ids Explicit IDs that every matching record must have.
473
+ * @returns Unique candidates for global predicate and ordering evaluation.
474
+ */
475
+ async largeIdPlanEntries(ids) {
476
+ const unique = new Map();
477
+ for (const id of ids)
478
+ unique.set(this.#codec.idName(id), id);
479
+ const selected = [...unique.values()];
480
+ const entries = [];
481
+ for (let start = 0; start < selected.length; start += 30) {
482
+ const batch = selected.slice(start, start + 30);
483
+ entries.push(...(await this.exhaustivePlanEntries({ exhaustive: true, predicate: { kind: "ids", ids: batch } }, true)));
484
+ }
485
+ return entries;
486
+ }
487
+ /**
488
+ * Reads every provider page before storage applies the global query semantics.
489
+ *
490
+ * @param plan Validated normalized query plan.
491
+ * @param pushdown Whether the provider can apply the predicate and ordering.
492
+ * @returns Every candidate entry in provider page order.
493
+ */
494
+ async exhaustivePlanEntries(plan, pushdown) {
495
+ const entries = [];
496
+ let cursor;
497
+ const seen = new Set();
498
+ for (;;) {
499
+ const query = this.planQuery(plan, pushdown);
500
+ query.limit(128);
501
+ if (cursor !== undefined)
502
+ query.start(cursor);
503
+ const response = await this.provider(() => this.client.runQuery(query, wrappedReadOptions));
504
+ entries.push(...DatastoreResults.entities(response).map((entity) => ({
505
+ id: this.#codec.id(entity, this.client),
506
+ record: this.#codec.decode(entity),
507
+ })));
508
+ const info = DatastoreResults.queryInfo(response);
509
+ if (!info.more)
510
+ return entries;
511
+ const next = info.cursor;
512
+ const key = typeof next === "string" ? next : next.toString("base64");
513
+ if (seen.has(key))
514
+ throw new Error("Datastore query continuation did not advance.");
515
+ seen.add(key);
516
+ cursor = next;
517
+ }
518
+ }
519
+ /**
520
+ * Reads one bounded provider result for public and Process Manager plans.
521
+ *
522
+ * @param plan Validated normalized query plan.
523
+ * @returns Candidate entries within the provider's configured bound.
524
+ */
525
+ async boundedPlanEntries(plan) {
526
+ const query = this.planQuery(plan, true);
358
527
  const providerLimit = Math.min(this.#codec.queryLimit(), plan.limit ?? Number.POSITIVE_INFINITY, (plan.candidateLimit ?? defaultQueryCandidateLimit) + 1);
359
528
  query.limit(providerLimit);
360
529
  const entities = DatastoreResults.entities(await this.provider(() => this.client.runQuery(query, wrappedReadOptions)));
@@ -366,6 +535,21 @@ export class DatastoreRecordStorage extends RecordStorage {
366
535
  record: this.#codec.decode(entity),
367
536
  }));
368
537
  }
538
+ /**
539
+ * Builds one Datastore query with safe predicate and order pushdown.
540
+ *
541
+ * @param plan Normalized query plan to translate.
542
+ * @param pushdown Whether the plan is legal for provider filtering.
543
+ * @returns Query without a page bound or cursor.
544
+ */
545
+ planQuery(plan, pushdown) {
546
+ const query = this.#codec.createQuery(this.client);
547
+ if (pushdown)
548
+ DatastoreQueryPushdown.plan(query, plan, (id) => this.#codec.key(this.client, id), (column) => this.#codec.columnProperty(column), (column, value) => this.#codec.columnValue(column, value));
549
+ if (plan.exhaustive && (!pushdown || (plan.order?.length ?? 0) === 0))
550
+ query.order("__key__");
551
+ return query;
552
+ }
369
553
  /**
370
554
  * Reads the record at one storage slot.
371
555
  * @param id The storage slot identifier.
@@ -397,6 +581,14 @@ export class DatastoreRecordStorage extends RecordStorage {
397
581
  }
398
582
  throw new Error("Datastore compare-and-set retry limit was reached.");
399
583
  }
584
+ /**
585
+ * Executes one transactional compare-and-set attempt against the stored payload.
586
+ *
587
+ * @param id Record slot selected for mutation.
588
+ * @param expected Required prior materialized record, or absence.
589
+ * @param next Replacement materialized record, or deletion.
590
+ * @returns True when the expected payload matched and commit completed.
591
+ */
400
592
  async attemptCompareAndSet(id, expected, next) {
401
593
  const key = this.#codec.key(this.client, id);
402
594
  const transaction = this.transaction();
@@ -458,6 +650,13 @@ export class DatastoreRecordStorage extends RecordStorage {
458
650
  excludeFromIndexes: this.#codec.unindexedProperties(data),
459
651
  }));
460
652
  }
653
+ /**
654
+ * Executes a provider operation and hides provider error details from callers.
655
+ *
656
+ * @typeParam T Result produced by the provider operation.
657
+ * @param operation Provider request to execute.
658
+ * @returns Provider result after successful execution.
659
+ */
461
660
  async provider(operation) {
462
661
  try {
463
662
  return await operation();
@@ -481,24 +680,36 @@ export class DatastoreRecordStorage extends RecordStorage {
481
680
  }
482
681
  }
483
682
  /**
484
- * Converts values between Spine records and the Datastore provider representation.
683
+ * Compares decoded query values and persisted payload bytes.
485
684
  */
486
- const RecordValues = Object.freeze(new (class {
685
+ class RecordValueMethods {
487
686
  // prettier-ignore
488
687
  /**
489
688
  * Compares canonical local values for equality.
689
+ *
690
+ * @param left First decoded query value.
691
+ * @param right Second decoded query value.
692
+ * @returns True when the query evaluator considers the values equal.
490
693
  */
491
694
  equal(left, right) {
492
695
  return StorageQueryValues.equal(left, right);
493
696
  }
494
697
  /**
495
698
  * Compares canonical local values for ordering.
699
+ *
700
+ * @param left First decoded query value.
701
+ * @param right Second decoded query value.
702
+ * @returns Signed query sort comparison.
496
703
  */
497
704
  compare(left, right) {
498
705
  return StorageQueryValues.compare(left, right);
499
706
  }
500
707
  /**
501
708
  * Compares two persisted payload byte arrays.
709
+ *
710
+ * @param left First persisted payload candidate.
711
+ * @param right Second persisted payload candidate.
712
+ * @returns True only when both are byte arrays with identical bytes.
502
713
  */
503
714
  payloadEqual(left, right) {
504
715
  return (left instanceof Uint8Array &&
@@ -506,7 +717,11 @@ const RecordValues = Object.freeze(new (class {
506
717
  left.byteLength === right.byteLength &&
507
718
  left.every((value, index) => value === right[index]));
508
719
  }
509
- })());
720
+ }
721
+ /**
722
+ * Exposes comparisons for decoded query values and persisted payload bytes.
723
+ */
724
+ const RecordValues = Object.freeze(new RecordValueMethods());
510
725
  /**
511
726
  * Raised when a query would exceed the adapter's configured finite client-side scan budget.
512
727
  */
@@ -531,6 +746,13 @@ class DatastoreQueryPushdownHelper {
531
746
  // prettier-ignore
532
747
  /**
533
748
  * Translates the legacy record-query surface.
749
+ *
750
+ * @typeParam I Record identifier type used by the query.
751
+ * @param query Mutable Datastore query receiving filters and ordering.
752
+ * @param recordQuery Legacy record query to translate.
753
+ * @param keyFor Converts a record ID to a Datastore key.
754
+ * @param columnProperty Resolves a declared column to a property name.
755
+ * @param columnValue Converts a column value for the provider.
534
756
  */
535
757
  translate(query, recordQuery, keyFor, columnProperty, columnValue) {
536
758
  if (recordQuery.ids !== undefined && recordQuery.ids.length > 0) {
@@ -560,6 +782,13 @@ class DatastoreQueryPushdownHelper {
560
782
  }
561
783
  /**
562
784
  * Applies the all-or-nothing legal portion of a normalized query plan.
785
+ *
786
+ * @typeParam I Record identifier type used by the plan.
787
+ * @param query Mutable Datastore query receiving safe clauses.
788
+ * @param plan Normalized plan whose predicate and ordering are inspected.
789
+ * @param keyFor Converts a record ID to a Datastore key.
790
+ * @param columnProperty Resolves a declared column to a property name.
791
+ * @param columnValue Converts a column value for the provider.
563
792
  */
564
793
  plan(query, plan, keyFor, columnProperty, columnValue) {
565
794
  const legal = this.legal(plan);
@@ -576,6 +805,10 @@ class DatastoreQueryPushdownHelper {
576
805
  }
577
806
  /**
578
807
  * Returns the predicate only when every clause satisfies provider restrictions.
808
+ *
809
+ * @typeParam I Record identifier type in ID predicates.
810
+ * @param plan Normalized plan checked for supported predicate shape and ordering.
811
+ * @returns Predicate when Datastore can execute it completely.
579
812
  */
580
813
  legal(plan) {
581
814
  const predicate = plan.predicate;
@@ -605,8 +838,40 @@ class DatastoreQueryPushdownHelper {
605
838
  }
606
839
  return predicate;
607
840
  }
841
+ /**
842
+ * Finds a mandatory ID set too wide for one Datastore IN filter.
843
+ *
844
+ * @typeParam I Identifier type in the predicate.
845
+ * @param predicate Predicate checked for a mandatory large ID set.
846
+ * @param order Ordering checked against any inequality comparison.
847
+ * @returns A required large ID set, or undefined for other shapes.
848
+ */
849
+ largeIdSet(predicate, order) {
850
+ const leaves = predicate.kind === "all" ? predicate.predicates : [predicate];
851
+ if (leaves.some((leaf) => leaf.kind !== "ids" && leaf.kind !== "comparison"))
852
+ return undefined;
853
+ const large = leaves.find((leaf) => leaf.kind === "ids" && leaf.ids.length > 30);
854
+ if (large?.kind !== "ids")
855
+ return undefined;
856
+ const narrowed = leaves.map((leaf) => leaf.kind === "ids" && leaf.ids.length > 30 ? { ...leaf, ids: leaf.ids.slice(0, 1) } : leaf);
857
+ const representative = predicate.kind === "all"
858
+ ? { kind: "all", predicates: narrowed }
859
+ : { kind: "ids", ids: large.ids.slice(0, 1) };
860
+ const legal = this.legal({
861
+ predicate: representative,
862
+ ...(order === undefined ? {} : { order }),
863
+ });
864
+ return legal === undefined ? undefined : large.ids;
865
+ }
608
866
  /**
609
867
  * Adds one already-legal normalized predicate to the provider query.
868
+ *
869
+ * @typeParam I Record identifier type in ID predicates.
870
+ * @param query Mutable Datastore query receiving provider filters.
871
+ * @param predicate Normalized predicate approved for provider execution.
872
+ * @param keyFor Converts a record ID to a Datastore key.
873
+ * @param columnProperty Resolves a declared column to a property name.
874
+ * @param columnValue Converts a comparison value for the provider.
610
875
  */
611
876
  predicate(query, predicate, keyFor, columnProperty, columnValue) {
612
877
  if (predicate.kind === "all") {
@@ -645,13 +910,14 @@ class DatastoreQueryPushdownHelper {
645
910
  */
646
911
  const DatastoreQueryPushdown = Object.freeze(new DatastoreQueryPushdownHelper());
647
912
  /**
648
- * Selects safe provider bounds for direct record queries.
913
+ * Selects safe provider bounds for direct legacy record queries.
649
914
  */
650
- const DatastoreRecordQuery = Object.freeze(new (class {
915
+ class DatastoreRecordQueryMethods {
651
916
  // prettier-ignore
652
917
  /**
653
918
  * Returns a caller limit only when it cannot change local query semantics.
654
919
  *
920
+ * @typeParam I Record identifier type in the legacy query.
655
921
  * @param query Supplies the requested record query.
656
922
  * @param scanLimit Supplies the configured materialization limit.
657
923
  * @returns The finite provider row bound.
@@ -672,6 +938,7 @@ const DatastoreRecordQuery = Object.freeze(new (class {
672
938
  /**
673
939
  * Returns whether a legacy query is exactly an ascending identifier keyset page.
674
940
  *
941
+ * @typeParam I Record identifier type in the legacy query.
675
942
  * @param query The record query to classify.
676
943
  * @returns `true` when Datastore can apply its limit and continuation completely.
677
944
  */
@@ -691,7 +958,11 @@ const DatastoreRecordQuery = Object.freeze(new (class {
691
958
  continuationValue?.field === "id" &&
692
959
  RecordValues.equal(continuationValue.value, continuation.id))));
693
960
  }
694
- })());
961
+ }
962
+ /**
963
+ * Provides the bounded legacy record-query checks used by this adapter.
964
+ */
965
+ const DatastoreRecordQuery = Object.freeze(new DatastoreRecordQueryMethods());
695
966
  /**
696
967
  * Applies record-query filtering, ordering, continuation, and paging locally.
697
968
  */
@@ -699,6 +970,12 @@ class LocalQueryResultsHelper {
699
970
  // prettier-ignore
700
971
  /**
701
972
  * Applies one legacy record query to materialized candidate entries.
973
+ *
974
+ * @typeParam I Record identifier type in the candidates.
975
+ * @typeParam R Generated record message type in the candidates.
976
+ * @param entries Materialized candidates with declared column values.
977
+ * @param query Legacy filter, ordering, continuation, and paging request.
978
+ * @returns Selected record entries after local query evaluation.
702
979
  */
703
980
  apply(entries, query) {
704
981
  const matching = entries.filter((entry) => this.matches(entry, query));
@@ -714,6 +991,12 @@ class LocalQueryResultsHelper {
714
991
  }
715
992
  /**
716
993
  * Tests whether a candidate satisfies the ID and equality filters.
994
+ *
995
+ * @typeParam I Record identifier type in the candidate.
996
+ * @typeParam R Generated record message type in the candidate.
997
+ * @param entry Candidate record with materialized columns.
998
+ * @param query Legacy ID and column filters.
999
+ * @returns True when the candidate satisfies every requested filter.
717
1000
  */
718
1001
  matches(entry, query) {
719
1002
  return ((query.ids === undefined ||
@@ -726,7 +1009,14 @@ class LocalQueryResultsHelper {
726
1009
  }));
727
1010
  }
728
1011
  /**
729
- * Orders candidates by requested sort fields and canonical ID tie-breaker.
1012
+ * Compares candidates by requested sort fields and canonical ID tie-breaker.
1013
+ *
1014
+ * @typeParam I Record identifier type in the candidates.
1015
+ * @typeParam R Generated record message type in the candidates.
1016
+ * @param left First candidate entry.
1017
+ * @param right Second candidate entry.
1018
+ * @param orders Requested field order in priority order.
1019
+ * @returns Signed sort comparison with ID as final tie-breaker.
730
1020
  */
731
1021
  order(left, right, orders) {
732
1022
  for (const order of orders) {
@@ -738,6 +1028,13 @@ class LocalQueryResultsHelper {
738
1028
  }
739
1029
  /**
740
1030
  * Compares a candidate with a normalized continuation cursor.
1031
+ *
1032
+ * @typeParam I Record identifier type in the candidate and cursor.
1033
+ * @typeParam R Generated record message type in the candidate.
1034
+ * @param entry Candidate record with materialized columns.
1035
+ * @param orders Requested field order used by the cursor.
1036
+ * @param after Continuation cursor from the preceding page.
1037
+ * @returns Signed comparison after applying order directions and ID tie-breaker.
741
1038
  */
742
1039
  continuation(entry, orders, after) {
743
1040
  for (let index = 0; index < orders.length; index += 1) {
@@ -751,7 +1048,13 @@ class LocalQueryResultsHelper {
751
1048
  return RecordValues.compare(entry.id, after.id);
752
1049
  }
753
1050
  /**
754
- * Obtains an ID or indexed column value from a candidate.
1051
+ * Reads an ID or indexed column value from a candidate.
1052
+ *
1053
+ * @typeParam I Record identifier type in the candidate.
1054
+ * @typeParam R Generated record message type in the candidate.
1055
+ * @param entry Candidate record with materialized columns.
1056
+ * @param field ID or declared column name.
1057
+ * @returns Candidate ID or stored column value.
755
1058
  */
756
1059
  value(entry, field) {
757
1060
  return field === "id" ? entry.id : entry.columns.get(field);
@@ -767,14 +1070,20 @@ const LocalQueryResults = Object.freeze(new LocalQueryResultsHelper());
767
1070
  class DatastoreTransactionsHelper {
768
1071
  // prettier-ignore
769
1072
  /**
770
- * Redacts credential-like errors while preserving safe provider failures.
1073
+ * Replaces transaction errors with a stable public failure message.
1074
+ *
1075
+ * @param error Provider failure whose details must be hidden.
1076
+ * @returns New error without provider details.
771
1077
  */
772
1078
  redact(error) {
773
1079
  void error;
774
1080
  return new Error("Datastore transaction failed.");
775
1081
  }
776
1082
  /**
777
- * Identifies a safe-to-retry Datastore transaction conflict.
1083
+ * Checks for a safe-to-retry Datastore transaction conflict.
1084
+ *
1085
+ * @param error Provider failure to classify.
1086
+ * @returns True for non-sensitive Datastore conflict code 10.
778
1087
  */
779
1088
  retry(error) {
780
1089
  return (error instanceof Error &&
@@ -783,6 +1092,9 @@ class DatastoreTransactionsHelper {
783
1092
  }
784
1093
  /**
785
1094
  * Waits using exponential jitter before a bounded CAS retry.
1095
+ *
1096
+ * @param attempt Zero-based retry attempt used for backoff.
1097
+ * @returns Completes after the randomized delay.
786
1098
  */
787
1099
  async wait(attempt) {
788
1100
  const exponentialDelay = casRetryDelayMs * 2 ** attempt;
@@ -800,7 +1112,10 @@ const DatastoreTransactions = Object.freeze(new DatastoreTransactionsHelper());
800
1112
  class DatastoreResultsHelper {
801
1113
  // prettier-ignore
802
1114
  /**
803
- * Extracts the optional first entity from a Datastore read response.
1115
+ * Reads the optional first entity from a Datastore read response.
1116
+ *
1117
+ * @param response Raw provider read response.
1118
+ * @returns First validated entity, if a row was returned.
804
1119
  */
805
1120
  first(response) {
806
1121
  if (!Array.isArray(response))
@@ -809,7 +1124,10 @@ class DatastoreResultsHelper {
809
1124
  return entity === undefined ? undefined : this.entity(entity);
810
1125
  }
811
1126
  /**
812
- * Extracts entity rows from a Datastore query response.
1127
+ * Reads entity rows from a Datastore query response.
1128
+ *
1129
+ * @param response Raw provider query response.
1130
+ * @returns Validated entity objects in provider order.
813
1131
  */
814
1132
  entities(response) {
815
1133
  if (!Array.isArray(response) || !Array.isArray(response[0])) {
@@ -818,7 +1136,10 @@ class DatastoreResultsHelper {
818
1136
  return response[0].map((value) => this.entity(value));
819
1137
  }
820
1138
  /**
821
- * Extracts the provider continuation and the remaining-row indication.
1139
+ * Reads the provider continuation and the remaining-row indication.
1140
+ *
1141
+ * @param response Raw provider query response.
1142
+ * @returns Validated cursor and whether another page exists.
822
1143
  */
823
1144
  queryInfo(response) {
824
1145
  if (!Array.isArray(response) || typeof response[1] !== "object" || response[1] === null)
@@ -831,15 +1152,21 @@ class DatastoreResultsHelper {
831
1152
  moreResults !== "MORE_RESULTS_AFTER_CURSOR")
832
1153
  throw new Error("Datastore returned an invalid query response.");
833
1154
  const more = moreResults === "MORE_RESULTS_AFTER_LIMIT" || moreResults === "MORE_RESULTS_AFTER_CURSOR";
834
- if (more && !(cursor instanceof Buffer || typeof cursor === "string"))
835
- throw new Error("Datastore returned an invalid query response.");
1155
+ if (more) {
1156
+ if (!(cursor instanceof Buffer || typeof cursor === "string"))
1157
+ throw new Error("Datastore returned an invalid query response.");
1158
+ return { cursor, more: true };
1159
+ }
836
1160
  return {
837
1161
  cursor: cursor instanceof Buffer || typeof cursor === "string" ? cursor : undefined,
838
- more,
1162
+ more: false,
839
1163
  };
840
1164
  }
841
1165
  /**
842
1166
  * Validates one provider entity object.
1167
+ *
1168
+ * @param value Candidate entity returned by Datastore.
1169
+ * @returns Record-like entity object.
843
1170
  */
844
1171
  entity(value) {
845
1172
  if (typeof value !== "object" || value === null) {