@peerbit/document 13.0.44 → 13.1.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/src/search.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  type AbstractType,
3
+ FixedArrayKind,
3
4
  field,
4
5
  getSchema,
5
6
  serialize,
@@ -14,6 +15,12 @@ import {
14
15
  sha256Base64Sync,
15
16
  } from "@peerbit/crypto";
16
17
  import * as types from "@peerbit/document-interface";
18
+ import {
19
+ type SimpleDocumentFieldExtractionPlan,
20
+ type SimpleDocumentProjectionContext,
21
+ type SimpleDocumentProjectionPlan,
22
+ tryProjectDocumentIndexSimple,
23
+ } from "./native-rust.js";
17
24
  import { CachedIndex, type QueryCacheOptions } from "@peerbit/indexer-cache";
18
25
  import * as indexerTypes from "@peerbit/indexer-interface";
19
26
  import { HashmapIndex } from "@peerbit/indexer-simple";
@@ -58,6 +65,15 @@ import {
58
65
  isResults,
59
66
  } from "./result-shape.js";
60
67
  import { ResumableIterators } from "./resumable-iterator.js";
68
+ import {
69
+ canPrepareDocumentTransformBeforeAppend,
70
+ canPrepareDocumentTransformWithAppendFacts,
71
+ documentTransformPreservesFieldPath,
72
+ getDocumentTransformDescriptor,
73
+ type DocumentTransformFacts,
74
+ type DocumentTransformDescriptor,
75
+ type DocumentTransformer,
76
+ } from "./transform.js";
61
77
 
62
78
  const WARNING_WHEN_ITERATING_FOR_MORE_THAN = 1e5;
63
79
 
@@ -70,6 +86,191 @@ const indexCacheLogger = documentIndexLogger.newScope("cache");
70
86
  const indexPrefetchLogger = documentIndexLogger.newScope("prefetch");
71
87
  const indexIteratorLogger = documentIndexLogger.newScope("iterate");
72
88
 
89
+ const isPromiseLike = <T>(value: MaybePromise<T>): value is Promise<T> =>
90
+ !!value && typeof (value as Promise<T>).then === "function";
91
+
92
+ const schemaVariant = (
93
+ schema: ReturnType<typeof getSchema>,
94
+ ): { type?: "u8" | "string"; value?: string } | undefined => {
95
+ const variant = schema?.variant;
96
+ if (variant == null) {
97
+ return {};
98
+ }
99
+ if (typeof variant === "number") {
100
+ return { type: "u8", value: String(variant) };
101
+ }
102
+ if (typeof variant === "string") {
103
+ return { type: "string", value: variant };
104
+ }
105
+ return;
106
+ };
107
+
108
+ const schemaTypeName = (type: unknown): string | undefined => {
109
+ if (typeof type === "string") {
110
+ switch (type) {
111
+ case "string":
112
+ case "u8":
113
+ case "u32":
114
+ case "u64":
115
+ case "bool":
116
+ return type;
117
+ default:
118
+ return;
119
+ }
120
+ }
121
+ if (type === Uint8Array) {
122
+ return "bytes";
123
+ }
124
+ if (type === PublicSignKey) {
125
+ return "publicsignkey";
126
+ }
127
+ if (type instanceof FixedArrayKind && type.elementType === "u8") {
128
+ return `fixedbytes:${type.length}`;
129
+ }
130
+ const kind = type as {
131
+ constructor?: { name?: string };
132
+ elementType?: unknown;
133
+ sizeEncoding?: string;
134
+ };
135
+ if (kind.constructor?.name === "OptionKind") {
136
+ const element = schemaTypeName(kind.elementType);
137
+ return element ? `option:${element}` : undefined;
138
+ }
139
+ if (kind.constructor?.name === "VecKind") {
140
+ const element = schemaTypeName(kind.elementType);
141
+ return kind.sizeEncoding === "u32" && element
142
+ ? `vec:${element}`
143
+ : undefined;
144
+ }
145
+ return;
146
+ };
147
+
148
+ const schemaFieldPlan = (
149
+ schema: ReturnType<typeof getSchema>,
150
+ ): { names: string[]; types: string[] } | undefined => {
151
+ if (!schema?.fields) {
152
+ return;
153
+ }
154
+ const names: string[] = [];
155
+ const types: string[] = [];
156
+ for (const field of schema.fields) {
157
+ const type = schemaTypeName(field.type);
158
+ if (!type) {
159
+ return;
160
+ }
161
+ names.push(field.key);
162
+ types.push(type);
163
+ }
164
+ return { names, types };
165
+ };
166
+
167
+ const asSingleFieldPath = (
168
+ path: string | readonly string[],
169
+ ): string | undefined =>
170
+ typeof path === "string" ? path : path.length === 1 ? path[0] : undefined;
171
+
172
+ const createSimpleProjectionPlan = (
173
+ documentSchema: ReturnType<typeof getSchema>,
174
+ indexedSchema: ReturnType<typeof getSchema>,
175
+ descriptor: DocumentTransformDescriptor | undefined,
176
+ ): SimpleDocumentProjectionPlan | undefined => {
177
+ if (!descriptor || descriptor.kind === "identity") {
178
+ return;
179
+ }
180
+ const documentVariant = schemaVariant(documentSchema);
181
+ const outputVariant = schemaVariant(indexedSchema);
182
+ const documentFields = schemaFieldPlan(documentSchema);
183
+ const outputFields = schemaFieldPlan(indexedSchema);
184
+ if (!documentVariant || !outputVariant || !documentFields || !outputFields) {
185
+ return;
186
+ }
187
+ const sources = new Map<string, { kind: string; value: string }>();
188
+ if (descriptor.kind === "pick") {
189
+ for (const field of descriptor.fields) {
190
+ const name = asSingleFieldPath(field);
191
+ if (!name) {
192
+ return;
193
+ }
194
+ sources.set(name, { kind: "field", value: name });
195
+ }
196
+ } else {
197
+ for (const field of descriptor.fields) {
198
+ const target = asSingleFieldPath(field.target);
199
+ if (!target) {
200
+ return;
201
+ }
202
+ switch (field.source.kind) {
203
+ case "field": {
204
+ const source = asSingleFieldPath(field.source.path);
205
+ if (!source) {
206
+ return;
207
+ }
208
+ sources.set(target, { kind: "field", value: source });
209
+ break;
210
+ }
211
+ case "context":
212
+ sources.set(target, {
213
+ kind: "context",
214
+ value: field.source.field,
215
+ });
216
+ break;
217
+ case "entryFirstSignerPublicKey":
218
+ sources.set(target, {
219
+ kind: "entryFirstSignerPublicKey",
220
+ value: "",
221
+ });
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ const sourceKinds: string[] = [];
227
+ const sourceValues: string[] = [];
228
+ for (const field of outputFields.names) {
229
+ const source = sources.get(field);
230
+ if (!source) {
231
+ return;
232
+ }
233
+ sourceKinds.push(source.kind);
234
+ sourceValues.push(source.value);
235
+ }
236
+ return {
237
+ documentVariantType: documentVariant.type,
238
+ documentVariantValue: documentVariant.value,
239
+ documentFieldNames: documentFields.names,
240
+ documentFieldTypes: documentFields.types,
241
+ outputVariantType: outputVariant.type,
242
+ outputVariantValue: outputVariant.value,
243
+ outputFieldTypes: outputFields.types,
244
+ sourceKinds,
245
+ sourceValues,
246
+ };
247
+ };
248
+
249
+ const createSimpleFieldExtractionPlan = (
250
+ documentSchema: ReturnType<typeof getSchema>,
251
+ path: string | readonly string[],
252
+ ): SimpleDocumentFieldExtractionPlan | undefined => {
253
+ const fieldName = asSingleFieldPath(path);
254
+ if (!fieldName) {
255
+ return;
256
+ }
257
+ const documentVariant = schemaVariant(documentSchema);
258
+ const documentFields = schemaFieldPlan(documentSchema);
259
+ if (!documentVariant || !documentFields) {
260
+ return;
261
+ }
262
+ if (!documentFields.names.includes(fieldName)) {
263
+ return;
264
+ }
265
+ return {
266
+ documentVariantType: documentVariant.type,
267
+ documentVariantValue: documentVariant.value,
268
+ documentFieldNames: documentFields.names,
269
+ documentFieldTypes: documentFields.types,
270
+ fieldName,
271
+ };
272
+ };
273
+
73
274
  type BufferedResult<T, I extends Record<string, any>> = {
74
275
  value: T;
75
276
  indexed: WithContext<I>;
@@ -282,7 +483,11 @@ export type SearchOptions<
282
483
  Resolve extends boolean | undefined,
283
484
  > = QueryOptions<T, I, D, Resolve>;
284
485
 
285
- type Transformer<T, I> = (obj: T, context: types.Context) => MaybePromise<I>;
486
+ type Transformer<T, I> = (
487
+ obj: T,
488
+ context: types.Context,
489
+ facts?: DocumentTransformFacts,
490
+ ) => MaybePromise<I>;
286
491
 
287
492
  export type ResultsIterator<T> = {
288
493
  close: () => Promise<void>;
@@ -593,7 +798,7 @@ export type TransformerAsConstructor<T, I> = {
593
798
 
594
799
  export type TransformerAsFunction<T, I> = {
595
800
  type: AbstractType<I>;
596
- transform: (arg: T, context: types.Context) => I | Promise<I>;
801
+ transform: DocumentTransformer<T, I>;
597
802
  };
598
803
  export type TransformOptions<T, I> =
599
804
  | TransformerAsConstructor<T, I>
@@ -654,6 +859,7 @@ export type OpenOptions<
654
859
  maybeOpen: (value: T & Program) => Promise<T & Program>;
655
860
  prefetch?: boolean | Partial<PrefetchOptions>;
656
861
  includeIndexed?: boolean; // if true, indexed representations will always be included in the search results
862
+ immutable?: boolean; // conflict rule of the owning store: oldest version wins when true (mirrors Documents "immutable")
657
863
  };
658
864
 
659
865
  type IndexableClass<I> = new (
@@ -661,6 +867,151 @@ type IndexableClass<I> = new (
661
867
  context: types.Context,
662
868
  ) => WithContext<I>;
663
869
 
870
+ type ContextualPutOptions = {
871
+ replace?: boolean;
872
+ encodedValue?: Uint8Array;
873
+ encodedValueParts?: {
874
+ prefix: Uint8Array;
875
+ suffix: Uint8Array;
876
+ };
877
+ transformFacts?: DocumentTransformFacts;
878
+ };
879
+
880
+ const stripEncodedValue = (
881
+ options: ContextualPutOptions | undefined,
882
+ ): { replace?: boolean } | undefined =>
883
+ options?.encodedValue || options?.encodedValueParts || options?.transformFacts
884
+ ? { replace: options.replace }
885
+ : options;
886
+
887
+ const writeU32Le = (target: Uint8Array, offset: number, value: number) => {
888
+ target[offset] = value & 0xff;
889
+ target[offset + 1] = (value >>> 8) & 0xff;
890
+ target[offset + 2] = (value >>> 16) & 0xff;
891
+ target[offset + 3] = (value >>> 24) & 0xff;
892
+ return offset + 4;
893
+ };
894
+
895
+ const writeU64Le = (target: Uint8Array, offset: number, value: bigint) => {
896
+ let remaining = value;
897
+ for (let i = 0; i < 8; i++) {
898
+ target[offset + i] = Number(remaining & 0xffn);
899
+ remaining >>= 8n;
900
+ }
901
+ return offset + 8;
902
+ };
903
+
904
+ export const encodeContextSuffix = (context: types.Context): Uint8Array => {
905
+ const head = fromString(context.head);
906
+ const gid = fromString(context.gid);
907
+ const encoded = new Uint8Array(
908
+ 1 + 8 + 8 + 4 + head.byteLength + 4 + gid.byteLength + 4,
909
+ );
910
+ let offset = 0;
911
+ // Context is @variant(0); keep this byte-for-byte aligned with Borsh.
912
+ encoded[offset++] = 0;
913
+ offset = writeU64Le(encoded, offset, context.created);
914
+ offset = writeU64Le(encoded, offset, context.modified);
915
+ offset = writeU32Le(encoded, offset, head.byteLength);
916
+ encoded.set(head, offset);
917
+ offset += head.byteLength;
918
+ offset = writeU32Le(encoded, offset, gid.byteLength);
919
+ encoded.set(gid, offset);
920
+ offset += gid.byteLength;
921
+ writeU32Le(encoded, offset, context.size);
922
+ return encoded;
923
+ };
924
+
925
+ type ContextualIndexPut<I> = {
926
+ putWithContext?: (
927
+ value: I,
928
+ id: indexerTypes.IdKey,
929
+ context: types.Context,
930
+ options?: ContextualPutOptions,
931
+ ) => Promise<void> | void;
932
+ putStoredContextualEncodedValue?: (
933
+ id: indexerTypes.IdKey,
934
+ encodedValueParts: {
935
+ prefix: Uint8Array;
936
+ suffix: Uint8Array;
937
+ },
938
+ options?: { replace?: boolean },
939
+ ) => Promise<void> | void | false;
940
+ persistStoredContextualEncodedValue?: (
941
+ id: indexerTypes.IdKey,
942
+ encodedValueParts: {
943
+ prefix: Uint8Array;
944
+ suffix: Uint8Array;
945
+ },
946
+ options?: { replace?: boolean },
947
+ ) => Promise<void> | void | false;
948
+ putStoredContextualEncodedValueBatch?: (
949
+ values: Array<{
950
+ id: indexerTypes.IdKey;
951
+ encodedValueParts: {
952
+ prefix: Uint8Array;
953
+ suffix: Uint8Array;
954
+ };
955
+ options?: { replace?: boolean };
956
+ }>,
957
+ ) => Promise<boolean> | boolean;
958
+ putWithContextBatch?: (
959
+ values: Array<{
960
+ value: I;
961
+ id: indexerTypes.IdKey;
962
+ context: types.Context;
963
+ options?: ContextualPutOptions;
964
+ }>,
965
+ ) => Promise<void> | void;
966
+ };
967
+
968
+ type ContextHeadIndex<I> = {
969
+ getByContextHead?: (
970
+ head: string,
971
+ ) => indexerTypes.IndexedResult<WithContext<I>> | undefined;
972
+ getByContextHeadBatch?: (
973
+ heads: string[],
974
+ ) => Array<indexerTypes.IndexedResult<WithContext<I>> | undefined>;
975
+ getIdByContextHead?: (head: string) => indexerTypes.IdKey | undefined;
976
+ };
977
+
978
+ type ExactDeleteIndex = {
979
+ delIdsNoReturn?: (
980
+ deleteIds: Array<indexerTypes.IdKey | indexerTypes.Ideable>,
981
+ ) => Promise<void> | void;
982
+ delIds?: (
983
+ deleteIds: Array<indexerTypes.IdKey | indexerTypes.Ideable>,
984
+ ) => Promise<indexerTypes.IdKey[]> | indexerTypes.IdKey[];
985
+ };
986
+
987
+ type NativeBackboneDocumentIndex = {
988
+ attachNativeBackboneDocumentIndex?: (
989
+ backbone: unknown,
990
+ options?: { preserveExisting?: boolean },
991
+ ) => boolean | void;
992
+ };
993
+
994
+ type NativeBackboneDocumentProjection = {
995
+ projectDocumentIndexSimple?: (
996
+ encodedDocument: Uint8Array,
997
+ plan: SimpleDocumentProjectionPlan,
998
+ context: SimpleDocumentProjectionContext,
999
+ ) => Uint8Array | undefined;
1000
+ };
1001
+
1002
+ type NativeBackboneDocumentIndexCommit<I> = {
1003
+ valuePrefixBytes?: Uint8Array;
1004
+ usePlainPutPayload?: boolean;
1005
+ projection?: {
1006
+ encodedDocument: Uint8Array;
1007
+ plan: SimpleDocumentProjectionPlan;
1008
+ signer?: Uint8Array;
1009
+ };
1010
+ indexable?: I;
1011
+ getIndexable?: () => I;
1012
+ setContext?: (context: types.Context) => void;
1013
+ };
1014
+
664
1015
  export const coerceWithContext = <T>(
665
1016
  value: T | WithContext<T>,
666
1017
  context: types.Context,
@@ -679,6 +1030,36 @@ export const coerceWithIndexed = <T, I>(
679
1030
  return valueWithContext;
680
1031
  };
681
1032
 
1033
+ export const coerceWithLazyIndexed = <T, I>(
1034
+ value: T | WithIndexedContext<T, I>,
1035
+ getIndexable: () => I,
1036
+ ): WithIndexedContext<T, I> => {
1037
+ let cached: I | undefined;
1038
+ let hasCached = false;
1039
+ Object.defineProperty(value, "__indexed", {
1040
+ configurable: true,
1041
+ enumerable: true,
1042
+ get() {
1043
+ if (!hasCached) {
1044
+ cached = getIndexable();
1045
+ hasCached = true;
1046
+ Object.defineProperty(value, "__indexed", {
1047
+ configurable: true,
1048
+ enumerable: true,
1049
+ writable: true,
1050
+ value: cached,
1051
+ });
1052
+ }
1053
+ return cached;
1054
+ },
1055
+ set(indexed: I) {
1056
+ cached = indexed;
1057
+ hasCached = true;
1058
+ },
1059
+ });
1060
+ return value as WithIndexedContext<T, I>;
1061
+ };
1062
+
682
1063
  @variant("documents_index")
683
1064
  export class DocumentIndex<
684
1065
  T,
@@ -693,6 +1074,10 @@ export class DocumentIndex<
693
1074
 
694
1075
  // transform options
695
1076
  transformer: Transformer<T, I>;
1077
+ private transformerIsIdentity = false;
1078
+ private nativeTransformDescriptor?: DocumentTransformDescriptor;
1079
+ private nativeTransformProjectionPlan?: SimpleDocumentProjectionPlan;
1080
+ private nativeBackboneDocumentProjection?: NativeBackboneDocumentProjection;
696
1081
 
697
1082
  // The indexed document wrapped in a context
698
1083
  wrappedIndexedType: IndexableClass<I>;
@@ -709,6 +1094,7 @@ export class DocumentIndex<
709
1094
  private _resumableIterators: ResumableIterators<WithContext<I>>;
710
1095
  private _prefetch?: PrefetchOptions | undefined;
711
1096
  private includeIndexed: boolean | undefined = undefined;
1097
+ private immutable: boolean = false;
712
1098
 
713
1099
  compatibility: 6 | 7 | 8 | 9 | undefined;
714
1100
 
@@ -757,11 +1143,21 @@ export class DocumentIndex<
757
1143
  >;
758
1144
  private iteratorKeepAliveTimers?: Map<string, ReturnType<typeof setTimeout>>;
759
1145
 
1146
+ private deleteResolvedCacheForKey(key: indexerTypes.IdKey): void {
1147
+ if (this.isProgramValued) {
1148
+ this._resolverProgramCache!.delete(key.primitive);
1149
+ indexCacheLogger("cache:del:program", { id: key.primitive });
1150
+ } else if (this._resolverCache?.del(key.primitive)) {
1151
+ indexCacheLogger("cache:del:value", { id: key.primitive });
1152
+ }
1153
+ }
1154
+
760
1155
  constructor(properties?: {
761
1156
  query?: RPC<types.AbstractSearchRequest, types.AbstractSearchResult>;
762
1157
  }) {
763
1158
  super();
764
1159
  this._query = properties?.query || new RPC();
1160
+ this._resultQueue = new Map();
765
1161
  this.iteratorKeepAliveTimers = new Map();
766
1162
  }
767
1163
 
@@ -903,9 +1299,16 @@ export class DocumentIndex<
903
1299
  return results;
904
1300
  }
905
1301
 
906
- private handleDocumentChange = async (
1302
+ // Bound in open(). Deserialized instances skip constructor/field initializers
1303
+ // (borsh creates objects via Object.create), so this must not rely on a field
1304
+ // initializer to exist.
1305
+ private handleDocumentChange?: (
907
1306
  event: CustomEvent<DocumentsChange<T, I>>,
908
- ) => {
1307
+ ) => Promise<void>;
1308
+
1309
+ private async onDocumentChange(
1310
+ event: CustomEvent<DocumentsChange<T, I>>,
1311
+ ): Promise<void> {
909
1312
  const added = event.detail.added;
910
1313
  if (!added.length) {
911
1314
  return;
@@ -980,7 +1383,7 @@ export class DocumentIndex<
980
1383
  queue.pushInFlight = false;
981
1384
  }
982
1385
  }
983
- };
1386
+ }
984
1387
 
985
1388
  private get nestedProperties() {
986
1389
  return {
@@ -1026,6 +1429,7 @@ export class DocumentIndex<
1026
1429
  this.canRead = properties.canRead;
1027
1430
  this.canSearch = properties.canSearch;
1028
1431
  this.includeIndexed = properties.includeIndexed;
1432
+ this.immutable = properties.immutable ?? false;
1029
1433
 
1030
1434
  @variant(0)
1031
1435
  class IndexedClassWithContext {
@@ -1093,9 +1497,25 @@ export class DocumentIndex<
1093
1497
  };
1094
1498
 
1095
1499
  const transformOptions = properties.transform;
1500
+ const hasTransformFunction =
1501
+ transformOptions != null && isTransformerWithFunction(transformOptions);
1502
+ this.nativeTransformDescriptor = hasTransformFunction
1503
+ ? getDocumentTransformDescriptor(transformOptions.transform)
1504
+ : undefined;
1505
+ this.nativeTransformProjectionPlan = createSimpleProjectionPlan(
1506
+ getSchema(this.documentType),
1507
+ indexedSchema,
1508
+ this.nativeTransformDescriptor,
1509
+ );
1510
+ this.transformerIsIdentity =
1511
+ transformOptions == null ||
1512
+ (!hasTransformFunction && transformOptions.type == null) ||
1513
+ (this.nativeTransformDescriptor?.kind === "identity" &&
1514
+ this.indexedTypeIsDocumentType);
1096
1515
  this.transformer = transformOptions
1097
- ? isTransformerWithFunction(transformOptions)
1098
- ? (obj, context) => transformOptions.transform(obj, context)
1516
+ ? hasTransformFunction
1517
+ ? (obj, context, facts) =>
1518
+ transformOptions.transform(obj, context, facts)
1099
1519
  : transformOptions.type
1100
1520
  ? (obj, context) => new transformOptions.type!(obj, context)
1101
1521
  : (obj) => obj as any as I
@@ -1210,9 +1630,307 @@ export class DocumentIndex<
1210
1630
  responseType: types.AbstractSearchResult,
1211
1631
  queryType: types.AbstractSearchRequest,
1212
1632
  });
1213
- if (this.handleDocumentChange) {
1214
- this.documentEvents.addEventListener("change", this.handleDocumentChange);
1633
+ this.handleDocumentChange ??= (event) => this.onDocumentChange(event);
1634
+ this.documentEvents.addEventListener("change", this.handleDocumentChange);
1635
+ }
1636
+
1637
+ private attachNativeBackboneDocumentIndex(
1638
+ backbone: unknown,
1639
+ options?: { preserveExisting?: boolean },
1640
+ ): boolean {
1641
+ this.nativeBackboneDocumentProjection = undefined;
1642
+ if (
1643
+ !backbone ||
1644
+ this.isProgramValued ||
1645
+ !this.canUseNativeBackboneDocumentIndex()
1646
+ ) {
1647
+ return false;
1648
+ }
1649
+ const attach = (this.index as NativeBackboneDocumentIndex)
1650
+ .attachNativeBackboneDocumentIndex;
1651
+ const attached =
1652
+ typeof attach === "function" &&
1653
+ attach.call(this.index, backbone, options) === true;
1654
+ if (attached) {
1655
+ const projection = backbone as NativeBackboneDocumentProjection;
1656
+ if (typeof projection.projectDocumentIndexSimple === "function") {
1657
+ this.nativeBackboneDocumentProjection = projection;
1658
+ }
1659
+ }
1660
+ return attached;
1661
+ }
1662
+
1663
+ private canUseNativeBackboneDocumentIndex(): boolean {
1664
+ return (
1665
+ (this.transformerIsIdentity && this.indexedTypeIsDocumentType) ||
1666
+ this.nativeTransformDescriptor != null
1667
+ );
1668
+ }
1669
+
1670
+ private getNativeDocumentFieldExtractionPlan(
1671
+ path: string | readonly string[],
1672
+ ): SimpleDocumentFieldExtractionPlan | undefined {
1673
+ return createSimpleFieldExtractionPlan(getSchema(this.documentType), path);
1674
+ }
1675
+
1676
+ private canPrepareNativeBackboneDocumentIndexCommit(): boolean {
1677
+ return (
1678
+ (this.transformerIsIdentity && this.indexedTypeIsDocumentType) ||
1679
+ canPrepareDocumentTransformBeforeAppend(
1680
+ this.nativeTransformDescriptor,
1681
+ )
1682
+ );
1683
+ }
1684
+
1685
+ private canPrepareNativeBackboneDocumentIndexCommitWithAppendFacts(): boolean {
1686
+ return (
1687
+ this.canPrepareNativeBackboneDocumentIndexCommit() ||
1688
+ !!this.nativeTransformProjectionPlan ||
1689
+ canPrepareDocumentTransformWithAppendFacts(
1690
+ this.nativeTransformDescriptor,
1691
+ )
1692
+ );
1693
+ }
1694
+
1695
+ private canUseNativeBackboneContextualBatch(): boolean {
1696
+ return (
1697
+ !this.isProgramValued &&
1698
+ typeof (this.index as ContextualIndexPut<I>).putWithContextBatch ===
1699
+ "function" &&
1700
+ ((this.transformerIsIdentity && this.indexedTypeIsDocumentType) ||
1701
+ this.canPrepareNativeBackboneDocumentIndexCommitWithAppendFacts())
1702
+ );
1703
+ }
1704
+
1705
+ private prepareNativeBackboneDocumentIndexCommit(
1706
+ value: T,
1707
+ encodedDocument: Uint8Array,
1708
+ transformFacts?: DocumentTransformFacts,
1709
+ ): MaybePromise<NativeBackboneDocumentIndexCommit<I> | undefined> {
1710
+ if (this.transformerIsIdentity && this.indexedTypeIsDocumentType) {
1711
+ return {
1712
+ valuePrefixBytes: encodedDocument,
1713
+ usePlainPutPayload: true,
1714
+ indexable: value as any as I,
1715
+ };
1716
+ }
1717
+ if (this.nativeTransformProjectionPlan) {
1718
+ let projectionContext: types.Context | undefined;
1719
+ let cached: I | undefined;
1720
+ let hasCached = false;
1721
+ return {
1722
+ projection: {
1723
+ encodedDocument,
1724
+ plan: this.nativeTransformProjectionPlan,
1725
+ signer: transformFacts?.entryPublicKeys?.[0]?.bytes,
1726
+ },
1727
+ getIndexable: () => {
1728
+ if (!hasCached) {
1729
+ const transformed = this.transformer(
1730
+ value,
1731
+ projectionContext as types.Context,
1732
+ transformFacts,
1733
+ );
1734
+ if (isPromiseLike(transformed)) {
1735
+ throw new Error(
1736
+ "Native descriptor transform unexpectedly returned a promise",
1737
+ );
1738
+ }
1739
+ cached = transformed;
1740
+ hasCached = true;
1741
+ }
1742
+ return cached!;
1743
+ },
1744
+ setContext: (context) => {
1745
+ projectionContext = context;
1746
+ hasCached = false;
1747
+ cached = undefined;
1748
+ },
1749
+ };
1750
+ }
1751
+ if (
1752
+ !canPrepareDocumentTransformBeforeAppend(
1753
+ this.nativeTransformDescriptor,
1754
+ )
1755
+ ) {
1756
+ return;
1757
+ }
1758
+ const transformed = this.transformer(
1759
+ value,
1760
+ undefined as unknown as types.Context,
1761
+ transformFacts,
1762
+ );
1763
+ const finish = (indexable: I): NativeBackboneDocumentIndexCommit<I> => ({
1764
+ valuePrefixBytes: serialize(this.asIndexedTypeValue(indexable)),
1765
+ indexable,
1766
+ });
1767
+ return isPromiseLike(transformed)
1768
+ ? transformed.then(finish)
1769
+ : finish(transformed);
1770
+ }
1771
+
1772
+ private prepareNativeBackboneDocumentIndexCommitWithAppendFacts(
1773
+ value: T,
1774
+ encodedDocument: Uint8Array,
1775
+ context: types.Context,
1776
+ transformFacts?: DocumentTransformFacts,
1777
+ ): NativeBackboneDocumentIndexCommit<I> | undefined {
1778
+ if (this.transformerIsIdentity && this.indexedTypeIsDocumentType) {
1779
+ return {
1780
+ valuePrefixBytes: encodedDocument,
1781
+ usePlainPutPayload: true,
1782
+ indexable: value as any as I,
1783
+ };
1784
+ }
1785
+ if (this.nativeTransformProjectionPlan) {
1786
+ let projectionContext = {
1787
+ created: context.created,
1788
+ modified: context.modified,
1789
+ head: context.head,
1790
+ gid: context.gid,
1791
+ size: context.size,
1792
+ signer: transformFacts?.entryPublicKeys?.[0]?.bytes,
1793
+ };
1794
+ let cached: I | undefined;
1795
+ let hasCached = false;
1796
+ return {
1797
+ projection: {
1798
+ encodedDocument,
1799
+ plan: this.nativeTransformProjectionPlan,
1800
+ signer: projectionContext.signer,
1801
+ },
1802
+ getIndexable: () => {
1803
+ if (!hasCached) {
1804
+ const transformed = this.transformer(
1805
+ value,
1806
+ projectionContext as types.Context,
1807
+ transformFacts,
1808
+ );
1809
+ if (isPromiseLike(transformed)) {
1810
+ throw new Error(
1811
+ "Native descriptor transform unexpectedly returned a promise",
1812
+ );
1813
+ }
1814
+ cached = transformed;
1815
+ hasCached = true;
1816
+ }
1817
+ return cached!;
1818
+ },
1819
+ setContext: (nextContext) => {
1820
+ projectionContext = {
1821
+ created: nextContext.created,
1822
+ modified: nextContext.modified,
1823
+ head: nextContext.head,
1824
+ gid: nextContext.gid,
1825
+ size: nextContext.size,
1826
+ signer: transformFacts?.entryPublicKeys?.[0]?.bytes,
1827
+ };
1828
+ hasCached = false;
1829
+ cached = undefined;
1830
+ },
1831
+ };
1215
1832
  }
1833
+ if (
1834
+ !canPrepareDocumentTransformWithAppendFacts(
1835
+ this.nativeTransformDescriptor,
1836
+ ) &&
1837
+ !canPrepareDocumentTransformBeforeAppend(
1838
+ this.nativeTransformDescriptor,
1839
+ )
1840
+ ) {
1841
+ return;
1842
+ }
1843
+ const transformed = this.transformer(value, context, transformFacts);
1844
+ if (isPromiseLike(transformed)) {
1845
+ return;
1846
+ }
1847
+ const indexable = transformed;
1848
+ return {
1849
+ valuePrefixBytes: serialize(this.asIndexedTypeValue(indexable)),
1850
+ indexable,
1851
+ };
1852
+ }
1853
+
1854
+ private prepareNativeBackboneDocumentIndexStoredCommitWithAppendFacts(
1855
+ encodedDocument: Uint8Array,
1856
+ context: types.Context,
1857
+ transformFacts?: DocumentTransformFacts,
1858
+ ): NativeBackboneDocumentIndexCommit<I> | undefined {
1859
+ if (this.transformerIsIdentity && this.indexedTypeIsDocumentType) {
1860
+ return {
1861
+ valuePrefixBytes: encodedDocument,
1862
+ usePlainPutPayload: true,
1863
+ };
1864
+ }
1865
+ if (this.nativeTransformProjectionPlan) {
1866
+ return {
1867
+ projection: {
1868
+ encodedDocument,
1869
+ plan: this.nativeTransformProjectionPlan,
1870
+ signer: transformFacts?.entryPublicKeys?.[0]?.bytes,
1871
+ },
1872
+ };
1873
+ }
1874
+ if (
1875
+ !canPrepareDocumentTransformWithAppendFacts(
1876
+ this.nativeTransformDescriptor,
1877
+ ) &&
1878
+ !canPrepareDocumentTransformBeforeAppend(
1879
+ this.nativeTransformDescriptor,
1880
+ )
1881
+ ) {
1882
+ return;
1883
+ }
1884
+ return;
1885
+ }
1886
+
1887
+ private nativeBackboneDocumentIndexValuePrefixBytes(
1888
+ nativeDocumentIndex: NativeBackboneDocumentIndexCommit<I>,
1889
+ context: types.Context,
1890
+ ): Uint8Array | undefined {
1891
+ return (
1892
+ nativeDocumentIndex.valuePrefixBytes ??
1893
+ (nativeDocumentIndex.projection
1894
+ ? (this.nativeBackboneDocumentProjection?.projectDocumentIndexSimple?.(
1895
+ nativeDocumentIndex.projection.encodedDocument,
1896
+ nativeDocumentIndex.projection.plan,
1897
+ {
1898
+ created: context.created,
1899
+ modified: context.modified,
1900
+ head: context.head,
1901
+ gid: context.gid,
1902
+ size: context.size,
1903
+ signer: nativeDocumentIndex.projection.signer,
1904
+ },
1905
+ ) ??
1906
+ tryProjectDocumentIndexSimple(
1907
+ nativeDocumentIndex.projection.encodedDocument,
1908
+ nativeDocumentIndex.projection.plan,
1909
+ {
1910
+ created: context.created,
1911
+ modified: context.modified,
1912
+ head: context.head,
1913
+ gid: context.gid,
1914
+ size: context.size,
1915
+ signer: nativeDocumentIndex.projection.signer,
1916
+ },
1917
+ ))
1918
+ : undefined)
1919
+ );
1920
+ }
1921
+
1922
+ private asIndexedTypeValue(value: I): I {
1923
+ if (
1924
+ value &&
1925
+ Object.getPrototypeOf(value) ===
1926
+ (this.indexedType as { prototype: object }).prototype
1927
+ ) {
1928
+ return value;
1929
+ }
1930
+ return Object.assign(
1931
+ Object.create((this.indexedType as { prototype: object }).prototype),
1932
+ value,
1933
+ );
1216
1934
  }
1217
1935
 
1218
1936
  get prefetch() {
@@ -1435,14 +2153,14 @@ export class DocumentIndex<
1435
2153
  if (this._joinListener) {
1436
2154
  this._query.events.removeEventListener("join", this._joinListener);
1437
2155
  }
1438
- if (this.handleDocumentChange) {
2156
+ if (this.handleDocumentChange && this.documentEvents) {
1439
2157
  this.documentEvents.removeEventListener(
1440
2158
  "change",
1441
2159
  this.handleDocumentChange,
1442
2160
  );
1443
2161
  }
1444
2162
  this.clearAllResultQueues();
1445
- await this._resumableIterators.clearAll();
2163
+ await this._resumableIterators?.clearAll();
1446
2164
  if (this.iteratorKeepAliveTimers) {
1447
2165
  for (const timer of this.iteratorKeepAliveTimers.values()) {
1448
2166
  clearTimeout(timer);
@@ -1466,12 +2184,12 @@ export class DocumentIndex<
1466
2184
  async drop(from?: Program): Promise<boolean> {
1467
2185
  const dropped = await super.drop(from);
1468
2186
  if (dropped) {
1469
- this.documentEvents.removeEventListener(
2187
+ this.documentEvents?.removeEventListener(
1470
2188
  "change",
1471
2189
  this.handleDocumentChange,
1472
2190
  );
1473
2191
  this.clearAllResultQueues();
1474
- await this._resumableIterators.clearAll();
2192
+ await this._resumableIterators?.clearAll();
1475
2193
  if (this.iteratorKeepAliveTimers) {
1476
2194
  for (const timer of this.iteratorKeepAliveTimers.values()) {
1477
2195
  clearTimeout(timer);
@@ -1580,59 +2298,687 @@ export class DocumentIndex<
1580
2298
  }
1581
2299
  }
1582
2300
 
1583
- // Re-query on peer joins (like iterate), scoped to the joining peer
1584
- let joinListener: (() => void) | undefined;
1585
- if (baseRemote) {
1586
- joinListener = this.createReplicatorJoinListener({
1587
- eager: baseRemote.reach?.eager,
1588
- onPeer: async (pk) => {
1589
- if (cleanedUp) return;
1590
- const hash = pk.hashcode();
1591
- const requeryOptions: QueryOptions<T, I, D, Resolve> = {
1592
- ...(options as any),
1593
- remote: {
1594
- ...(baseRemote || {}),
1595
- from: [hash],
1596
- },
1597
- };
1598
- const re = await this.getDetailed(idKey, requeryOptions as any);
1599
- const first = re?.[0]?.results[0];
1600
- if (first) {
1601
- deferred!.resolve(first.value as any);
1602
- }
2301
+ // Re-query on peer joins (like iterate), scoped to the joining peer
2302
+ let joinListener: (() => void) | undefined;
2303
+ if (baseRemote) {
2304
+ joinListener = this.createReplicatorJoinListener({
2305
+ eager: baseRemote.reach?.eager,
2306
+ onPeer: async (pk) => {
2307
+ if (cleanedUp) return;
2308
+ const hash = pk.hashcode();
2309
+ const requeryOptions: QueryOptions<T, I, D, Resolve> = {
2310
+ ...(options as any),
2311
+ remote: {
2312
+ ...(baseRemote || {}),
2313
+ from: [hash],
2314
+ },
2315
+ };
2316
+ const re = await this.getDetailed(idKey, requeryOptions as any);
2317
+ const first = re?.[0]?.results[0];
2318
+ if (first) {
2319
+ deferred!.resolve(first.value as any);
2320
+ }
2321
+ },
2322
+ });
2323
+ }
2324
+ }
2325
+
2326
+ const initialOptions = baseRemote
2327
+ ? ({ ...(options as any), remote: baseRemote } as Options)
2328
+ : options;
2329
+ const result = (await this.getDetailed(idKey, initialOptions))?.[0]
2330
+ ?.results[0];
2331
+
2332
+ // if no results, and we have remote joining options, we wait for the timout and if there are joining peers we re-query
2333
+ if (!result) {
2334
+ return deferred?.promise;
2335
+ } else if (deferred) {
2336
+ deferred.resolve(undefined);
2337
+ }
2338
+ return result?.value;
2339
+ }
2340
+
2341
+ public async getFromGid(gid: string) {
2342
+ const iterator = this.index.iterate({ query: { gid } });
2343
+ const one = await iterator.next(1);
2344
+ await iterator.close();
2345
+ return one[0];
2346
+ }
2347
+
2348
+ public async getFromHash(hash: string) {
2349
+ const iterator = this.index.iterate({ query: { hash } });
2350
+ const one = await iterator.next(1);
2351
+ await iterator.close();
2352
+ return one[0];
2353
+ }
2354
+
2355
+ public async getIdentityIndexedByHead(
2356
+ head: string,
2357
+ ): Promise<indexerTypes.IndexedResult<WithContext<I>> | undefined> {
2358
+ if (!this.canGetIdentityIndexedByHead()) {
2359
+ return;
2360
+ }
2361
+ return (this.index as ContextHeadIndex<I>).getByContextHead?.(head);
2362
+ }
2363
+
2364
+ public async getIdentityIndexedKeyByHead(
2365
+ head: string,
2366
+ ): Promise<indexerTypes.IdKey | undefined> {
2367
+ const key = this.getIndexedKeyByHead(head);
2368
+ if (key) {
2369
+ return key;
2370
+ }
2371
+ const indexed = await this.getIdentityIndexedByHead(head);
2372
+ return indexed?.id;
2373
+ }
2374
+
2375
+ private getIndexedKeyByHead(
2376
+ head: string,
2377
+ ): indexerTypes.IdKey | undefined {
2378
+ const getIdByHead = (this.index as ContextHeadIndex<I>).getIdByContextHead;
2379
+ return typeof getIdByHead === "function"
2380
+ ? getIdByHead.call(this.index, head)
2381
+ : undefined;
2382
+ }
2383
+
2384
+ public getIndexedKeysByHeads(
2385
+ heads: string[],
2386
+ ): Array<indexerTypes.IdKey | undefined> | undefined {
2387
+ const getIdByHead = (this.index as ContextHeadIndex<I>).getIdByContextHead;
2388
+ if (typeof getIdByHead !== "function") {
2389
+ return;
2390
+ }
2391
+ return heads.map((head) => getIdByHead.call(this.index, head));
2392
+ }
2393
+
2394
+ public tryGetIdentityIndexedKeyByHead(
2395
+ head: string,
2396
+ ): { supported: boolean; key?: indexerTypes.IdKey } {
2397
+ const getIdByHead = (this.index as ContextHeadIndex<I>).getIdByContextHead;
2398
+ if (typeof getIdByHead !== "function") {
2399
+ return { supported: false };
2400
+ }
2401
+ return {
2402
+ supported: true,
2403
+ key: getIdByHead.call(this.index, head),
2404
+ };
2405
+ }
2406
+
2407
+ public async getIdentityIndexedByHeads(
2408
+ heads: string[],
2409
+ ): Promise<
2410
+ Array<indexerTypes.IndexedResult<WithContext<I>> | undefined> | undefined
2411
+ > {
2412
+ if (!this.canGetIdentityIndexedByHead()) {
2413
+ return;
2414
+ }
2415
+ const batch = (this.index as ContextHeadIndex<I>).getByContextHeadBatch;
2416
+ if (batch) {
2417
+ return batch.call(this.index, heads);
2418
+ }
2419
+ return Promise.all(
2420
+ heads.map((head) => this.getIdentityIndexedByHead(head)),
2421
+ );
2422
+ }
2423
+
2424
+ public canGetIdentityIndexedByHead(): boolean {
2425
+ return (
2426
+ this.transformerIsIdentity &&
2427
+ this.indexedTypeIsDocumentType &&
2428
+ !this.isProgramValued &&
2429
+ typeof (this.index as ContextHeadIndex<I>).getByContextHead === "function"
2430
+ );
2431
+ }
2432
+
2433
+ public canGetIndexedKeyByHead(): boolean {
2434
+ return (
2435
+ !this.isProgramValued &&
2436
+ typeof (this.index as ContextHeadIndex<I>).getIdByContextHead ===
2437
+ "function"
2438
+ );
2439
+ }
2440
+
2441
+ public canReadOriginalFieldPathsFromIndexedValue(
2442
+ paths: readonly (string | readonly string[])[],
2443
+ ): boolean {
2444
+ return (
2445
+ !this.isProgramValued &&
2446
+ paths.every((path) =>
2447
+ this.transformerIsIdentity && this.indexedTypeIsDocumentType
2448
+ ? true
2449
+ : documentTransformPreservesFieldPath(
2450
+ this.nativeTransformDescriptor,
2451
+ path,
2452
+ ),
2453
+ )
2454
+ );
2455
+ }
2456
+
2457
+ public canReadNativeIndexedFieldValues(
2458
+ paths: readonly (string | readonly string[])[],
2459
+ ): boolean {
2460
+ return (
2461
+ this.canReadOriginalFieldPathsFromIndexedValue(paths) &&
2462
+ typeof (
2463
+ this.index as {
2464
+ getNativeIndexedFieldValue?: (
2465
+ id: indexerTypes.IdKey,
2466
+ path: readonly string[],
2467
+ ) => unknown;
2468
+ }
2469
+ ).getNativeIndexedFieldValue === "function"
2470
+ );
2471
+ }
2472
+
2473
+ public getNativeIndexedFieldValue(
2474
+ id: indexerTypes.IdKey,
2475
+ path: string | readonly string[],
2476
+ ): unknown {
2477
+ const read = (
2478
+ this.index as {
2479
+ getNativeIndexedFieldValue?: (
2480
+ id: indexerTypes.IdKey,
2481
+ path: readonly string[],
2482
+ ) => unknown;
2483
+ }
2484
+ ).getNativeIndexedFieldValue;
2485
+ if (typeof read !== "function") {
2486
+ return undefined;
2487
+ }
2488
+ return read.call(this.index, id, typeof path === "string" ? [path] : path);
2489
+ }
2490
+
2491
+ public _putIdentityWithContext(
2492
+ value: T,
2493
+ id: indexerTypes.IdKey,
2494
+ context: types.Context,
2495
+ options?: ContextualPutOptions,
2496
+ ): MaybePromise<WithIndexedContext<T, I> | undefined> {
2497
+ const contextualPut = this.transformerIsIdentity
2498
+ ? (this.index as ContextualIndexPut<I>).putWithContext
2499
+ : undefined;
2500
+ if (!contextualPut || this.isProgramValued) {
2501
+ return;
2502
+ }
2503
+ const indexable = value as any as I;
2504
+ const indexedValue = coerceWithIndexed(
2505
+ coerceWithContext(value, context),
2506
+ indexable,
2507
+ );
2508
+ this.cacheResolvedValue(id.primitive, value);
2509
+ const handleError = (error: unknown) => {
2510
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2511
+ return indexedValue;
2512
+ }
2513
+ throw error;
2514
+ };
2515
+ try {
2516
+ const putResult = contextualPut.call(
2517
+ this.index,
2518
+ indexable,
2519
+ id,
2520
+ context,
2521
+ this.withContextualEncodedValue(options, context),
2522
+ );
2523
+ return isPromiseLike(putResult)
2524
+ ? putResult.then(() => indexedValue, handleError)
2525
+ : indexedValue;
2526
+ } catch (error) {
2527
+ return handleError(error);
2528
+ }
2529
+ }
2530
+
2531
+ public _putStoredIdentityWithContext(
2532
+ value: T,
2533
+ id: indexerTypes.IdKey,
2534
+ context: types.Context,
2535
+ encodedValueParts: NonNullable<ContextualPutOptions["encodedValueParts"]>,
2536
+ options?: { replace?: boolean },
2537
+ ): MaybePromise<WithIndexedContext<T, I> | undefined> {
2538
+ const contextualStoredPut = this.transformerIsIdentity
2539
+ ? (this.index as ContextualIndexPut<I>).putStoredContextualEncodedValue
2540
+ : undefined;
2541
+ if (
2542
+ !contextualStoredPut ||
2543
+ this.isProgramValued ||
2544
+ !this.indexedTypeIsDocumentType
2545
+ ) {
2546
+ return;
2547
+ }
2548
+ const indexable = value as any as I;
2549
+ const indexedValue = coerceWithIndexed(
2550
+ coerceWithContext(value, context),
2551
+ indexable,
2552
+ );
2553
+ this.cacheResolvedValue(id.primitive, value);
2554
+ const handleError = (error: unknown) => {
2555
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2556
+ return indexedValue;
2557
+ }
2558
+ throw error;
2559
+ };
2560
+ try {
2561
+ const putResult = contextualStoredPut.call(
2562
+ this.index,
2563
+ id,
2564
+ encodedValueParts,
2565
+ options,
2566
+ );
2567
+ if (putResult === false) {
2568
+ return;
2569
+ }
2570
+ return isPromiseLike(putResult)
2571
+ ? putResult.then(() => indexedValue, handleError)
2572
+ : indexedValue;
2573
+ } catch (error) {
2574
+ return handleError(error);
2575
+ }
2576
+ }
2577
+
2578
+ private _putPreparedNativeBackboneDocumentIndexWithContext(
2579
+ value: T,
2580
+ id: indexerTypes.IdKey,
2581
+ context: types.Context,
2582
+ nativeDocumentIndex: NativeBackboneDocumentIndexCommit<I>,
2583
+ options?: { replace?: boolean },
2584
+ ): MaybePromise<WithIndexedContext<T, I> | undefined> {
2585
+ const contextualStoredPut = (this.index as ContextualIndexPut<I>)
2586
+ .putStoredContextualEncodedValue;
2587
+ if (!contextualStoredPut || this.isProgramValued) {
2588
+ return;
2589
+ }
2590
+ nativeDocumentIndex.setContext?.(context);
2591
+ const valueWithContext = coerceWithContext(value, context);
2592
+ const indexedValue = nativeDocumentIndex.indexable
2593
+ ? coerceWithIndexed(valueWithContext, nativeDocumentIndex.indexable)
2594
+ : nativeDocumentIndex.getIndexable
2595
+ ? coerceWithLazyIndexed(
2596
+ valueWithContext,
2597
+ nativeDocumentIndex.getIndexable,
2598
+ )
2599
+ : undefined;
2600
+ if (!indexedValue) {
2601
+ return;
2602
+ }
2603
+ this.cacheResolvedValue(id.primitive, value);
2604
+ const valuePrefixBytes = this.nativeBackboneDocumentIndexValuePrefixBytes(
2605
+ nativeDocumentIndex,
2606
+ context,
2607
+ );
2608
+ if (!valuePrefixBytes) {
2609
+ return;
2610
+ }
2611
+ const encodedValueParts = {
2612
+ prefix: valuePrefixBytes,
2613
+ suffix: encodeContextSuffix(context),
2614
+ };
2615
+ const handleError = (error: unknown) => {
2616
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2617
+ return indexedValue;
2618
+ }
2619
+ throw error;
2620
+ };
2621
+ try {
2622
+ const putResult = contextualStoredPut.call(
2623
+ this.index,
2624
+ id,
2625
+ encodedValueParts,
2626
+ options,
2627
+ );
2628
+ if (putResult === false) {
2629
+ return;
2630
+ }
2631
+ return isPromiseLike(putResult)
2632
+ ? putResult.then(() => indexedValue, handleError)
2633
+ : indexedValue;
2634
+ } catch (error) {
2635
+ return handleError(error);
2636
+ }
2637
+ }
2638
+
2639
+ private _putPreparedNativeBackboneDocumentIndexStoredWithContext(
2640
+ id: indexerTypes.IdKey,
2641
+ context: types.Context,
2642
+ nativeDocumentIndex: NativeBackboneDocumentIndexCommit<I>,
2643
+ options?: { replace?: boolean },
2644
+ ): MaybePromise<boolean | undefined> {
2645
+ const contextualStoredPut = (this.index as ContextualIndexPut<I>)
2646
+ .putStoredContextualEncodedValue;
2647
+ if (!contextualStoredPut || this.isProgramValued) {
2648
+ return;
2649
+ }
2650
+ nativeDocumentIndex.setContext?.(context);
2651
+ const valuePrefixBytes = this.nativeBackboneDocumentIndexValuePrefixBytes(
2652
+ nativeDocumentIndex,
2653
+ context,
2654
+ );
2655
+ if (!valuePrefixBytes) {
2656
+ return;
2657
+ }
2658
+ const encodedValueParts = {
2659
+ prefix: valuePrefixBytes,
2660
+ suffix: encodeContextSuffix(context),
2661
+ };
2662
+ const handleError = (error: unknown) => {
2663
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2664
+ return true;
2665
+ }
2666
+ throw error;
2667
+ };
2668
+ try {
2669
+ const putResult = contextualStoredPut.call(
2670
+ this.index,
2671
+ id,
2672
+ encodedValueParts,
2673
+ options,
2674
+ );
2675
+ if (putResult === false) {
2676
+ return false;
2677
+ }
2678
+ return isPromiseLike(putResult)
2679
+ ? putResult.then(() => true, handleError)
2680
+ : true;
2681
+ } catch (error) {
2682
+ return handleError(error);
2683
+ }
2684
+ }
2685
+
2686
+ private _persistPreparedNativeBackboneDocumentIndexStoredWithContext(
2687
+ id: indexerTypes.IdKey,
2688
+ context: types.Context,
2689
+ nativeDocumentIndex?: NativeBackboneDocumentIndexCommit<I>,
2690
+ encodedValueParts?: NonNullable<ContextualPutOptions["encodedValueParts"]>,
2691
+ options?: { replace?: boolean },
2692
+ ): MaybePromise<boolean | undefined> {
2693
+ const persistStoredPut = (this.index as ContextualIndexPut<I>)
2694
+ .persistStoredContextualEncodedValue;
2695
+ if (!persistStoredPut || this.isProgramValued) {
2696
+ return;
2697
+ }
2698
+ let storedParts:
2699
+ | NonNullable<ContextualPutOptions["encodedValueParts"]>
2700
+ | undefined;
2701
+ if (
2702
+ this.transformerIsIdentity &&
2703
+ this.indexedTypeIsDocumentType &&
2704
+ encodedValueParts
2705
+ ) {
2706
+ storedParts = encodedValueParts;
2707
+ } else if (nativeDocumentIndex) {
2708
+ nativeDocumentIndex.setContext?.(context);
2709
+ const valuePrefixBytes =
2710
+ nativeDocumentIndex.valuePrefixBytes ??
2711
+ this.nativeBackboneDocumentIndexValuePrefixBytes(
2712
+ nativeDocumentIndex,
2713
+ context,
2714
+ );
2715
+ if (!valuePrefixBytes) {
2716
+ return;
2717
+ }
2718
+ storedParts = {
2719
+ prefix: valuePrefixBytes,
2720
+ suffix: encodeContextSuffix(context),
2721
+ };
2722
+ } else {
2723
+ return;
2724
+ }
2725
+ try {
2726
+ const persistResult = persistStoredPut.call(
2727
+ this.index,
2728
+ id,
2729
+ storedParts,
2730
+ options,
2731
+ );
2732
+ if (persistResult === false) {
2733
+ return false;
2734
+ }
2735
+ return isPromiseLike(persistResult)
2736
+ ? persistResult.then(() => true, (error: unknown) => {
2737
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2738
+ return true;
2739
+ }
2740
+ throw error;
2741
+ })
2742
+ : true;
2743
+ } catch (error) {
2744
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2745
+ return true;
2746
+ }
2747
+ throw error;
2748
+ }
2749
+ }
2750
+
2751
+ private async _putManyPreparedNativeBackboneDocumentIndexWithContext(
2752
+ values: Array<{
2753
+ value: T;
2754
+ id: indexerTypes.IdKey;
2755
+ context: types.Context;
2756
+ nativeDocumentIndex?: NativeBackboneDocumentIndexCommit<I>;
2757
+ options?: { replace?: boolean };
2758
+ }>,
2759
+ ): Promise<WithIndexedContext<T, I>[] | undefined> {
2760
+ if (values.length === 0) {
2761
+ return [];
2762
+ }
2763
+ const contextualBatchPut = (this.index as ContextualIndexPut<I>)
2764
+ .putWithContextBatch;
2765
+ if (!contextualBatchPut || this.isProgramValued) {
2766
+ return;
2767
+ }
2768
+ const indexedValues: WithIndexedContext<T, I>[] = [];
2769
+ const batchValues: Array<{
2770
+ value: I;
2771
+ id: indexerTypes.IdKey;
2772
+ context: types.Context;
2773
+ options: ContextualPutOptions;
2774
+ }> = [];
2775
+ for (const item of values) {
2776
+ if (!item.nativeDocumentIndex) {
2777
+ return;
2778
+ }
2779
+ item.nativeDocumentIndex.setContext?.(item.context);
2780
+ const valueWithContext = coerceWithContext(item.value, item.context);
2781
+ const indexedValue = item.nativeDocumentIndex.indexable
2782
+ ? coerceWithIndexed(
2783
+ valueWithContext,
2784
+ item.nativeDocumentIndex.indexable,
2785
+ )
2786
+ : item.nativeDocumentIndex.getIndexable
2787
+ ? coerceWithLazyIndexed(
2788
+ valueWithContext,
2789
+ item.nativeDocumentIndex.getIndexable,
2790
+ )
2791
+ : undefined;
2792
+ if (!indexedValue) {
2793
+ return;
2794
+ }
2795
+ const valuePrefixBytes = this.nativeBackboneDocumentIndexValuePrefixBytes(
2796
+ item.nativeDocumentIndex,
2797
+ item.context,
2798
+ );
2799
+ if (!valuePrefixBytes) {
2800
+ return;
2801
+ }
2802
+ this.cacheResolvedValue(item.id.primitive, item.value);
2803
+ indexedValues.push(indexedValue);
2804
+ batchValues.push({
2805
+ // Encoded native batches store from encodedValueParts; descriptor
2806
+ // projections keep the JS indexable lazy for event consumers.
2807
+ value:
2808
+ item.nativeDocumentIndex.indexable ?? (undefined as unknown as I),
2809
+ id: item.id,
2810
+ context: item.context,
2811
+ options: {
2812
+ replace: item.options?.replace,
2813
+ encodedValueParts: {
2814
+ prefix: valuePrefixBytes,
2815
+ suffix: encodeContextSuffix(item.context),
1603
2816
  },
1604
- });
2817
+ },
2818
+ });
2819
+ }
2820
+ const handleError = (error: unknown) => {
2821
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2822
+ return indexedValues;
1605
2823
  }
2824
+ throw error;
2825
+ };
2826
+ try {
2827
+ const putResult = contextualBatchPut.call(this.index, batchValues);
2828
+ return isPromiseLike(putResult)
2829
+ ? putResult.then(() => indexedValues, handleError)
2830
+ : indexedValues;
2831
+ } catch (error) {
2832
+ return handleError(error);
1606
2833
  }
2834
+ }
1607
2835
 
1608
- const initialOptions = baseRemote
1609
- ? ({ ...(options as any), remote: baseRemote } as Options)
1610
- : options;
1611
- const result = (await this.getDetailed(idKey, initialOptions))?.[0]
1612
- ?.results[0];
1613
-
1614
- // if no results, and we have remote joining options, we wait for the timout and if there are joining peers we re-query
1615
- if (!result) {
1616
- return deferred?.promise;
1617
- } else if (deferred) {
1618
- deferred.resolve(undefined);
2836
+ private async _putManyPreparedNativeBackboneDocumentIndexStored(
2837
+ values: Array<{
2838
+ value: T;
2839
+ id: indexerTypes.IdKey;
2840
+ context: types.Context;
2841
+ encodedValueParts?: NonNullable<
2842
+ ContextualPutOptions["encodedValueParts"]
2843
+ >;
2844
+ nativeDocumentIndex?: NativeBackboneDocumentIndexCommit<I>;
2845
+ options?: { replace?: boolean };
2846
+ }>,
2847
+ ): Promise<boolean | undefined> {
2848
+ if (values.length === 0) {
2849
+ return true;
2850
+ }
2851
+ if (this.isProgramValued) {
2852
+ return;
2853
+ }
2854
+ const storedBatchPut = (this.index as ContextualIndexPut<I>)
2855
+ .putStoredContextualEncodedValueBatch;
2856
+ if (!storedBatchPut) {
2857
+ return;
2858
+ }
2859
+ const batchValues: Array<{
2860
+ id: indexerTypes.IdKey;
2861
+ encodedValueParts: NonNullable<
2862
+ ContextualPutOptions["encodedValueParts"]
2863
+ >;
2864
+ options?: { replace?: boolean };
2865
+ }> = [];
2866
+ for (const item of values) {
2867
+ let encodedValueParts:
2868
+ | NonNullable<ContextualPutOptions["encodedValueParts"]>
2869
+ | undefined;
2870
+ if (
2871
+ this.transformerIsIdentity &&
2872
+ this.indexedTypeIsDocumentType &&
2873
+ item.encodedValueParts
2874
+ ) {
2875
+ encodedValueParts = item.encodedValueParts;
2876
+ } else if (item.nativeDocumentIndex) {
2877
+ item.nativeDocumentIndex.setContext?.(item.context);
2878
+ const valuePrefixBytes =
2879
+ item.nativeDocumentIndex.valuePrefixBytes ??
2880
+ this.nativeBackboneDocumentIndexValuePrefixBytes(
2881
+ item.nativeDocumentIndex,
2882
+ item.context,
2883
+ );
2884
+ if (!valuePrefixBytes) {
2885
+ return;
2886
+ }
2887
+ encodedValueParts = {
2888
+ prefix: valuePrefixBytes,
2889
+ suffix: encodeContextSuffix(item.context),
2890
+ };
2891
+ } else {
2892
+ return;
2893
+ }
2894
+ this.cacheResolvedValue(item.id.primitive, item.value);
2895
+ batchValues.push({
2896
+ id: item.id,
2897
+ encodedValueParts,
2898
+ options: item.options,
2899
+ });
2900
+ }
2901
+ const handleError = (error: unknown) => {
2902
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2903
+ return true;
2904
+ }
2905
+ throw error;
2906
+ };
2907
+ try {
2908
+ return await storedBatchPut.call(this.index, batchValues);
2909
+ } catch (error) {
2910
+ return handleError(error);
1619
2911
  }
1620
- return result?.value;
1621
2912
  }
1622
2913
 
1623
- public async getFromGid(gid: string) {
1624
- const iterator = this.index.iterate({ query: { gid } });
1625
- const one = await iterator.next(1);
1626
- await iterator.close();
1627
- return one[0];
1628
- }
2914
+ public async _putManyIdentityWithContext(
2915
+ values: Array<{
2916
+ value: T;
2917
+ id: indexerTypes.IdKey;
2918
+ context: types.Context;
2919
+ options?: ContextualPutOptions;
2920
+ }>,
2921
+ ): Promise<WithIndexedContext<T, I>[] | undefined> {
2922
+ if (values.length === 0) {
2923
+ return [];
2924
+ }
2925
+ const contextualPut = this.transformerIsIdentity
2926
+ ? (this.index as ContextualIndexPut<I>).putWithContext
2927
+ : undefined;
2928
+ const contextualBatchPut = this.transformerIsIdentity
2929
+ ? (this.index as ContextualIndexPut<I>).putWithContextBatch
2930
+ : undefined;
2931
+ if ((!contextualBatchPut && !contextualPut) || this.isProgramValued) {
2932
+ return;
2933
+ }
1629
2934
 
1630
- public async getFromHash(hash: string) {
1631
- const iterator = this.index.iterate({ query: { hash } });
1632
- const one = await iterator.next(1);
1633
- await iterator.close();
1634
- return one[0];
2935
+ const indexedValues = values.map((item) => {
2936
+ const indexable = item.value as any as I;
2937
+ this.cacheResolvedValue(item.id.primitive, item.value);
2938
+ return {
2939
+ indexable,
2940
+ value: coerceWithIndexed(
2941
+ coerceWithContext(item.value, item.context),
2942
+ indexable,
2943
+ ),
2944
+ };
2945
+ });
2946
+
2947
+ try {
2948
+ if (contextualBatchPut) {
2949
+ await contextualBatchPut.call(
2950
+ this.index,
2951
+ values.map((item, index) => ({
2952
+ value: indexedValues[index]!.indexable,
2953
+ id: item.id,
2954
+ context: item.context,
2955
+ options: this.withContextualEncodedValue(
2956
+ item.options,
2957
+ item.context,
2958
+ ),
2959
+ })),
2960
+ );
2961
+ } else {
2962
+ for (let i = 0; i < values.length; i++) {
2963
+ const item = values[i]!;
2964
+ await contextualPut!.call(
2965
+ this.index,
2966
+ indexedValues[i]!.indexable,
2967
+ item.id,
2968
+ item.context,
2969
+ this.withContextualEncodedValue(item.options, item.context),
2970
+ );
2971
+ }
2972
+ }
2973
+ } catch (error) {
2974
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2975
+ return indexedValues.map((item) => item.value);
2976
+ }
2977
+ throw error;
2978
+ }
2979
+ return indexedValues.map((item) => item.value);
1635
2980
  }
2981
+
1636
2982
  public async put(
1637
2983
  value: T,
1638
2984
  id: indexerTypes.IdKey,
@@ -1660,6 +3006,7 @@ export class DocumentIndex<
1660
3006
  });
1661
3007
  return this.putWithContext(value, id, context, {
1662
3008
  replace: existingDefined != null,
3009
+ transformFacts: { entryPublicKeys: entry.publicKeys },
1663
3010
  });
1664
3011
  }
1665
3012
 
@@ -1667,37 +3014,49 @@ export class DocumentIndex<
1667
3014
  value: T,
1668
3015
  id: indexerTypes.IdKey,
1669
3016
  context: types.Context,
1670
- options?: { replace?: boolean },
3017
+ options?: ContextualPutOptions,
1671
3018
  ): Promise<{ context: types.Context; indexable: I }> {
1672
3019
  const idString = id.primitive;
1673
- if (
1674
- this.isProgramValued /*
1675
- TODO should we skip caching program value if they are not openend through this db?
1676
- &&
1677
- (value as Program).closed === false &&
1678
- (value as Program).parents.includes(this._log) */
1679
- ) {
1680
- // TODO make last condition more efficient if there are many docs
1681
- this._resolverProgramCache!.set(idString, value);
1682
- indexCacheLogger("cache:set:program", { id: idString });
1683
- } else {
1684
- if (this._resolverCache) {
1685
- this._resolverCache.add(idString, value);
1686
- indexCacheLogger("cache:set:value", { id: idString });
1687
- }
1688
- }
1689
- const valueToIndex = await this.transformer(value, context);
1690
- const wrappedValueToIndex = new this.wrappedIndexedType(
1691
- valueToIndex as I,
1692
- context,
1693
- );
3020
+ this.cacheResolvedValue(idString, value);
3021
+ const valueToIndex = this.transformerIsIdentity
3022
+ ? (value as any as I)
3023
+ : await this.transformer(value, context, options?.transformFacts);
1694
3024
 
1695
3025
  coerceWithIndexed(value, valueToIndex);
1696
3026
 
1697
3027
  coerceWithContext(value, context);
1698
3028
 
1699
3029
  try {
1700
- await this.index.put(wrappedValueToIndex, undefined, options);
3030
+ const contextualPut = this.transformerIsIdentity
3031
+ ? (this.index as ContextualIndexPut<I>).putWithContext
3032
+ : undefined;
3033
+ if (contextualPut) {
3034
+ const encodedValueParts = this.encodeContextualIndexedValueParts(
3035
+ options?.encodedValue,
3036
+ context,
3037
+ );
3038
+ await contextualPut.call(
3039
+ this.index,
3040
+ valueToIndex,
3041
+ id,
3042
+ context,
3043
+ encodedValueParts
3044
+ ? { ...options, encodedValue: undefined, encodedValueParts }
3045
+ : options?.encodedValue
3046
+ ? { ...options, encodedValue: undefined }
3047
+ : options,
3048
+ );
3049
+ } else {
3050
+ const wrappedValueToIndex = new this.wrappedIndexedType(
3051
+ valueToIndex as I,
3052
+ context,
3053
+ );
3054
+ await this.index.put(
3055
+ wrappedValueToIndex,
3056
+ id,
3057
+ stripEncodedValue(options),
3058
+ );
3059
+ }
1701
3060
  } catch (error) {
1702
3061
  if (error instanceof indexerTypes.NotStartedError && this.closed) {
1703
3062
  return { context, indexable: valueToIndex };
@@ -1707,20 +3066,211 @@ export class DocumentIndex<
1707
3066
  return { context, indexable: valueToIndex };
1708
3067
  }
1709
3068
 
1710
- public del(key: indexerTypes.IdKey) {
1711
- if (this.isProgramValued) {
1712
- this._resolverProgramCache!.delete(key.primitive);
1713
- indexCacheLogger("cache:del:program", { id: key.primitive });
3069
+ public async putManyWithContext(
3070
+ values: Array<{
3071
+ value: T;
3072
+ id: indexerTypes.IdKey;
3073
+ context: types.Context;
3074
+ options?: ContextualPutOptions;
3075
+ }>,
3076
+ ): Promise<Array<{ context: types.Context; indexable: I }>> {
3077
+ if (values.length === 0) {
3078
+ return [];
3079
+ }
3080
+ let transformed: Array<
3081
+ (typeof values)[number] & {
3082
+ indexable: I;
3083
+ }
3084
+ >;
3085
+ if (this.transformerIsIdentity) {
3086
+ transformed = new Array(values.length);
3087
+ for (let i = 0; i < values.length; i++) {
3088
+ const item = values[i]!;
3089
+ this.cacheResolvedValue(item.id.primitive, item.value);
3090
+ const indexable = item.value as any as I;
3091
+ coerceWithIndexed(item.value, indexable);
3092
+ coerceWithContext(item.value, item.context);
3093
+ transformed[i] = { ...item, indexable };
3094
+ }
1714
3095
  } else {
1715
- if (this._resolverCache?.del(key.primitive)) {
1716
- indexCacheLogger("cache:del:value", { id: key.primitive });
3096
+ transformed = await Promise.all(
3097
+ values.map(async (item) => {
3098
+ this.cacheResolvedValue(item.id.primitive, item.value);
3099
+ const indexable = await this.transformer(item.value, item.context);
3100
+ coerceWithIndexed(item.value, indexable);
3101
+ coerceWithContext(item.value, item.context);
3102
+ return { ...item, indexable };
3103
+ }),
3104
+ );
3105
+ }
3106
+
3107
+ try {
3108
+ const contextualBatchPut = this.transformerIsIdentity
3109
+ ? (this.index as ContextualIndexPut<I>).putWithContextBatch
3110
+ : undefined;
3111
+ if (contextualBatchPut) {
3112
+ await contextualBatchPut.call(
3113
+ this.index,
3114
+ transformed.map((item) => ({
3115
+ value: item.indexable,
3116
+ id: item.id,
3117
+ context: item.context,
3118
+ options: this.withContextualEncodedValue(
3119
+ item.options,
3120
+ item.context,
3121
+ ),
3122
+ })),
3123
+ );
3124
+ } else if (
3125
+ transformed.every((item) => item.options?.replace !== true) &&
3126
+ this.index.putBatch
3127
+ ) {
3128
+ await this.index.putBatch(
3129
+ transformed.map(
3130
+ (item) => new this.wrappedIndexedType(item.indexable, item.context),
3131
+ ),
3132
+ );
3133
+ } else {
3134
+ const contextualPut = this.transformerIsIdentity
3135
+ ? (this.index as ContextualIndexPut<I>).putWithContext
3136
+ : undefined;
3137
+ for (const item of transformed) {
3138
+ if (contextualPut) {
3139
+ await contextualPut.call(
3140
+ this.index,
3141
+ item.indexable,
3142
+ item.id,
3143
+ item.context,
3144
+ this.withContextualEncodedValue(item.options, item.context),
3145
+ );
3146
+ } else {
3147
+ await this.index.put(
3148
+ new this.wrappedIndexedType(item.indexable, item.context),
3149
+ item.id,
3150
+ stripEncodedValue(item.options),
3151
+ );
3152
+ }
3153
+ }
3154
+ }
3155
+ } catch (error) {
3156
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
3157
+ return transformed.map((item) => ({
3158
+ context: item.context,
3159
+ indexable: item.indexable,
3160
+ }));
1717
3161
  }
3162
+ throw error;
3163
+ }
3164
+
3165
+ return transformed.map((item) => ({
3166
+ context: item.context,
3167
+ indexable: item.indexable,
3168
+ }));
3169
+ }
3170
+
3171
+ public _cacheResolvedIdentityValue(
3172
+ id: string | number | bigint,
3173
+ value: T,
3174
+ ): void {
3175
+ this.cacheResolvedValue(id, value);
3176
+ }
3177
+
3178
+ private cacheResolvedValue(id: string | number | bigint, value: T): void {
3179
+ if (this.isProgramValued) {
3180
+ this._resolverProgramCache!.set(id, value);
3181
+ indexCacheLogger("cache:set:program", { id });
3182
+ } else if (this._resolverCache) {
3183
+ this._resolverCache.add(id, value);
3184
+ indexCacheLogger("cache:set:value", { id });
3185
+ }
3186
+ }
3187
+
3188
+ private withContextualEncodedValue(
3189
+ options: ContextualPutOptions | undefined,
3190
+ context: types.Context,
3191
+ ): ContextualPutOptions | undefined {
3192
+ if (!options?.encodedValue) {
3193
+ return options;
3194
+ }
3195
+ const encodedValueParts = this.encodeContextualIndexedValueParts(
3196
+ options.encodedValue,
3197
+ context,
3198
+ );
3199
+ return encodedValueParts
3200
+ ? { ...options, encodedValue: undefined, encodedValueParts }
3201
+ : options;
3202
+ }
3203
+
3204
+ private encodeContextualIndexedValueParts(
3205
+ encodedValue: Uint8Array | undefined,
3206
+ context: types.Context,
3207
+ ): ContextualPutOptions["encodedValueParts"] | undefined {
3208
+ if (
3209
+ !encodedValue ||
3210
+ !this.transformerIsIdentity ||
3211
+ !this.indexedTypeIsDocumentType
3212
+ ) {
3213
+ return;
1718
3214
  }
3215
+ return {
3216
+ prefix: encodedValue,
3217
+ suffix: encodeContextSuffix(context),
3218
+ };
3219
+ }
3220
+
3221
+ public del(key: indexerTypes.IdKey) {
3222
+ this.deleteResolvedCacheForKey(key);
1719
3223
  return this.index.del({
1720
3224
  query: [indexerTypes.getMatcher(this.indexBy, key.key)],
1721
3225
  });
1722
3226
  }
1723
3227
 
3228
+ public async delMany(keys: indexerTypes.IdKey[]): Promise<void> {
3229
+ if (keys.length === 0) {
3230
+ return;
3231
+ }
3232
+ for (const key of keys) {
3233
+ this.deleteResolvedCacheForKey(key);
3234
+ }
3235
+ const delIdsNoReturn = (this.index as ExactDeleteIndex).delIdsNoReturn;
3236
+ if (delIdsNoReturn) {
3237
+ await delIdsNoReturn.call(this.index, keys);
3238
+ return;
3239
+ }
3240
+ const delIds = (this.index as ExactDeleteIndex).delIds;
3241
+ if (delIds) {
3242
+ await delIds.call(this.index, keys);
3243
+ return;
3244
+ }
3245
+ await Promise.all(keys.map((key) => this.del(key)));
3246
+ }
3247
+
3248
+ public clearResolvedCacheForKeys(keys: indexerTypes.IdKey[]): void {
3249
+ for (const key of keys) {
3250
+ this.deleteResolvedCacheForKey(key);
3251
+ }
3252
+ }
3253
+
3254
+ public delManyMaybe(keys: indexerTypes.IdKey[]): MaybePromise<void> {
3255
+ if (keys.length === 0) {
3256
+ return;
3257
+ }
3258
+ for (const key of keys) {
3259
+ this.deleteResolvedCacheForKey(key);
3260
+ }
3261
+ const delIdsNoReturn = (this.index as ExactDeleteIndex).delIdsNoReturn;
3262
+ if (delIdsNoReturn) {
3263
+ const result = delIdsNoReturn.call(this.index, keys);
3264
+ return isPromiseLike(result) ? result.then(() => undefined) : undefined;
3265
+ }
3266
+ const delIds = (this.index as ExactDeleteIndex).delIds;
3267
+ if (delIds) {
3268
+ const result = delIds.call(this.index, keys);
3269
+ return isPromiseLike(result) ? result.then(() => undefined) : undefined;
3270
+ }
3271
+ return Promise.all(keys.map((key) => this.del(key))).then(() => undefined);
3272
+ }
3273
+
1724
3274
  public async getDetailed<
1725
3275
  Options extends QueryOptions<T, I, D, Resolve>,
1726
3276
  Resolve extends boolean | undefined = ExtractResolveFromOptions<Options>,
@@ -2198,15 +3748,18 @@ export class DocumentIndex<
2198
3748
  }
2199
3749
 
2200
3750
  get countIteratorsInProgress() {
2201
- return this._resumableIterators.queues.size;
3751
+ return this._resumableIterators?.queues.size ?? 0;
2202
3752
  }
2203
3753
 
2204
3754
  private clearAllResultQueues() {
3755
+ if (!this._resultQueue) {
3756
+ return;
3757
+ }
2205
3758
  for (const [key, queue] of this._resultQueue) {
2206
3759
  clearTimeout(queue.timeout);
2207
3760
  this._resultQueue.delete(key);
2208
3761
  this.cancelIteratorKeepAlive(key);
2209
- this._resumableIterators.close({ idString: key });
3762
+ this._resumableIterators?.close({ idString: key });
2210
3763
  }
2211
3764
  }
2212
3765
 
@@ -2501,9 +4054,10 @@ export class DocumentIndex<
2501
4054
  signal: options?.signal,
2502
4055
  });
2503
4056
 
2504
- // Cold start: cover can be temporarily self-only while replication metadata
2505
- // converges. For explicit remote searches, query bounded connected peers
2506
- // instead of waiting for replicator metadata to catch up.
4057
+ // Cold start: cover can be temporarily self-only or empty while
4058
+ // replication metadata converges. For explicit bounded remote searches,
4059
+ // query bounded connected peers instead of waiting for replicator
4060
+ // metadata to catch up.
2507
4061
  if (!options?.remote?.from && isDefaultDomainArgs && remoteWasExplicit) {
2508
4062
  const selfHash = this.node.identity.publicKey.hashcode();
2509
4063
  const remoteCount = replicatorGroups.filter(
@@ -2513,25 +4067,53 @@ export class DocumentIndex<
2513
4067
  const waitEnabled = Boolean(remote.wait);
2514
4068
  const coverIsSelfOnly =
2515
4069
  replicatorGroups.length === 1 && replicatorGroups[0] === selfHash;
4070
+ const coverIsColdEmpty =
4071
+ replicatorGroups.length === 0 && remote.timeout != null;
4072
+
4073
+ // If the cover is explicitly empty (no shards), don't override it
4074
+ // unless the caller requested waiting for joins (e.g. get(waitFor))
4075
+ // or bounded the remote query with a timeout.
4076
+ if (waitEnabled || coverIsSelfOnly || coverIsColdEmpty) {
4077
+ const extra: string[] = [];
4078
+ const addExtra = (hash: string | undefined) => {
4079
+ if (!hash || hash === selfHash || extra.includes(hash)) {
4080
+ return;
4081
+ }
4082
+ extra.push(hash);
4083
+ };
2516
4084
 
2517
- // If the cover is explicitly empty (no shards), don't override it unless
2518
- // the caller requested waiting for joins (e.g. get(waitFor)).
2519
- if (waitEnabled || coverIsSelfOnly) {
2520
4085
  const peerMap: Map<string, unknown> | undefined = (
2521
4086
  this.node.services.pubsub as any
2522
4087
  )?.peers;
4088
+
4089
+ // Only consider replicators that are currently reachable.
4090
+ // The replicator index can contain stale (offline) peers, e.g.
4091
+ // persisted replicators after a restart; querying those would
4092
+ // block the first batch for the full wait timeout instead of
4093
+ // letting joining peers be merged as they arrive.
4094
+ if (peerMap?.has) {
4095
+ try {
4096
+ for (const hash of await this._log.getReplicators()) {
4097
+ if (peerMap.has(hash)) {
4098
+ addExtra(hash);
4099
+ }
4100
+ if (extra.length >= 8) break;
4101
+ }
4102
+ } catch {
4103
+ // Fall through to connected peers when the local replicator
4104
+ // index is not ready yet.
4105
+ }
4106
+ }
4107
+
2523
4108
  if (peerMap?.keys) {
2524
- const extra: string[] = [];
2525
4109
  for (const hash of peerMap.keys()) {
2526
- if (!hash || hash === selfHash) continue;
2527
- extra.push(hash);
4110
+ addExtra(hash);
2528
4111
  if (extra.length >= 8) break;
2529
4112
  }
2530
- if (extra.length > 0) {
2531
- replicatorGroups = [
2532
- ...new Set([...replicatorGroups, ...extra]),
2533
- ];
2534
- }
4113
+ }
4114
+
4115
+ if (extra.length > 0) {
4116
+ replicatorGroups = [...new Set([...replicatorGroups, ...extra])];
2535
4117
  }
2536
4118
  }
2537
4119
  }
@@ -3084,6 +4666,53 @@ export class DocumentIndex<
3084
4666
  return indexedPlaceholders;
3085
4667
  };
3086
4668
 
4669
+ // The `visited` set prevents re-emitting the same document across pages
4670
+ // and sources, but different sources can return different versions of the
4671
+ // same document (e.g. a stale local head merged before a newer remote
4672
+ // head arrives). Plain id-based dedupe would let the first-seen, stale
4673
+ // version permanently shadow the newer one. If the id is still buffered
4674
+ // (not yet handed to the consumer) and the incoming result is strictly
4675
+ // preferred by the store's conflict rule, evict the stale buffered entry
4676
+ // so the caller can buffer the preferred result in its place. Returns
4677
+ // true when the caller should proceed to buffer the incoming result.
4678
+ // Results that were already emitted to the consumer cannot be retracted
4679
+ // here; those are only corrected via live updates/replication.
4680
+ // The preference direction mirrors the index merge rule in program.ts:
4681
+ // newest wins for mutable stores, oldest wins for immutable stores.
4682
+ const isPreferredContext = (
4683
+ incoming: types.Context,
4684
+ existing: types.Context,
4685
+ ): boolean =>
4686
+ incoming.head !== existing.head &&
4687
+ (this.immutable
4688
+ ? incoming.modified < existing.modified
4689
+ : incoming.modified > existing.modified);
4690
+
4691
+ const evictStaleBuffered = (
4692
+ indexKey: indexerTypes.IdPrimitive,
4693
+ incomingContext: types.Context,
4694
+ ): boolean => {
4695
+ for (const peerBuffer of peerBufferMap.values()) {
4696
+ for (let i = 0; i < peerBuffer.buffer.length; i++) {
4697
+ const existing = peerBuffer.buffer[i];
4698
+ const existingKey = indexerTypes.toId(
4699
+ this.indexByResolver(existing.indexed),
4700
+ ).primitive;
4701
+ if (existingKey !== indexKey) {
4702
+ continue;
4703
+ }
4704
+ if (!isPreferredContext(incomingContext, existing.context)) {
4705
+ // same or non-preferred version: keep normal dedupe behavior
4706
+ return false;
4707
+ }
4708
+ peerBuffer.buffer.splice(i, 1);
4709
+ indexedPlaceholders?.delete(indexKey);
4710
+ return true;
4711
+ }
4712
+ }
4713
+ return false; // not buffered (already emitted): skip incoming
4714
+ };
4715
+
3087
4716
  let done = false;
3088
4717
  let drain = false; // if true, close on empty once (overrides manual)
3089
4718
  let first = false;
@@ -3342,7 +4971,10 @@ export class DocumentIndex<
3342
4971
  indexedPlaceholders?.delete(indexKey);
3343
4972
  continue;
3344
4973
  }
3345
- if (visited.has(indexKey)) {
4974
+ if (
4975
+ visited.has(indexKey) &&
4976
+ !evictStaleBuffered(indexKey, result.context)
4977
+ ) {
3346
4978
  continue;
3347
4979
  }
3348
4980
  visited.add(indexKey);
@@ -3361,7 +4993,8 @@ export class DocumentIndex<
3361
4993
  result as unknown as types.ResultIndexedValue<I>;
3362
4994
  if (
3363
4995
  visited.has(indexKey) &&
3364
- !indexedPlaceholders?.has(indexKey)
4996
+ !indexedPlaceholders?.has(indexKey) &&
4997
+ !evictStaleBuffered(indexKey, indexedResult.context)
3365
4998
  ) {
3366
4999
  continue;
3367
5000
  }
@@ -3548,7 +5181,10 @@ export class DocumentIndex<
3548
5181
  indexedPlaceholders?.delete(keyPrimitive);
3549
5182
  continue;
3550
5183
  }
3551
- if (visited.has(keyPrimitive)) {
5184
+ if (
5185
+ visited.has(keyPrimitive) &&
5186
+ !evictStaleBuffered(keyPrimitive, result.context)
5187
+ ) {
3552
5188
  continue;
3553
5189
  }
3554
5190
  visited.add(keyPrimitive);
@@ -3575,7 +5211,11 @@ export class DocumentIndex<
3575
5211
  result as unknown as types.ResultIndexedValue<I>;
3576
5212
  if (
3577
5213
  visited.has(keyPrimitive) &&
3578
- !indexedPlaceholders?.has(keyPrimitive)
5214
+ !indexedPlaceholders?.has(keyPrimitive) &&
5215
+ !evictStaleBuffered(
5216
+ keyPrimitive,
5217
+ indexedResult.context,
5218
+ )
3579
5219
  ) {
3580
5220
  continue;
3581
5221
  }
@@ -3700,7 +5340,10 @@ export class DocumentIndex<
3700
5340
  indexedPlaceholders?.delete(indexKey);
3701
5341
  continue;
3702
5342
  }
3703
- if (visited.has(indexKey)) {
5343
+ if (
5344
+ visited.has(indexKey) &&
5345
+ !evictStaleBuffered(indexKey, result.context)
5346
+ ) {
3704
5347
  continue;
3705
5348
  }
3706
5349
  visited.add(indexKey);
@@ -3730,7 +5373,11 @@ export class DocumentIndex<
3730
5373
  result as unknown as types.ResultIndexedValue<I>;
3731
5374
  if (
3732
5375
  visited.has(indexKey) &&
3733
- !indexedPlaceholders?.has(indexKey)
5376
+ !indexedPlaceholders?.has(indexKey) &&
5377
+ !evictStaleBuffered(
5378
+ indexKey,
5379
+ indexedResult.context,
5380
+ )
3734
5381
  ) {
3735
5382
  continue;
3736
5383
  }
@@ -3805,6 +5452,8 @@ export class DocumentIndex<
3805
5452
  return [];
3806
5453
  }
3807
5454
 
5455
+ await pendingUpdateProcessing;
5456
+
3808
5457
  const bufferedBeforeFetch = peerBuffers().length;
3809
5458
  const localHash = this.node.identity.publicKey.hashcode();
3810
5459
  const hasBufferedRemoteResults = [...peerBufferMap.entries()].some(
@@ -4087,6 +5736,7 @@ export class DocumentIndex<
4087
5736
  | Extract<UpdateReason, "join" | "change" | "push">
4088
5737
  | undefined;
4089
5738
  let hasDeliveredResults = false;
5739
+ let pendingUpdateProcessing: Promise<void> = Promise.resolve();
4090
5740
 
4091
5741
  const emitOnBatch = async (
4092
5742
  batch: ValueTypeFromRequest<Resolve, T, I>[],
@@ -4381,7 +6031,7 @@ export class DocumentIndex<
4381
6031
  return value as WithContext<I>;
4382
6032
  };
4383
6033
 
4384
- const onChange = async (evt: CustomEvent<DocumentsChange<T, I>>) => {
6034
+ const processChange = async (evt: CustomEvent<DocumentsChange<T, I>>) => {
4385
6035
  // Optional filter to mutate/suppress change events
4386
6036
  indexIteratorLogger.trace(
4387
6037
  "processing live update change event",
@@ -4523,6 +6173,13 @@ export class DocumentIndex<
4523
6173
  }
4524
6174
  signalUpdate();
4525
6175
  };
6176
+ const onChange = (evt: CustomEvent<DocumentsChange<T, I>>) => {
6177
+ const task = pendingUpdateProcessing.then(() => processChange(evt));
6178
+ pendingUpdateProcessing = task.catch((error) => {
6179
+ warn("Failed to process iterator update", error);
6180
+ });
6181
+ return task;
6182
+ };
4526
6183
 
4527
6184
  this.documentEvents.addEventListener("change", onChange);
4528
6185
  updatesCleanup = () => {