@peerbit/document 13.0.44 → 13.1.0

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>
@@ -661,6 +866,151 @@ type IndexableClass<I> = new (
661
866
  context: types.Context,
662
867
  ) => WithContext<I>;
663
868
 
869
+ type ContextualPutOptions = {
870
+ replace?: boolean;
871
+ encodedValue?: Uint8Array;
872
+ encodedValueParts?: {
873
+ prefix: Uint8Array;
874
+ suffix: Uint8Array;
875
+ };
876
+ transformFacts?: DocumentTransformFacts;
877
+ };
878
+
879
+ const stripEncodedValue = (
880
+ options: ContextualPutOptions | undefined,
881
+ ): { replace?: boolean } | undefined =>
882
+ options?.encodedValue || options?.encodedValueParts || options?.transformFacts
883
+ ? { replace: options.replace }
884
+ : options;
885
+
886
+ const writeU32Le = (target: Uint8Array, offset: number, value: number) => {
887
+ target[offset] = value & 0xff;
888
+ target[offset + 1] = (value >>> 8) & 0xff;
889
+ target[offset + 2] = (value >>> 16) & 0xff;
890
+ target[offset + 3] = (value >>> 24) & 0xff;
891
+ return offset + 4;
892
+ };
893
+
894
+ const writeU64Le = (target: Uint8Array, offset: number, value: bigint) => {
895
+ let remaining = value;
896
+ for (let i = 0; i < 8; i++) {
897
+ target[offset + i] = Number(remaining & 0xffn);
898
+ remaining >>= 8n;
899
+ }
900
+ return offset + 8;
901
+ };
902
+
903
+ export const encodeContextSuffix = (context: types.Context): Uint8Array => {
904
+ const head = fromString(context.head);
905
+ const gid = fromString(context.gid);
906
+ const encoded = new Uint8Array(
907
+ 1 + 8 + 8 + 4 + head.byteLength + 4 + gid.byteLength + 4,
908
+ );
909
+ let offset = 0;
910
+ // Context is @variant(0); keep this byte-for-byte aligned with Borsh.
911
+ encoded[offset++] = 0;
912
+ offset = writeU64Le(encoded, offset, context.created);
913
+ offset = writeU64Le(encoded, offset, context.modified);
914
+ offset = writeU32Le(encoded, offset, head.byteLength);
915
+ encoded.set(head, offset);
916
+ offset += head.byteLength;
917
+ offset = writeU32Le(encoded, offset, gid.byteLength);
918
+ encoded.set(gid, offset);
919
+ offset += gid.byteLength;
920
+ writeU32Le(encoded, offset, context.size);
921
+ return encoded;
922
+ };
923
+
924
+ type ContextualIndexPut<I> = {
925
+ putWithContext?: (
926
+ value: I,
927
+ id: indexerTypes.IdKey,
928
+ context: types.Context,
929
+ options?: ContextualPutOptions,
930
+ ) => Promise<void> | void;
931
+ putStoredContextualEncodedValue?: (
932
+ id: indexerTypes.IdKey,
933
+ encodedValueParts: {
934
+ prefix: Uint8Array;
935
+ suffix: Uint8Array;
936
+ },
937
+ options?: { replace?: boolean },
938
+ ) => Promise<void> | void | false;
939
+ persistStoredContextualEncodedValue?: (
940
+ id: indexerTypes.IdKey,
941
+ encodedValueParts: {
942
+ prefix: Uint8Array;
943
+ suffix: Uint8Array;
944
+ },
945
+ options?: { replace?: boolean },
946
+ ) => Promise<void> | void | false;
947
+ putStoredContextualEncodedValueBatch?: (
948
+ values: Array<{
949
+ id: indexerTypes.IdKey;
950
+ encodedValueParts: {
951
+ prefix: Uint8Array;
952
+ suffix: Uint8Array;
953
+ };
954
+ options?: { replace?: boolean };
955
+ }>,
956
+ ) => Promise<boolean> | boolean;
957
+ putWithContextBatch?: (
958
+ values: Array<{
959
+ value: I;
960
+ id: indexerTypes.IdKey;
961
+ context: types.Context;
962
+ options?: ContextualPutOptions;
963
+ }>,
964
+ ) => Promise<void> | void;
965
+ };
966
+
967
+ type ContextHeadIndex<I> = {
968
+ getByContextHead?: (
969
+ head: string,
970
+ ) => indexerTypes.IndexedResult<WithContext<I>> | undefined;
971
+ getByContextHeadBatch?: (
972
+ heads: string[],
973
+ ) => Array<indexerTypes.IndexedResult<WithContext<I>> | undefined>;
974
+ getIdByContextHead?: (head: string) => indexerTypes.IdKey | undefined;
975
+ };
976
+
977
+ type ExactDeleteIndex = {
978
+ delIdsNoReturn?: (
979
+ deleteIds: Array<indexerTypes.IdKey | indexerTypes.Ideable>,
980
+ ) => Promise<void> | void;
981
+ delIds?: (
982
+ deleteIds: Array<indexerTypes.IdKey | indexerTypes.Ideable>,
983
+ ) => Promise<indexerTypes.IdKey[]> | indexerTypes.IdKey[];
984
+ };
985
+
986
+ type NativeBackboneDocumentIndex = {
987
+ attachNativeBackboneDocumentIndex?: (
988
+ backbone: unknown,
989
+ options?: { preserveExisting?: boolean },
990
+ ) => boolean | void;
991
+ };
992
+
993
+ type NativeBackboneDocumentProjection = {
994
+ projectDocumentIndexSimple?: (
995
+ encodedDocument: Uint8Array,
996
+ plan: SimpleDocumentProjectionPlan,
997
+ context: SimpleDocumentProjectionContext,
998
+ ) => Uint8Array | undefined;
999
+ };
1000
+
1001
+ type NativeBackboneDocumentIndexCommit<I> = {
1002
+ valuePrefixBytes?: Uint8Array;
1003
+ usePlainPutPayload?: boolean;
1004
+ projection?: {
1005
+ encodedDocument: Uint8Array;
1006
+ plan: SimpleDocumentProjectionPlan;
1007
+ signer?: Uint8Array;
1008
+ };
1009
+ indexable?: I;
1010
+ getIndexable?: () => I;
1011
+ setContext?: (context: types.Context) => void;
1012
+ };
1013
+
664
1014
  export const coerceWithContext = <T>(
665
1015
  value: T | WithContext<T>,
666
1016
  context: types.Context,
@@ -679,6 +1029,36 @@ export const coerceWithIndexed = <T, I>(
679
1029
  return valueWithContext;
680
1030
  };
681
1031
 
1032
+ export const coerceWithLazyIndexed = <T, I>(
1033
+ value: T | WithIndexedContext<T, I>,
1034
+ getIndexable: () => I,
1035
+ ): WithIndexedContext<T, I> => {
1036
+ let cached: I | undefined;
1037
+ let hasCached = false;
1038
+ Object.defineProperty(value, "__indexed", {
1039
+ configurable: true,
1040
+ enumerable: true,
1041
+ get() {
1042
+ if (!hasCached) {
1043
+ cached = getIndexable();
1044
+ hasCached = true;
1045
+ Object.defineProperty(value, "__indexed", {
1046
+ configurable: true,
1047
+ enumerable: true,
1048
+ writable: true,
1049
+ value: cached,
1050
+ });
1051
+ }
1052
+ return cached;
1053
+ },
1054
+ set(indexed: I) {
1055
+ cached = indexed;
1056
+ hasCached = true;
1057
+ },
1058
+ });
1059
+ return value as WithIndexedContext<T, I>;
1060
+ };
1061
+
682
1062
  @variant("documents_index")
683
1063
  export class DocumentIndex<
684
1064
  T,
@@ -693,6 +1073,10 @@ export class DocumentIndex<
693
1073
 
694
1074
  // transform options
695
1075
  transformer: Transformer<T, I>;
1076
+ private transformerIsIdentity = false;
1077
+ private nativeTransformDescriptor?: DocumentTransformDescriptor;
1078
+ private nativeTransformProjectionPlan?: SimpleDocumentProjectionPlan;
1079
+ private nativeBackboneDocumentProjection?: NativeBackboneDocumentProjection;
696
1080
 
697
1081
  // The indexed document wrapped in a context
698
1082
  wrappedIndexedType: IndexableClass<I>;
@@ -757,11 +1141,21 @@ export class DocumentIndex<
757
1141
  >;
758
1142
  private iteratorKeepAliveTimers?: Map<string, ReturnType<typeof setTimeout>>;
759
1143
 
1144
+ private deleteResolvedCacheForKey(key: indexerTypes.IdKey): void {
1145
+ if (this.isProgramValued) {
1146
+ this._resolverProgramCache!.delete(key.primitive);
1147
+ indexCacheLogger("cache:del:program", { id: key.primitive });
1148
+ } else if (this._resolverCache?.del(key.primitive)) {
1149
+ indexCacheLogger("cache:del:value", { id: key.primitive });
1150
+ }
1151
+ }
1152
+
760
1153
  constructor(properties?: {
761
1154
  query?: RPC<types.AbstractSearchRequest, types.AbstractSearchResult>;
762
1155
  }) {
763
1156
  super();
764
1157
  this._query = properties?.query || new RPC();
1158
+ this._resultQueue = new Map();
765
1159
  this.iteratorKeepAliveTimers = new Map();
766
1160
  }
767
1161
 
@@ -903,9 +1297,16 @@ export class DocumentIndex<
903
1297
  return results;
904
1298
  }
905
1299
 
906
- private handleDocumentChange = async (
1300
+ // Bound in open(). Deserialized instances skip constructor/field initializers
1301
+ // (borsh creates objects via Object.create), so this must not rely on a field
1302
+ // initializer to exist.
1303
+ private handleDocumentChange?: (
907
1304
  event: CustomEvent<DocumentsChange<T, I>>,
908
- ) => {
1305
+ ) => Promise<void>;
1306
+
1307
+ private async onDocumentChange(
1308
+ event: CustomEvent<DocumentsChange<T, I>>,
1309
+ ): Promise<void> {
909
1310
  const added = event.detail.added;
910
1311
  if (!added.length) {
911
1312
  return;
@@ -980,7 +1381,7 @@ export class DocumentIndex<
980
1381
  queue.pushInFlight = false;
981
1382
  }
982
1383
  }
983
- };
1384
+ }
984
1385
 
985
1386
  private get nestedProperties() {
986
1387
  return {
@@ -1093,9 +1494,25 @@ export class DocumentIndex<
1093
1494
  };
1094
1495
 
1095
1496
  const transformOptions = properties.transform;
1497
+ const hasTransformFunction =
1498
+ transformOptions != null && isTransformerWithFunction(transformOptions);
1499
+ this.nativeTransformDescriptor = hasTransformFunction
1500
+ ? getDocumentTransformDescriptor(transformOptions.transform)
1501
+ : undefined;
1502
+ this.nativeTransformProjectionPlan = createSimpleProjectionPlan(
1503
+ getSchema(this.documentType),
1504
+ indexedSchema,
1505
+ this.nativeTransformDescriptor,
1506
+ );
1507
+ this.transformerIsIdentity =
1508
+ transformOptions == null ||
1509
+ (!hasTransformFunction && transformOptions.type == null) ||
1510
+ (this.nativeTransformDescriptor?.kind === "identity" &&
1511
+ this.indexedTypeIsDocumentType);
1096
1512
  this.transformer = transformOptions
1097
- ? isTransformerWithFunction(transformOptions)
1098
- ? (obj, context) => transformOptions.transform(obj, context)
1513
+ ? hasTransformFunction
1514
+ ? (obj, context, facts) =>
1515
+ transformOptions.transform(obj, context, facts)
1099
1516
  : transformOptions.type
1100
1517
  ? (obj, context) => new transformOptions.type!(obj, context)
1101
1518
  : (obj) => obj as any as I
@@ -1210,9 +1627,307 @@ export class DocumentIndex<
1210
1627
  responseType: types.AbstractSearchResult,
1211
1628
  queryType: types.AbstractSearchRequest,
1212
1629
  });
1213
- if (this.handleDocumentChange) {
1214
- this.documentEvents.addEventListener("change", this.handleDocumentChange);
1630
+ this.handleDocumentChange ??= (event) => this.onDocumentChange(event);
1631
+ this.documentEvents.addEventListener("change", this.handleDocumentChange);
1632
+ }
1633
+
1634
+ private attachNativeBackboneDocumentIndex(
1635
+ backbone: unknown,
1636
+ options?: { preserveExisting?: boolean },
1637
+ ): boolean {
1638
+ this.nativeBackboneDocumentProjection = undefined;
1639
+ if (
1640
+ !backbone ||
1641
+ this.isProgramValued ||
1642
+ !this.canUseNativeBackboneDocumentIndex()
1643
+ ) {
1644
+ return false;
1645
+ }
1646
+ const attach = (this.index as NativeBackboneDocumentIndex)
1647
+ .attachNativeBackboneDocumentIndex;
1648
+ const attached =
1649
+ typeof attach === "function" &&
1650
+ attach.call(this.index, backbone, options) === true;
1651
+ if (attached) {
1652
+ const projection = backbone as NativeBackboneDocumentProjection;
1653
+ if (typeof projection.projectDocumentIndexSimple === "function") {
1654
+ this.nativeBackboneDocumentProjection = projection;
1655
+ }
1656
+ }
1657
+ return attached;
1658
+ }
1659
+
1660
+ private canUseNativeBackboneDocumentIndex(): boolean {
1661
+ return (
1662
+ (this.transformerIsIdentity && this.indexedTypeIsDocumentType) ||
1663
+ this.nativeTransformDescriptor != null
1664
+ );
1665
+ }
1666
+
1667
+ private getNativeDocumentFieldExtractionPlan(
1668
+ path: string | readonly string[],
1669
+ ): SimpleDocumentFieldExtractionPlan | undefined {
1670
+ return createSimpleFieldExtractionPlan(getSchema(this.documentType), path);
1671
+ }
1672
+
1673
+ private canPrepareNativeBackboneDocumentIndexCommit(): boolean {
1674
+ return (
1675
+ (this.transformerIsIdentity && this.indexedTypeIsDocumentType) ||
1676
+ canPrepareDocumentTransformBeforeAppend(
1677
+ this.nativeTransformDescriptor,
1678
+ )
1679
+ );
1680
+ }
1681
+
1682
+ private canPrepareNativeBackboneDocumentIndexCommitWithAppendFacts(): boolean {
1683
+ return (
1684
+ this.canPrepareNativeBackboneDocumentIndexCommit() ||
1685
+ !!this.nativeTransformProjectionPlan ||
1686
+ canPrepareDocumentTransformWithAppendFacts(
1687
+ this.nativeTransformDescriptor,
1688
+ )
1689
+ );
1690
+ }
1691
+
1692
+ private canUseNativeBackboneContextualBatch(): boolean {
1693
+ return (
1694
+ !this.isProgramValued &&
1695
+ typeof (this.index as ContextualIndexPut<I>).putWithContextBatch ===
1696
+ "function" &&
1697
+ ((this.transformerIsIdentity && this.indexedTypeIsDocumentType) ||
1698
+ this.canPrepareNativeBackboneDocumentIndexCommitWithAppendFacts())
1699
+ );
1700
+ }
1701
+
1702
+ private prepareNativeBackboneDocumentIndexCommit(
1703
+ value: T,
1704
+ encodedDocument: Uint8Array,
1705
+ transformFacts?: DocumentTransformFacts,
1706
+ ): MaybePromise<NativeBackboneDocumentIndexCommit<I> | undefined> {
1707
+ if (this.transformerIsIdentity && this.indexedTypeIsDocumentType) {
1708
+ return {
1709
+ valuePrefixBytes: encodedDocument,
1710
+ usePlainPutPayload: true,
1711
+ indexable: value as any as I,
1712
+ };
1713
+ }
1714
+ if (this.nativeTransformProjectionPlan) {
1715
+ let projectionContext: types.Context | undefined;
1716
+ let cached: I | undefined;
1717
+ let hasCached = false;
1718
+ return {
1719
+ projection: {
1720
+ encodedDocument,
1721
+ plan: this.nativeTransformProjectionPlan,
1722
+ signer: transformFacts?.entryPublicKeys?.[0]?.bytes,
1723
+ },
1724
+ getIndexable: () => {
1725
+ if (!hasCached) {
1726
+ const transformed = this.transformer(
1727
+ value,
1728
+ projectionContext as types.Context,
1729
+ transformFacts,
1730
+ );
1731
+ if (isPromiseLike(transformed)) {
1732
+ throw new Error(
1733
+ "Native descriptor transform unexpectedly returned a promise",
1734
+ );
1735
+ }
1736
+ cached = transformed;
1737
+ hasCached = true;
1738
+ }
1739
+ return cached!;
1740
+ },
1741
+ setContext: (context) => {
1742
+ projectionContext = context;
1743
+ hasCached = false;
1744
+ cached = undefined;
1745
+ },
1746
+ };
1747
+ }
1748
+ if (
1749
+ !canPrepareDocumentTransformBeforeAppend(
1750
+ this.nativeTransformDescriptor,
1751
+ )
1752
+ ) {
1753
+ return;
1754
+ }
1755
+ const transformed = this.transformer(
1756
+ value,
1757
+ undefined as unknown as types.Context,
1758
+ transformFacts,
1759
+ );
1760
+ const finish = (indexable: I): NativeBackboneDocumentIndexCommit<I> => ({
1761
+ valuePrefixBytes: serialize(this.asIndexedTypeValue(indexable)),
1762
+ indexable,
1763
+ });
1764
+ return isPromiseLike(transformed)
1765
+ ? transformed.then(finish)
1766
+ : finish(transformed);
1767
+ }
1768
+
1769
+ private prepareNativeBackboneDocumentIndexCommitWithAppendFacts(
1770
+ value: T,
1771
+ encodedDocument: Uint8Array,
1772
+ context: types.Context,
1773
+ transformFacts?: DocumentTransformFacts,
1774
+ ): NativeBackboneDocumentIndexCommit<I> | undefined {
1775
+ if (this.transformerIsIdentity && this.indexedTypeIsDocumentType) {
1776
+ return {
1777
+ valuePrefixBytes: encodedDocument,
1778
+ usePlainPutPayload: true,
1779
+ indexable: value as any as I,
1780
+ };
1781
+ }
1782
+ if (this.nativeTransformProjectionPlan) {
1783
+ let projectionContext = {
1784
+ created: context.created,
1785
+ modified: context.modified,
1786
+ head: context.head,
1787
+ gid: context.gid,
1788
+ size: context.size,
1789
+ signer: transformFacts?.entryPublicKeys?.[0]?.bytes,
1790
+ };
1791
+ let cached: I | undefined;
1792
+ let hasCached = false;
1793
+ return {
1794
+ projection: {
1795
+ encodedDocument,
1796
+ plan: this.nativeTransformProjectionPlan,
1797
+ signer: projectionContext.signer,
1798
+ },
1799
+ getIndexable: () => {
1800
+ if (!hasCached) {
1801
+ const transformed = this.transformer(
1802
+ value,
1803
+ projectionContext as types.Context,
1804
+ transformFacts,
1805
+ );
1806
+ if (isPromiseLike(transformed)) {
1807
+ throw new Error(
1808
+ "Native descriptor transform unexpectedly returned a promise",
1809
+ );
1810
+ }
1811
+ cached = transformed;
1812
+ hasCached = true;
1813
+ }
1814
+ return cached!;
1815
+ },
1816
+ setContext: (nextContext) => {
1817
+ projectionContext = {
1818
+ created: nextContext.created,
1819
+ modified: nextContext.modified,
1820
+ head: nextContext.head,
1821
+ gid: nextContext.gid,
1822
+ size: nextContext.size,
1823
+ signer: transformFacts?.entryPublicKeys?.[0]?.bytes,
1824
+ };
1825
+ hasCached = false;
1826
+ cached = undefined;
1827
+ },
1828
+ };
1829
+ }
1830
+ if (
1831
+ !canPrepareDocumentTransformWithAppendFacts(
1832
+ this.nativeTransformDescriptor,
1833
+ ) &&
1834
+ !canPrepareDocumentTransformBeforeAppend(
1835
+ this.nativeTransformDescriptor,
1836
+ )
1837
+ ) {
1838
+ return;
1839
+ }
1840
+ const transformed = this.transformer(value, context, transformFacts);
1841
+ if (isPromiseLike(transformed)) {
1842
+ return;
1843
+ }
1844
+ const indexable = transformed;
1845
+ return {
1846
+ valuePrefixBytes: serialize(this.asIndexedTypeValue(indexable)),
1847
+ indexable,
1848
+ };
1849
+ }
1850
+
1851
+ private prepareNativeBackboneDocumentIndexStoredCommitWithAppendFacts(
1852
+ encodedDocument: Uint8Array,
1853
+ context: types.Context,
1854
+ transformFacts?: DocumentTransformFacts,
1855
+ ): NativeBackboneDocumentIndexCommit<I> | undefined {
1856
+ if (this.transformerIsIdentity && this.indexedTypeIsDocumentType) {
1857
+ return {
1858
+ valuePrefixBytes: encodedDocument,
1859
+ usePlainPutPayload: true,
1860
+ };
1861
+ }
1862
+ if (this.nativeTransformProjectionPlan) {
1863
+ return {
1864
+ projection: {
1865
+ encodedDocument,
1866
+ plan: this.nativeTransformProjectionPlan,
1867
+ signer: transformFacts?.entryPublicKeys?.[0]?.bytes,
1868
+ },
1869
+ };
1215
1870
  }
1871
+ if (
1872
+ !canPrepareDocumentTransformWithAppendFacts(
1873
+ this.nativeTransformDescriptor,
1874
+ ) &&
1875
+ !canPrepareDocumentTransformBeforeAppend(
1876
+ this.nativeTransformDescriptor,
1877
+ )
1878
+ ) {
1879
+ return;
1880
+ }
1881
+ return;
1882
+ }
1883
+
1884
+ private nativeBackboneDocumentIndexValuePrefixBytes(
1885
+ nativeDocumentIndex: NativeBackboneDocumentIndexCommit<I>,
1886
+ context: types.Context,
1887
+ ): Uint8Array | undefined {
1888
+ return (
1889
+ nativeDocumentIndex.valuePrefixBytes ??
1890
+ (nativeDocumentIndex.projection
1891
+ ? (this.nativeBackboneDocumentProjection?.projectDocumentIndexSimple?.(
1892
+ nativeDocumentIndex.projection.encodedDocument,
1893
+ nativeDocumentIndex.projection.plan,
1894
+ {
1895
+ created: context.created,
1896
+ modified: context.modified,
1897
+ head: context.head,
1898
+ gid: context.gid,
1899
+ size: context.size,
1900
+ signer: nativeDocumentIndex.projection.signer,
1901
+ },
1902
+ ) ??
1903
+ tryProjectDocumentIndexSimple(
1904
+ nativeDocumentIndex.projection.encodedDocument,
1905
+ nativeDocumentIndex.projection.plan,
1906
+ {
1907
+ created: context.created,
1908
+ modified: context.modified,
1909
+ head: context.head,
1910
+ gid: context.gid,
1911
+ size: context.size,
1912
+ signer: nativeDocumentIndex.projection.signer,
1913
+ },
1914
+ ))
1915
+ : undefined)
1916
+ );
1917
+ }
1918
+
1919
+ private asIndexedTypeValue(value: I): I {
1920
+ if (
1921
+ value &&
1922
+ Object.getPrototypeOf(value) ===
1923
+ (this.indexedType as { prototype: object }).prototype
1924
+ ) {
1925
+ return value;
1926
+ }
1927
+ return Object.assign(
1928
+ Object.create((this.indexedType as { prototype: object }).prototype),
1929
+ value,
1930
+ );
1216
1931
  }
1217
1932
 
1218
1933
  get prefetch() {
@@ -1435,14 +2150,14 @@ export class DocumentIndex<
1435
2150
  if (this._joinListener) {
1436
2151
  this._query.events.removeEventListener("join", this._joinListener);
1437
2152
  }
1438
- if (this.handleDocumentChange) {
2153
+ if (this.handleDocumentChange && this.documentEvents) {
1439
2154
  this.documentEvents.removeEventListener(
1440
2155
  "change",
1441
2156
  this.handleDocumentChange,
1442
2157
  );
1443
2158
  }
1444
2159
  this.clearAllResultQueues();
1445
- await this._resumableIterators.clearAll();
2160
+ await this._resumableIterators?.clearAll();
1446
2161
  if (this.iteratorKeepAliveTimers) {
1447
2162
  for (const timer of this.iteratorKeepAliveTimers.values()) {
1448
2163
  clearTimeout(timer);
@@ -1466,12 +2181,12 @@ export class DocumentIndex<
1466
2181
  async drop(from?: Program): Promise<boolean> {
1467
2182
  const dropped = await super.drop(from);
1468
2183
  if (dropped) {
1469
- this.documentEvents.removeEventListener(
2184
+ this.documentEvents?.removeEventListener(
1470
2185
  "change",
1471
2186
  this.handleDocumentChange,
1472
2187
  );
1473
2188
  this.clearAllResultQueues();
1474
- await this._resumableIterators.clearAll();
2189
+ await this._resumableIterators?.clearAll();
1475
2190
  if (this.iteratorKeepAliveTimers) {
1476
2191
  for (const timer of this.iteratorKeepAliveTimers.values()) {
1477
2192
  clearTimeout(timer);
@@ -1493,146 +2208,774 @@ export class DocumentIndex<
1493
2208
  }
1494
2209
  }
1495
2210
  }
1496
- return dropped;
2211
+ return dropped;
2212
+ }
2213
+
2214
+ public async get<Options extends GetOptions<T, I, D, true | undefined>>(
2215
+ key: indexerTypes.Ideable | indexerTypes.IdKey,
2216
+ options?: Options,
2217
+ ): Promise<WithIndexedContext<T, I>>;
2218
+
2219
+ public async get<Options extends GetOptions<T, I, D, false>>(
2220
+ key: indexerTypes.Ideable | indexerTypes.IdKey,
2221
+ options?: Options,
2222
+ ): Promise<WithContext<I>>;
2223
+
2224
+ public async get<
2225
+ Options extends GetOptions<T, I, D, Resolve>,
2226
+ Resolve extends boolean | undefined = ExtractResolveFromOptions<Options>,
2227
+ >(key: indexerTypes.Ideable | indexerTypes.IdKey, options?: Options) {
2228
+ let deferred:
2229
+ | DeferredPromise<WithIndexedContext<T, I> | WithContext<I>>
2230
+ | undefined;
2231
+ let baseRemote:
2232
+ | RemoteQueryOptions<
2233
+ types.AbstractSearchRequest,
2234
+ types.AbstractSearchResult,
2235
+ D
2236
+ >
2237
+ | undefined;
2238
+
2239
+ // Normalize the id key early so listeners can use it
2240
+ let idKey =
2241
+ key instanceof indexerTypes.IdKey ? key : indexerTypes.toId(key);
2242
+
2243
+ if (options?.waitFor) {
2244
+ // add change listener before query because we might get a concurrent change that matches the query,
2245
+ // that will not be included in the query result
2246
+ deferred = pDefer<WithIndexedContext<T, I> | WithContext<I>>();
2247
+
2248
+ const listener = (evt: CustomEvent<DocumentsChange<T, I>>) => {
2249
+ for (const added of evt.detail.added) {
2250
+ const id = indexerTypes.toId(
2251
+ this.indexByResolver(added.__indexed),
2252
+ ).primitive;
2253
+ if (id === idKey.primitive) {
2254
+ deferred!.resolve(added);
2255
+ }
2256
+ }
2257
+ };
2258
+ let cleanedUp = false;
2259
+ let cleanup = () => {
2260
+ if (cleanedUp) return;
2261
+ cleanedUp = true;
2262
+ this.documentEvents.removeEventListener("change", listener);
2263
+ clearTimeout(timeout);
2264
+ this.events.removeEventListener("close", resolveUndefined);
2265
+ joinListener?.();
2266
+ };
2267
+
2268
+ let resolveUndefined = () => {
2269
+ deferred!.resolve(undefined);
2270
+ };
2271
+
2272
+ let timeout = setTimeout(resolveUndefined, options.waitFor);
2273
+ this.events.addEventListener("close", resolveUndefined);
2274
+ this.documentEvents.addEventListener("change", listener);
2275
+ deferred.promise.then(cleanup);
2276
+
2277
+ // Prepare remote options without mutating caller options
2278
+ baseRemote =
2279
+ options?.remote === false
2280
+ ? undefined
2281
+ : typeof options?.remote === "object"
2282
+ ? { ...options.remote }
2283
+ : {};
2284
+ if (baseRemote) {
2285
+ const waitPolicy = baseRemote.wait;
2286
+ if (
2287
+ !waitPolicy ||
2288
+ (typeof waitPolicy === "object" &&
2289
+ (waitPolicy.timeout || 0) < options.waitFor)
2290
+ ) {
2291
+ baseRemote.wait = {
2292
+ ...(typeof waitPolicy === "object" ? waitPolicy : {}),
2293
+ timeout: options.waitFor,
2294
+ };
2295
+ }
2296
+ }
2297
+
2298
+ // Re-query on peer joins (like iterate), scoped to the joining peer
2299
+ let joinListener: (() => void) | undefined;
2300
+ if (baseRemote) {
2301
+ joinListener = this.createReplicatorJoinListener({
2302
+ eager: baseRemote.reach?.eager,
2303
+ onPeer: async (pk) => {
2304
+ if (cleanedUp) return;
2305
+ const hash = pk.hashcode();
2306
+ const requeryOptions: QueryOptions<T, I, D, Resolve> = {
2307
+ ...(options as any),
2308
+ remote: {
2309
+ ...(baseRemote || {}),
2310
+ from: [hash],
2311
+ },
2312
+ };
2313
+ const re = await this.getDetailed(idKey, requeryOptions as any);
2314
+ const first = re?.[0]?.results[0];
2315
+ if (first) {
2316
+ deferred!.resolve(first.value as any);
2317
+ }
2318
+ },
2319
+ });
2320
+ }
2321
+ }
2322
+
2323
+ const initialOptions = baseRemote
2324
+ ? ({ ...(options as any), remote: baseRemote } as Options)
2325
+ : options;
2326
+ const result = (await this.getDetailed(idKey, initialOptions))?.[0]
2327
+ ?.results[0];
2328
+
2329
+ // if no results, and we have remote joining options, we wait for the timout and if there are joining peers we re-query
2330
+ if (!result) {
2331
+ return deferred?.promise;
2332
+ } else if (deferred) {
2333
+ deferred.resolve(undefined);
2334
+ }
2335
+ return result?.value;
2336
+ }
2337
+
2338
+ public async getFromGid(gid: string) {
2339
+ const iterator = this.index.iterate({ query: { gid } });
2340
+ const one = await iterator.next(1);
2341
+ await iterator.close();
2342
+ return one[0];
2343
+ }
2344
+
2345
+ public async getFromHash(hash: string) {
2346
+ const iterator = this.index.iterate({ query: { hash } });
2347
+ const one = await iterator.next(1);
2348
+ await iterator.close();
2349
+ return one[0];
2350
+ }
2351
+
2352
+ public async getIdentityIndexedByHead(
2353
+ head: string,
2354
+ ): Promise<indexerTypes.IndexedResult<WithContext<I>> | undefined> {
2355
+ if (!this.canGetIdentityIndexedByHead()) {
2356
+ return;
2357
+ }
2358
+ return (this.index as ContextHeadIndex<I>).getByContextHead?.(head);
2359
+ }
2360
+
2361
+ public async getIdentityIndexedKeyByHead(
2362
+ head: string,
2363
+ ): Promise<indexerTypes.IdKey | undefined> {
2364
+ const key = this.getIndexedKeyByHead(head);
2365
+ if (key) {
2366
+ return key;
2367
+ }
2368
+ const indexed = await this.getIdentityIndexedByHead(head);
2369
+ return indexed?.id;
2370
+ }
2371
+
2372
+ private getIndexedKeyByHead(
2373
+ head: string,
2374
+ ): indexerTypes.IdKey | undefined {
2375
+ const getIdByHead = (this.index as ContextHeadIndex<I>).getIdByContextHead;
2376
+ return typeof getIdByHead === "function"
2377
+ ? getIdByHead.call(this.index, head)
2378
+ : undefined;
2379
+ }
2380
+
2381
+ public getIndexedKeysByHeads(
2382
+ heads: string[],
2383
+ ): Array<indexerTypes.IdKey | undefined> | undefined {
2384
+ const getIdByHead = (this.index as ContextHeadIndex<I>).getIdByContextHead;
2385
+ if (typeof getIdByHead !== "function") {
2386
+ return;
2387
+ }
2388
+ return heads.map((head) => getIdByHead.call(this.index, head));
2389
+ }
2390
+
2391
+ public tryGetIdentityIndexedKeyByHead(
2392
+ head: string,
2393
+ ): { supported: boolean; key?: indexerTypes.IdKey } {
2394
+ const getIdByHead = (this.index as ContextHeadIndex<I>).getIdByContextHead;
2395
+ if (typeof getIdByHead !== "function") {
2396
+ return { supported: false };
2397
+ }
2398
+ return {
2399
+ supported: true,
2400
+ key: getIdByHead.call(this.index, head),
2401
+ };
2402
+ }
2403
+
2404
+ public async getIdentityIndexedByHeads(
2405
+ heads: string[],
2406
+ ): Promise<
2407
+ Array<indexerTypes.IndexedResult<WithContext<I>> | undefined> | undefined
2408
+ > {
2409
+ if (!this.canGetIdentityIndexedByHead()) {
2410
+ return;
2411
+ }
2412
+ const batch = (this.index as ContextHeadIndex<I>).getByContextHeadBatch;
2413
+ if (batch) {
2414
+ return batch.call(this.index, heads);
2415
+ }
2416
+ return Promise.all(
2417
+ heads.map((head) => this.getIdentityIndexedByHead(head)),
2418
+ );
2419
+ }
2420
+
2421
+ public canGetIdentityIndexedByHead(): boolean {
2422
+ return (
2423
+ this.transformerIsIdentity &&
2424
+ this.indexedTypeIsDocumentType &&
2425
+ !this.isProgramValued &&
2426
+ typeof (this.index as ContextHeadIndex<I>).getByContextHead === "function"
2427
+ );
2428
+ }
2429
+
2430
+ public canGetIndexedKeyByHead(): boolean {
2431
+ return (
2432
+ !this.isProgramValued &&
2433
+ typeof (this.index as ContextHeadIndex<I>).getIdByContextHead ===
2434
+ "function"
2435
+ );
2436
+ }
2437
+
2438
+ public canReadOriginalFieldPathsFromIndexedValue(
2439
+ paths: readonly (string | readonly string[])[],
2440
+ ): boolean {
2441
+ return (
2442
+ !this.isProgramValued &&
2443
+ paths.every((path) =>
2444
+ this.transformerIsIdentity && this.indexedTypeIsDocumentType
2445
+ ? true
2446
+ : documentTransformPreservesFieldPath(
2447
+ this.nativeTransformDescriptor,
2448
+ path,
2449
+ ),
2450
+ )
2451
+ );
2452
+ }
2453
+
2454
+ public canReadNativeIndexedFieldValues(
2455
+ paths: readonly (string | readonly string[])[],
2456
+ ): boolean {
2457
+ return (
2458
+ this.canReadOriginalFieldPathsFromIndexedValue(paths) &&
2459
+ typeof (
2460
+ this.index as {
2461
+ getNativeIndexedFieldValue?: (
2462
+ id: indexerTypes.IdKey,
2463
+ path: readonly string[],
2464
+ ) => unknown;
2465
+ }
2466
+ ).getNativeIndexedFieldValue === "function"
2467
+ );
2468
+ }
2469
+
2470
+ public getNativeIndexedFieldValue(
2471
+ id: indexerTypes.IdKey,
2472
+ path: string | readonly string[],
2473
+ ): unknown {
2474
+ const read = (
2475
+ this.index as {
2476
+ getNativeIndexedFieldValue?: (
2477
+ id: indexerTypes.IdKey,
2478
+ path: readonly string[],
2479
+ ) => unknown;
2480
+ }
2481
+ ).getNativeIndexedFieldValue;
2482
+ if (typeof read !== "function") {
2483
+ return undefined;
2484
+ }
2485
+ return read.call(this.index, id, typeof path === "string" ? [path] : path);
2486
+ }
2487
+
2488
+ public _putIdentityWithContext(
2489
+ value: T,
2490
+ id: indexerTypes.IdKey,
2491
+ context: types.Context,
2492
+ options?: ContextualPutOptions,
2493
+ ): MaybePromise<WithIndexedContext<T, I> | undefined> {
2494
+ const contextualPut = this.transformerIsIdentity
2495
+ ? (this.index as ContextualIndexPut<I>).putWithContext
2496
+ : undefined;
2497
+ if (!contextualPut || this.isProgramValued) {
2498
+ return;
2499
+ }
2500
+ const indexable = value as any as I;
2501
+ const indexedValue = coerceWithIndexed(
2502
+ coerceWithContext(value, context),
2503
+ indexable,
2504
+ );
2505
+ this.cacheResolvedValue(id.primitive, value);
2506
+ const handleError = (error: unknown) => {
2507
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2508
+ return indexedValue;
2509
+ }
2510
+ throw error;
2511
+ };
2512
+ try {
2513
+ const putResult = contextualPut.call(
2514
+ this.index,
2515
+ indexable,
2516
+ id,
2517
+ context,
2518
+ this.withContextualEncodedValue(options, context),
2519
+ );
2520
+ return isPromiseLike(putResult)
2521
+ ? putResult.then(() => indexedValue, handleError)
2522
+ : indexedValue;
2523
+ } catch (error) {
2524
+ return handleError(error);
2525
+ }
2526
+ }
2527
+
2528
+ public _putStoredIdentityWithContext(
2529
+ value: T,
2530
+ id: indexerTypes.IdKey,
2531
+ context: types.Context,
2532
+ encodedValueParts: NonNullable<ContextualPutOptions["encodedValueParts"]>,
2533
+ options?: { replace?: boolean },
2534
+ ): MaybePromise<WithIndexedContext<T, I> | undefined> {
2535
+ const contextualStoredPut = this.transformerIsIdentity
2536
+ ? (this.index as ContextualIndexPut<I>).putStoredContextualEncodedValue
2537
+ : undefined;
2538
+ if (
2539
+ !contextualStoredPut ||
2540
+ this.isProgramValued ||
2541
+ !this.indexedTypeIsDocumentType
2542
+ ) {
2543
+ return;
2544
+ }
2545
+ const indexable = value as any as I;
2546
+ const indexedValue = coerceWithIndexed(
2547
+ coerceWithContext(value, context),
2548
+ indexable,
2549
+ );
2550
+ this.cacheResolvedValue(id.primitive, value);
2551
+ const handleError = (error: unknown) => {
2552
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2553
+ return indexedValue;
2554
+ }
2555
+ throw error;
2556
+ };
2557
+ try {
2558
+ const putResult = contextualStoredPut.call(
2559
+ this.index,
2560
+ id,
2561
+ encodedValueParts,
2562
+ options,
2563
+ );
2564
+ if (putResult === false) {
2565
+ return;
2566
+ }
2567
+ return isPromiseLike(putResult)
2568
+ ? putResult.then(() => indexedValue, handleError)
2569
+ : indexedValue;
2570
+ } catch (error) {
2571
+ return handleError(error);
2572
+ }
2573
+ }
2574
+
2575
+ private _putPreparedNativeBackboneDocumentIndexWithContext(
2576
+ value: T,
2577
+ id: indexerTypes.IdKey,
2578
+ context: types.Context,
2579
+ nativeDocumentIndex: NativeBackboneDocumentIndexCommit<I>,
2580
+ options?: { replace?: boolean },
2581
+ ): MaybePromise<WithIndexedContext<T, I> | undefined> {
2582
+ const contextualStoredPut = (this.index as ContextualIndexPut<I>)
2583
+ .putStoredContextualEncodedValue;
2584
+ if (!contextualStoredPut || this.isProgramValued) {
2585
+ return;
2586
+ }
2587
+ nativeDocumentIndex.setContext?.(context);
2588
+ const valueWithContext = coerceWithContext(value, context);
2589
+ const indexedValue = nativeDocumentIndex.indexable
2590
+ ? coerceWithIndexed(valueWithContext, nativeDocumentIndex.indexable)
2591
+ : nativeDocumentIndex.getIndexable
2592
+ ? coerceWithLazyIndexed(
2593
+ valueWithContext,
2594
+ nativeDocumentIndex.getIndexable,
2595
+ )
2596
+ : undefined;
2597
+ if (!indexedValue) {
2598
+ return;
2599
+ }
2600
+ this.cacheResolvedValue(id.primitive, value);
2601
+ const valuePrefixBytes = this.nativeBackboneDocumentIndexValuePrefixBytes(
2602
+ nativeDocumentIndex,
2603
+ context,
2604
+ );
2605
+ if (!valuePrefixBytes) {
2606
+ return;
2607
+ }
2608
+ const encodedValueParts = {
2609
+ prefix: valuePrefixBytes,
2610
+ suffix: encodeContextSuffix(context),
2611
+ };
2612
+ const handleError = (error: unknown) => {
2613
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2614
+ return indexedValue;
2615
+ }
2616
+ throw error;
2617
+ };
2618
+ try {
2619
+ const putResult = contextualStoredPut.call(
2620
+ this.index,
2621
+ id,
2622
+ encodedValueParts,
2623
+ options,
2624
+ );
2625
+ if (putResult === false) {
2626
+ return;
2627
+ }
2628
+ return isPromiseLike(putResult)
2629
+ ? putResult.then(() => indexedValue, handleError)
2630
+ : indexedValue;
2631
+ } catch (error) {
2632
+ return handleError(error);
2633
+ }
2634
+ }
2635
+
2636
+ private _putPreparedNativeBackboneDocumentIndexStoredWithContext(
2637
+ id: indexerTypes.IdKey,
2638
+ context: types.Context,
2639
+ nativeDocumentIndex: NativeBackboneDocumentIndexCommit<I>,
2640
+ options?: { replace?: boolean },
2641
+ ): MaybePromise<boolean | undefined> {
2642
+ const contextualStoredPut = (this.index as ContextualIndexPut<I>)
2643
+ .putStoredContextualEncodedValue;
2644
+ if (!contextualStoredPut || this.isProgramValued) {
2645
+ return;
2646
+ }
2647
+ nativeDocumentIndex.setContext?.(context);
2648
+ const valuePrefixBytes = this.nativeBackboneDocumentIndexValuePrefixBytes(
2649
+ nativeDocumentIndex,
2650
+ context,
2651
+ );
2652
+ if (!valuePrefixBytes) {
2653
+ return;
2654
+ }
2655
+ const encodedValueParts = {
2656
+ prefix: valuePrefixBytes,
2657
+ suffix: encodeContextSuffix(context),
2658
+ };
2659
+ const handleError = (error: unknown) => {
2660
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2661
+ return true;
2662
+ }
2663
+ throw error;
2664
+ };
2665
+ try {
2666
+ const putResult = contextualStoredPut.call(
2667
+ this.index,
2668
+ id,
2669
+ encodedValueParts,
2670
+ options,
2671
+ );
2672
+ if (putResult === false) {
2673
+ return false;
2674
+ }
2675
+ return isPromiseLike(putResult)
2676
+ ? putResult.then(() => true, handleError)
2677
+ : true;
2678
+ } catch (error) {
2679
+ return handleError(error);
2680
+ }
2681
+ }
2682
+
2683
+ private _persistPreparedNativeBackboneDocumentIndexStoredWithContext(
2684
+ id: indexerTypes.IdKey,
2685
+ context: types.Context,
2686
+ nativeDocumentIndex?: NativeBackboneDocumentIndexCommit<I>,
2687
+ encodedValueParts?: NonNullable<ContextualPutOptions["encodedValueParts"]>,
2688
+ options?: { replace?: boolean },
2689
+ ): MaybePromise<boolean | undefined> {
2690
+ const persistStoredPut = (this.index as ContextualIndexPut<I>)
2691
+ .persistStoredContextualEncodedValue;
2692
+ if (!persistStoredPut || this.isProgramValued) {
2693
+ return;
2694
+ }
2695
+ let storedParts:
2696
+ | NonNullable<ContextualPutOptions["encodedValueParts"]>
2697
+ | undefined;
2698
+ if (
2699
+ this.transformerIsIdentity &&
2700
+ this.indexedTypeIsDocumentType &&
2701
+ encodedValueParts
2702
+ ) {
2703
+ storedParts = encodedValueParts;
2704
+ } else if (nativeDocumentIndex) {
2705
+ nativeDocumentIndex.setContext?.(context);
2706
+ const valuePrefixBytes =
2707
+ nativeDocumentIndex.valuePrefixBytes ??
2708
+ this.nativeBackboneDocumentIndexValuePrefixBytes(
2709
+ nativeDocumentIndex,
2710
+ context,
2711
+ );
2712
+ if (!valuePrefixBytes) {
2713
+ return;
2714
+ }
2715
+ storedParts = {
2716
+ prefix: valuePrefixBytes,
2717
+ suffix: encodeContextSuffix(context),
2718
+ };
2719
+ } else {
2720
+ return;
2721
+ }
2722
+ try {
2723
+ const persistResult = persistStoredPut.call(
2724
+ this.index,
2725
+ id,
2726
+ storedParts,
2727
+ options,
2728
+ );
2729
+ if (persistResult === false) {
2730
+ return false;
2731
+ }
2732
+ return isPromiseLike(persistResult)
2733
+ ? persistResult.then(() => true, (error: unknown) => {
2734
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2735
+ return true;
2736
+ }
2737
+ throw error;
2738
+ })
2739
+ : true;
2740
+ } catch (error) {
2741
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2742
+ return true;
2743
+ }
2744
+ throw error;
2745
+ }
2746
+ }
2747
+
2748
+ private async _putManyPreparedNativeBackboneDocumentIndexWithContext(
2749
+ values: Array<{
2750
+ value: T;
2751
+ id: indexerTypes.IdKey;
2752
+ context: types.Context;
2753
+ nativeDocumentIndex?: NativeBackboneDocumentIndexCommit<I>;
2754
+ options?: { replace?: boolean };
2755
+ }>,
2756
+ ): Promise<WithIndexedContext<T, I>[] | undefined> {
2757
+ if (values.length === 0) {
2758
+ return [];
2759
+ }
2760
+ const contextualBatchPut = (this.index as ContextualIndexPut<I>)
2761
+ .putWithContextBatch;
2762
+ if (!contextualBatchPut || this.isProgramValued) {
2763
+ return;
2764
+ }
2765
+ const indexedValues: WithIndexedContext<T, I>[] = [];
2766
+ const batchValues: Array<{
2767
+ value: I;
2768
+ id: indexerTypes.IdKey;
2769
+ context: types.Context;
2770
+ options: ContextualPutOptions;
2771
+ }> = [];
2772
+ for (const item of values) {
2773
+ if (!item.nativeDocumentIndex) {
2774
+ return;
2775
+ }
2776
+ item.nativeDocumentIndex.setContext?.(item.context);
2777
+ const valueWithContext = coerceWithContext(item.value, item.context);
2778
+ const indexedValue = item.nativeDocumentIndex.indexable
2779
+ ? coerceWithIndexed(
2780
+ valueWithContext,
2781
+ item.nativeDocumentIndex.indexable,
2782
+ )
2783
+ : item.nativeDocumentIndex.getIndexable
2784
+ ? coerceWithLazyIndexed(
2785
+ valueWithContext,
2786
+ item.nativeDocumentIndex.getIndexable,
2787
+ )
2788
+ : undefined;
2789
+ if (!indexedValue) {
2790
+ return;
2791
+ }
2792
+ const valuePrefixBytes = this.nativeBackboneDocumentIndexValuePrefixBytes(
2793
+ item.nativeDocumentIndex,
2794
+ item.context,
2795
+ );
2796
+ if (!valuePrefixBytes) {
2797
+ return;
2798
+ }
2799
+ this.cacheResolvedValue(item.id.primitive, item.value);
2800
+ indexedValues.push(indexedValue);
2801
+ batchValues.push({
2802
+ // Encoded native batches store from encodedValueParts; descriptor
2803
+ // projections keep the JS indexable lazy for event consumers.
2804
+ value:
2805
+ item.nativeDocumentIndex.indexable ?? (undefined as unknown as I),
2806
+ id: item.id,
2807
+ context: item.context,
2808
+ options: {
2809
+ replace: item.options?.replace,
2810
+ encodedValueParts: {
2811
+ prefix: valuePrefixBytes,
2812
+ suffix: encodeContextSuffix(item.context),
2813
+ },
2814
+ },
2815
+ });
2816
+ }
2817
+ const handleError = (error: unknown) => {
2818
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2819
+ return indexedValues;
2820
+ }
2821
+ throw error;
2822
+ };
2823
+ try {
2824
+ const putResult = contextualBatchPut.call(this.index, batchValues);
2825
+ return isPromiseLike(putResult)
2826
+ ? putResult.then(() => indexedValues, handleError)
2827
+ : indexedValues;
2828
+ } catch (error) {
2829
+ return handleError(error);
2830
+ }
2831
+ }
2832
+
2833
+ private async _putManyPreparedNativeBackboneDocumentIndexStored(
2834
+ values: Array<{
2835
+ value: T;
2836
+ id: indexerTypes.IdKey;
2837
+ context: types.Context;
2838
+ encodedValueParts?: NonNullable<
2839
+ ContextualPutOptions["encodedValueParts"]
2840
+ >;
2841
+ nativeDocumentIndex?: NativeBackboneDocumentIndexCommit<I>;
2842
+ options?: { replace?: boolean };
2843
+ }>,
2844
+ ): Promise<boolean | undefined> {
2845
+ if (values.length === 0) {
2846
+ return true;
2847
+ }
2848
+ if (this.isProgramValued) {
2849
+ return;
2850
+ }
2851
+ const storedBatchPut = (this.index as ContextualIndexPut<I>)
2852
+ .putStoredContextualEncodedValueBatch;
2853
+ if (!storedBatchPut) {
2854
+ return;
2855
+ }
2856
+ const batchValues: Array<{
2857
+ id: indexerTypes.IdKey;
2858
+ encodedValueParts: NonNullable<
2859
+ ContextualPutOptions["encodedValueParts"]
2860
+ >;
2861
+ options?: { replace?: boolean };
2862
+ }> = [];
2863
+ for (const item of values) {
2864
+ let encodedValueParts:
2865
+ | NonNullable<ContextualPutOptions["encodedValueParts"]>
2866
+ | undefined;
2867
+ if (
2868
+ this.transformerIsIdentity &&
2869
+ this.indexedTypeIsDocumentType &&
2870
+ item.encodedValueParts
2871
+ ) {
2872
+ encodedValueParts = item.encodedValueParts;
2873
+ } else if (item.nativeDocumentIndex) {
2874
+ item.nativeDocumentIndex.setContext?.(item.context);
2875
+ const valuePrefixBytes =
2876
+ item.nativeDocumentIndex.valuePrefixBytes ??
2877
+ this.nativeBackboneDocumentIndexValuePrefixBytes(
2878
+ item.nativeDocumentIndex,
2879
+ item.context,
2880
+ );
2881
+ if (!valuePrefixBytes) {
2882
+ return;
2883
+ }
2884
+ encodedValueParts = {
2885
+ prefix: valuePrefixBytes,
2886
+ suffix: encodeContextSuffix(item.context),
2887
+ };
2888
+ } else {
2889
+ return;
2890
+ }
2891
+ this.cacheResolvedValue(item.id.primitive, item.value);
2892
+ batchValues.push({
2893
+ id: item.id,
2894
+ encodedValueParts,
2895
+ options: item.options,
2896
+ });
2897
+ }
2898
+ const handleError = (error: unknown) => {
2899
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2900
+ return true;
2901
+ }
2902
+ throw error;
2903
+ };
2904
+ try {
2905
+ return await storedBatchPut.call(this.index, batchValues);
2906
+ } catch (error) {
2907
+ return handleError(error);
2908
+ }
1497
2909
  }
1498
2910
 
1499
- public async get<Options extends GetOptions<T, I, D, true | undefined>>(
1500
- key: indexerTypes.Ideable | indexerTypes.IdKey,
1501
- options?: Options,
1502
- ): Promise<WithIndexedContext<T, I>>;
1503
-
1504
- public async get<Options extends GetOptions<T, I, D, false>>(
1505
- key: indexerTypes.Ideable | indexerTypes.IdKey,
1506
- options?: Options,
1507
- ): Promise<WithContext<I>>;
1508
-
1509
- public async get<
1510
- Options extends GetOptions<T, I, D, Resolve>,
1511
- Resolve extends boolean | undefined = ExtractResolveFromOptions<Options>,
1512
- >(key: indexerTypes.Ideable | indexerTypes.IdKey, options?: Options) {
1513
- let deferred:
1514
- | DeferredPromise<WithIndexedContext<T, I> | WithContext<I>>
1515
- | undefined;
1516
- let baseRemote:
1517
- | RemoteQueryOptions<
1518
- types.AbstractSearchRequest,
1519
- types.AbstractSearchResult,
1520
- D
1521
- >
1522
- | undefined;
1523
-
1524
- // Normalize the id key early so listeners can use it
1525
- let idKey =
1526
- key instanceof indexerTypes.IdKey ? key : indexerTypes.toId(key);
1527
-
1528
- if (options?.waitFor) {
1529
- // add change listener before query because we might get a concurrent change that matches the query,
1530
- // that will not be included in the query result
1531
- deferred = pDefer<WithIndexedContext<T, I> | WithContext<I>>();
1532
-
1533
- const listener = (evt: CustomEvent<DocumentsChange<T, I>>) => {
1534
- for (const added of evt.detail.added) {
1535
- const id = indexerTypes.toId(
1536
- this.indexByResolver(added.__indexed),
1537
- ).primitive;
1538
- if (id === idKey.primitive) {
1539
- deferred!.resolve(added);
1540
- }
1541
- }
1542
- };
1543
- let cleanedUp = false;
1544
- let cleanup = () => {
1545
- if (cleanedUp) return;
1546
- cleanedUp = true;
1547
- this.documentEvents.removeEventListener("change", listener);
1548
- clearTimeout(timeout);
1549
- this.events.removeEventListener("close", resolveUndefined);
1550
- joinListener?.();
1551
- };
2911
+ public async _putManyIdentityWithContext(
2912
+ values: Array<{
2913
+ value: T;
2914
+ id: indexerTypes.IdKey;
2915
+ context: types.Context;
2916
+ options?: ContextualPutOptions;
2917
+ }>,
2918
+ ): Promise<WithIndexedContext<T, I>[] | undefined> {
2919
+ if (values.length === 0) {
2920
+ return [];
2921
+ }
2922
+ const contextualPut = this.transformerIsIdentity
2923
+ ? (this.index as ContextualIndexPut<I>).putWithContext
2924
+ : undefined;
2925
+ const contextualBatchPut = this.transformerIsIdentity
2926
+ ? (this.index as ContextualIndexPut<I>).putWithContextBatch
2927
+ : undefined;
2928
+ if ((!contextualBatchPut && !contextualPut) || this.isProgramValued) {
2929
+ return;
2930
+ }
1552
2931
 
1553
- let resolveUndefined = () => {
1554
- deferred!.resolve(undefined);
2932
+ const indexedValues = values.map((item) => {
2933
+ const indexable = item.value as any as I;
2934
+ this.cacheResolvedValue(item.id.primitive, item.value);
2935
+ return {
2936
+ indexable,
2937
+ value: coerceWithIndexed(
2938
+ coerceWithContext(item.value, item.context),
2939
+ indexable,
2940
+ ),
1555
2941
  };
2942
+ });
1556
2943
 
1557
- let timeout = setTimeout(resolveUndefined, options.waitFor);
1558
- this.events.addEventListener("close", resolveUndefined);
1559
- this.documentEvents.addEventListener("change", listener);
1560
- deferred.promise.then(cleanup);
1561
-
1562
- // Prepare remote options without mutating caller options
1563
- baseRemote =
1564
- options?.remote === false
1565
- ? undefined
1566
- : typeof options?.remote === "object"
1567
- ? { ...options.remote }
1568
- : {};
1569
- if (baseRemote) {
1570
- const waitPolicy = baseRemote.wait;
1571
- if (
1572
- !waitPolicy ||
1573
- (typeof waitPolicy === "object" &&
1574
- (waitPolicy.timeout || 0) < options.waitFor)
1575
- ) {
1576
- baseRemote.wait = {
1577
- ...(typeof waitPolicy === "object" ? waitPolicy : {}),
1578
- timeout: options.waitFor,
1579
- };
2944
+ try {
2945
+ if (contextualBatchPut) {
2946
+ await contextualBatchPut.call(
2947
+ this.index,
2948
+ values.map((item, index) => ({
2949
+ value: indexedValues[index]!.indexable,
2950
+ id: item.id,
2951
+ context: item.context,
2952
+ options: this.withContextualEncodedValue(
2953
+ item.options,
2954
+ item.context,
2955
+ ),
2956
+ })),
2957
+ );
2958
+ } else {
2959
+ for (let i = 0; i < values.length; i++) {
2960
+ const item = values[i]!;
2961
+ await contextualPut!.call(
2962
+ this.index,
2963
+ indexedValues[i]!.indexable,
2964
+ item.id,
2965
+ item.context,
2966
+ this.withContextualEncodedValue(item.options, item.context),
2967
+ );
1580
2968
  }
1581
2969
  }
1582
-
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
- }
1603
- },
1604
- });
2970
+ } catch (error) {
2971
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
2972
+ return indexedValues.map((item) => item.value);
1605
2973
  }
2974
+ throw error;
1606
2975
  }
1607
-
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);
1619
- }
1620
- return result?.value;
1621
- }
1622
-
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];
2976
+ return indexedValues.map((item) => item.value);
1628
2977
  }
1629
2978
 
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];
1635
- }
1636
2979
  public async put(
1637
2980
  value: T,
1638
2981
  id: indexerTypes.IdKey,
@@ -1660,6 +3003,7 @@ export class DocumentIndex<
1660
3003
  });
1661
3004
  return this.putWithContext(value, id, context, {
1662
3005
  replace: existingDefined != null,
3006
+ transformFacts: { entryPublicKeys: entry.publicKeys },
1663
3007
  });
1664
3008
  }
1665
3009
 
@@ -1667,37 +3011,49 @@ export class DocumentIndex<
1667
3011
  value: T,
1668
3012
  id: indexerTypes.IdKey,
1669
3013
  context: types.Context,
1670
- options?: { replace?: boolean },
3014
+ options?: ContextualPutOptions,
1671
3015
  ): Promise<{ context: types.Context; indexable: I }> {
1672
3016
  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
- );
3017
+ this.cacheResolvedValue(idString, value);
3018
+ const valueToIndex = this.transformerIsIdentity
3019
+ ? (value as any as I)
3020
+ : await this.transformer(value, context, options?.transformFacts);
1694
3021
 
1695
3022
  coerceWithIndexed(value, valueToIndex);
1696
3023
 
1697
3024
  coerceWithContext(value, context);
1698
3025
 
1699
3026
  try {
1700
- await this.index.put(wrappedValueToIndex, undefined, options);
3027
+ const contextualPut = this.transformerIsIdentity
3028
+ ? (this.index as ContextualIndexPut<I>).putWithContext
3029
+ : undefined;
3030
+ if (contextualPut) {
3031
+ const encodedValueParts = this.encodeContextualIndexedValueParts(
3032
+ options?.encodedValue,
3033
+ context,
3034
+ );
3035
+ await contextualPut.call(
3036
+ this.index,
3037
+ valueToIndex,
3038
+ id,
3039
+ context,
3040
+ encodedValueParts
3041
+ ? { ...options, encodedValue: undefined, encodedValueParts }
3042
+ : options?.encodedValue
3043
+ ? { ...options, encodedValue: undefined }
3044
+ : options,
3045
+ );
3046
+ } else {
3047
+ const wrappedValueToIndex = new this.wrappedIndexedType(
3048
+ valueToIndex as I,
3049
+ context,
3050
+ );
3051
+ await this.index.put(
3052
+ wrappedValueToIndex,
3053
+ id,
3054
+ stripEncodedValue(options),
3055
+ );
3056
+ }
1701
3057
  } catch (error) {
1702
3058
  if (error instanceof indexerTypes.NotStartedError && this.closed) {
1703
3059
  return { context, indexable: valueToIndex };
@@ -1707,20 +3063,211 @@ export class DocumentIndex<
1707
3063
  return { context, indexable: valueToIndex };
1708
3064
  }
1709
3065
 
1710
- public del(key: indexerTypes.IdKey) {
1711
- if (this.isProgramValued) {
1712
- this._resolverProgramCache!.delete(key.primitive);
1713
- indexCacheLogger("cache:del:program", { id: key.primitive });
3066
+ public async putManyWithContext(
3067
+ values: Array<{
3068
+ value: T;
3069
+ id: indexerTypes.IdKey;
3070
+ context: types.Context;
3071
+ options?: ContextualPutOptions;
3072
+ }>,
3073
+ ): Promise<Array<{ context: types.Context; indexable: I }>> {
3074
+ if (values.length === 0) {
3075
+ return [];
3076
+ }
3077
+ let transformed: Array<
3078
+ (typeof values)[number] & {
3079
+ indexable: I;
3080
+ }
3081
+ >;
3082
+ if (this.transformerIsIdentity) {
3083
+ transformed = new Array(values.length);
3084
+ for (let i = 0; i < values.length; i++) {
3085
+ const item = values[i]!;
3086
+ this.cacheResolvedValue(item.id.primitive, item.value);
3087
+ const indexable = item.value as any as I;
3088
+ coerceWithIndexed(item.value, indexable);
3089
+ coerceWithContext(item.value, item.context);
3090
+ transformed[i] = { ...item, indexable };
3091
+ }
1714
3092
  } else {
1715
- if (this._resolverCache?.del(key.primitive)) {
1716
- indexCacheLogger("cache:del:value", { id: key.primitive });
3093
+ transformed = await Promise.all(
3094
+ values.map(async (item) => {
3095
+ this.cacheResolvedValue(item.id.primitive, item.value);
3096
+ const indexable = await this.transformer(item.value, item.context);
3097
+ coerceWithIndexed(item.value, indexable);
3098
+ coerceWithContext(item.value, item.context);
3099
+ return { ...item, indexable };
3100
+ }),
3101
+ );
3102
+ }
3103
+
3104
+ try {
3105
+ const contextualBatchPut = this.transformerIsIdentity
3106
+ ? (this.index as ContextualIndexPut<I>).putWithContextBatch
3107
+ : undefined;
3108
+ if (contextualBatchPut) {
3109
+ await contextualBatchPut.call(
3110
+ this.index,
3111
+ transformed.map((item) => ({
3112
+ value: item.indexable,
3113
+ id: item.id,
3114
+ context: item.context,
3115
+ options: this.withContextualEncodedValue(
3116
+ item.options,
3117
+ item.context,
3118
+ ),
3119
+ })),
3120
+ );
3121
+ } else if (
3122
+ transformed.every((item) => item.options?.replace !== true) &&
3123
+ this.index.putBatch
3124
+ ) {
3125
+ await this.index.putBatch(
3126
+ transformed.map(
3127
+ (item) => new this.wrappedIndexedType(item.indexable, item.context),
3128
+ ),
3129
+ );
3130
+ } else {
3131
+ const contextualPut = this.transformerIsIdentity
3132
+ ? (this.index as ContextualIndexPut<I>).putWithContext
3133
+ : undefined;
3134
+ for (const item of transformed) {
3135
+ if (contextualPut) {
3136
+ await contextualPut.call(
3137
+ this.index,
3138
+ item.indexable,
3139
+ item.id,
3140
+ item.context,
3141
+ this.withContextualEncodedValue(item.options, item.context),
3142
+ );
3143
+ } else {
3144
+ await this.index.put(
3145
+ new this.wrappedIndexedType(item.indexable, item.context),
3146
+ item.id,
3147
+ stripEncodedValue(item.options),
3148
+ );
3149
+ }
3150
+ }
1717
3151
  }
3152
+ } catch (error) {
3153
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
3154
+ return transformed.map((item) => ({
3155
+ context: item.context,
3156
+ indexable: item.indexable,
3157
+ }));
3158
+ }
3159
+ throw error;
1718
3160
  }
3161
+
3162
+ return transformed.map((item) => ({
3163
+ context: item.context,
3164
+ indexable: item.indexable,
3165
+ }));
3166
+ }
3167
+
3168
+ public _cacheResolvedIdentityValue(
3169
+ id: string | number | bigint,
3170
+ value: T,
3171
+ ): void {
3172
+ this.cacheResolvedValue(id, value);
3173
+ }
3174
+
3175
+ private cacheResolvedValue(id: string | number | bigint, value: T): void {
3176
+ if (this.isProgramValued) {
3177
+ this._resolverProgramCache!.set(id, value);
3178
+ indexCacheLogger("cache:set:program", { id });
3179
+ } else if (this._resolverCache) {
3180
+ this._resolverCache.add(id, value);
3181
+ indexCacheLogger("cache:set:value", { id });
3182
+ }
3183
+ }
3184
+
3185
+ private withContextualEncodedValue(
3186
+ options: ContextualPutOptions | undefined,
3187
+ context: types.Context,
3188
+ ): ContextualPutOptions | undefined {
3189
+ if (!options?.encodedValue) {
3190
+ return options;
3191
+ }
3192
+ const encodedValueParts = this.encodeContextualIndexedValueParts(
3193
+ options.encodedValue,
3194
+ context,
3195
+ );
3196
+ return encodedValueParts
3197
+ ? { ...options, encodedValue: undefined, encodedValueParts }
3198
+ : options;
3199
+ }
3200
+
3201
+ private encodeContextualIndexedValueParts(
3202
+ encodedValue: Uint8Array | undefined,
3203
+ context: types.Context,
3204
+ ): ContextualPutOptions["encodedValueParts"] | undefined {
3205
+ if (
3206
+ !encodedValue ||
3207
+ !this.transformerIsIdentity ||
3208
+ !this.indexedTypeIsDocumentType
3209
+ ) {
3210
+ return;
3211
+ }
3212
+ return {
3213
+ prefix: encodedValue,
3214
+ suffix: encodeContextSuffix(context),
3215
+ };
3216
+ }
3217
+
3218
+ public del(key: indexerTypes.IdKey) {
3219
+ this.deleteResolvedCacheForKey(key);
1719
3220
  return this.index.del({
1720
3221
  query: [indexerTypes.getMatcher(this.indexBy, key.key)],
1721
3222
  });
1722
3223
  }
1723
3224
 
3225
+ public async delMany(keys: indexerTypes.IdKey[]): Promise<void> {
3226
+ if (keys.length === 0) {
3227
+ return;
3228
+ }
3229
+ for (const key of keys) {
3230
+ this.deleteResolvedCacheForKey(key);
3231
+ }
3232
+ const delIdsNoReturn = (this.index as ExactDeleteIndex).delIdsNoReturn;
3233
+ if (delIdsNoReturn) {
3234
+ await delIdsNoReturn.call(this.index, keys);
3235
+ return;
3236
+ }
3237
+ const delIds = (this.index as ExactDeleteIndex).delIds;
3238
+ if (delIds) {
3239
+ await delIds.call(this.index, keys);
3240
+ return;
3241
+ }
3242
+ await Promise.all(keys.map((key) => this.del(key)));
3243
+ }
3244
+
3245
+ public clearResolvedCacheForKeys(keys: indexerTypes.IdKey[]): void {
3246
+ for (const key of keys) {
3247
+ this.deleteResolvedCacheForKey(key);
3248
+ }
3249
+ }
3250
+
3251
+ public delManyMaybe(keys: indexerTypes.IdKey[]): MaybePromise<void> {
3252
+ if (keys.length === 0) {
3253
+ return;
3254
+ }
3255
+ for (const key of keys) {
3256
+ this.deleteResolvedCacheForKey(key);
3257
+ }
3258
+ const delIdsNoReturn = (this.index as ExactDeleteIndex).delIdsNoReturn;
3259
+ if (delIdsNoReturn) {
3260
+ const result = delIdsNoReturn.call(this.index, keys);
3261
+ return isPromiseLike(result) ? result.then(() => undefined) : undefined;
3262
+ }
3263
+ const delIds = (this.index as ExactDeleteIndex).delIds;
3264
+ if (delIds) {
3265
+ const result = delIds.call(this.index, keys);
3266
+ return isPromiseLike(result) ? result.then(() => undefined) : undefined;
3267
+ }
3268
+ return Promise.all(keys.map((key) => this.del(key))).then(() => undefined);
3269
+ }
3270
+
1724
3271
  public async getDetailed<
1725
3272
  Options extends QueryOptions<T, I, D, Resolve>,
1726
3273
  Resolve extends boolean | undefined = ExtractResolveFromOptions<Options>,
@@ -2198,15 +3745,18 @@ export class DocumentIndex<
2198
3745
  }
2199
3746
 
2200
3747
  get countIteratorsInProgress() {
2201
- return this._resumableIterators.queues.size;
3748
+ return this._resumableIterators?.queues.size ?? 0;
2202
3749
  }
2203
3750
 
2204
3751
  private clearAllResultQueues() {
3752
+ if (!this._resultQueue) {
3753
+ return;
3754
+ }
2205
3755
  for (const [key, queue] of this._resultQueue) {
2206
3756
  clearTimeout(queue.timeout);
2207
3757
  this._resultQueue.delete(key);
2208
3758
  this.cancelIteratorKeepAlive(key);
2209
- this._resumableIterators.close({ idString: key });
3759
+ this._resumableIterators?.close({ idString: key });
2210
3760
  }
2211
3761
  }
2212
3762
 
@@ -2501,9 +4051,10 @@ export class DocumentIndex<
2501
4051
  signal: options?.signal,
2502
4052
  });
2503
4053
 
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.
4054
+ // Cold start: cover can be temporarily self-only or empty while
4055
+ // replication metadata converges. For explicit bounded remote searches,
4056
+ // query bounded connected peers instead of waiting for replicator
4057
+ // metadata to catch up.
2507
4058
  if (!options?.remote?.from && isDefaultDomainArgs && remoteWasExplicit) {
2508
4059
  const selfHash = this.node.identity.publicKey.hashcode();
2509
4060
  const remoteCount = replicatorGroups.filter(
@@ -2513,25 +4064,53 @@ export class DocumentIndex<
2513
4064
  const waitEnabled = Boolean(remote.wait);
2514
4065
  const coverIsSelfOnly =
2515
4066
  replicatorGroups.length === 1 && replicatorGroups[0] === selfHash;
4067
+ const coverIsColdEmpty =
4068
+ replicatorGroups.length === 0 && remote.timeout != null;
4069
+
4070
+ // If the cover is explicitly empty (no shards), don't override it
4071
+ // unless the caller requested waiting for joins (e.g. get(waitFor))
4072
+ // or bounded the remote query with a timeout.
4073
+ if (waitEnabled || coverIsSelfOnly || coverIsColdEmpty) {
4074
+ const extra: string[] = [];
4075
+ const addExtra = (hash: string | undefined) => {
4076
+ if (!hash || hash === selfHash || extra.includes(hash)) {
4077
+ return;
4078
+ }
4079
+ extra.push(hash);
4080
+ };
2516
4081
 
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
4082
  const peerMap: Map<string, unknown> | undefined = (
2521
4083
  this.node.services.pubsub as any
2522
4084
  )?.peers;
4085
+
4086
+ // Only consider replicators that are currently reachable.
4087
+ // The replicator index can contain stale (offline) peers, e.g.
4088
+ // persisted replicators after a restart; querying those would
4089
+ // block the first batch for the full wait timeout instead of
4090
+ // letting joining peers be merged as they arrive.
4091
+ if (peerMap?.has) {
4092
+ try {
4093
+ for (const hash of await this._log.getReplicators()) {
4094
+ if (peerMap.has(hash)) {
4095
+ addExtra(hash);
4096
+ }
4097
+ if (extra.length >= 8) break;
4098
+ }
4099
+ } catch {
4100
+ // Fall through to connected peers when the local replicator
4101
+ // index is not ready yet.
4102
+ }
4103
+ }
4104
+
2523
4105
  if (peerMap?.keys) {
2524
- const extra: string[] = [];
2525
4106
  for (const hash of peerMap.keys()) {
2526
- if (!hash || hash === selfHash) continue;
2527
- extra.push(hash);
4107
+ addExtra(hash);
2528
4108
  if (extra.length >= 8) break;
2529
4109
  }
2530
- if (extra.length > 0) {
2531
- replicatorGroups = [
2532
- ...new Set([...replicatorGroups, ...extra]),
2533
- ];
2534
- }
4110
+ }
4111
+
4112
+ if (extra.length > 0) {
4113
+ replicatorGroups = [...new Set([...replicatorGroups, ...extra])];
2535
4114
  }
2536
4115
  }
2537
4116
  }
@@ -3805,6 +5384,8 @@ export class DocumentIndex<
3805
5384
  return [];
3806
5385
  }
3807
5386
 
5387
+ await pendingUpdateProcessing;
5388
+
3808
5389
  const bufferedBeforeFetch = peerBuffers().length;
3809
5390
  const localHash = this.node.identity.publicKey.hashcode();
3810
5391
  const hasBufferedRemoteResults = [...peerBufferMap.entries()].some(
@@ -4087,6 +5668,7 @@ export class DocumentIndex<
4087
5668
  | Extract<UpdateReason, "join" | "change" | "push">
4088
5669
  | undefined;
4089
5670
  let hasDeliveredResults = false;
5671
+ let pendingUpdateProcessing: Promise<void> = Promise.resolve();
4090
5672
 
4091
5673
  const emitOnBatch = async (
4092
5674
  batch: ValueTypeFromRequest<Resolve, T, I>[],
@@ -4381,7 +5963,7 @@ export class DocumentIndex<
4381
5963
  return value as WithContext<I>;
4382
5964
  };
4383
5965
 
4384
- const onChange = async (evt: CustomEvent<DocumentsChange<T, I>>) => {
5966
+ const processChange = async (evt: CustomEvent<DocumentsChange<T, I>>) => {
4385
5967
  // Optional filter to mutate/suppress change events
4386
5968
  indexIteratorLogger.trace(
4387
5969
  "processing live update change event",
@@ -4523,6 +6105,13 @@ export class DocumentIndex<
4523
6105
  }
4524
6106
  signalUpdate();
4525
6107
  };
6108
+ const onChange = (evt: CustomEvent<DocumentsChange<T, I>>) => {
6109
+ const task = pendingUpdateProcessing.then(() => processChange(evt));
6110
+ pendingUpdateProcessing = task.catch((error) => {
6111
+ warn("Failed to process iterator update", error);
6112
+ });
6113
+ return task;
6114
+ };
4526
6115
 
4527
6116
  this.documentEvents.addEventListener("change", onChange);
4528
6117
  updatesCleanup = () => {