@peerbit/document 10.0.3 → 10.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.
Files changed (46) hide show
  1. package/dist/benchmark/index.js +114 -59
  2. package/dist/benchmark/index.js.map +1 -1
  3. package/dist/benchmark/iterate-replicate-2.js +117 -63
  4. package/dist/benchmark/iterate-replicate-2.js.map +1 -1
  5. package/dist/benchmark/iterate-replicate.js +106 -56
  6. package/dist/benchmark/iterate-replicate.js.map +1 -1
  7. package/dist/benchmark/memory/child.js +114 -59
  8. package/dist/benchmark/memory/child.js.map +1 -1
  9. package/dist/benchmark/replication.js +106 -52
  10. package/dist/benchmark/replication.js.map +1 -1
  11. package/dist/src/domain.d.ts.map +1 -1
  12. package/dist/src/domain.js +1 -3
  13. package/dist/src/domain.js.map +1 -1
  14. package/dist/src/events.d.ts +1 -1
  15. package/dist/src/events.d.ts.map +1 -1
  16. package/dist/src/index.d.ts +1 -1
  17. package/dist/src/index.d.ts.map +1 -1
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/most-common-query-predictor.d.ts +3 -3
  20. package/dist/src/most-common-query-predictor.d.ts.map +1 -1
  21. package/dist/src/most-common-query-predictor.js.map +1 -1
  22. package/dist/src/operation.js +175 -81
  23. package/dist/src/operation.js.map +1 -1
  24. package/dist/src/prefetch.d.ts +2 -2
  25. package/dist/src/prefetch.d.ts.map +1 -1
  26. package/dist/src/prefetch.js.map +1 -1
  27. package/dist/src/program.d.ts +2 -2
  28. package/dist/src/program.d.ts.map +1 -1
  29. package/dist/src/program.js +550 -508
  30. package/dist/src/program.js.map +1 -1
  31. package/dist/src/resumable-iterator.d.ts.map +1 -1
  32. package/dist/src/resumable-iterator.js +44 -0
  33. package/dist/src/resumable-iterator.js.map +1 -1
  34. package/dist/src/search.d.ts +14 -10
  35. package/dist/src/search.d.ts.map +1 -1
  36. package/dist/src/search.js +2477 -2119
  37. package/dist/src/search.js.map +1 -1
  38. package/package.json +21 -19
  39. package/src/domain.ts +1 -3
  40. package/src/events.ts +1 -1
  41. package/src/index.ts +1 -0
  42. package/src/most-common-query-predictor.ts +19 -5
  43. package/src/prefetch.ts +12 -3
  44. package/src/program.ts +7 -5
  45. package/src/resumable-iterator.ts +44 -0
  46. package/src/search.ts +567 -198
package/src/search.ts CHANGED
@@ -48,9 +48,14 @@ import { ResumableIterators } from "./resumable-iterator.js";
48
48
 
49
49
  const WARNING_WHEN_ITERATING_FOR_MORE_THAN = 1e5;
50
50
 
51
- const logger: ReturnType<typeof loggerFn> = loggerFn({
52
- module: "document-index",
53
- });
51
+ const logger = loggerFn("peerbit:program:document:search");
52
+ const warn = logger.newScope("warn");
53
+ const documentIndexLogger = loggerFn("peerbit:document:index");
54
+ const indexLifecycleLogger = documentIndexLogger.newScope("lifecycle");
55
+ const indexRpcLogger = documentIndexLogger.newScope("rpc");
56
+ const indexCacheLogger = documentIndexLogger.newScope("cache");
57
+ const indexPrefetchLogger = documentIndexLogger.newScope("prefetch");
58
+ const indexIteratorLogger = documentIndexLogger.newScope("iterate");
54
59
 
55
60
  type BufferedResult<T, I extends Record<string, any>> = {
56
61
  value: T;
@@ -63,7 +68,7 @@ export type UpdateMergeStrategy<
63
68
  T,
64
69
  I,
65
70
  Resolve extends boolean | undefined,
66
- RT = ValueTypeFromRequest<Resolve, T, I>,
71
+ _RT = ValueTypeFromRequest<Resolve, T, I>,
67
72
  > =
68
73
  | boolean
69
74
  | {
@@ -71,7 +76,7 @@ export type UpdateMergeStrategy<
71
76
  evt: DocumentsChange<T, I>,
72
77
  ) => MaybePromise<DocumentsChange<T, I> | void>;
73
78
  };
74
- export type ResultBatchReason = "initial" | "next" | "join" | "change";
79
+ export type UpdateReason = "initial" | "manual" | "join" | "change" | "push";
75
80
 
76
81
  export type UpdateCallbacks<
77
82
  T,
@@ -80,18 +85,18 @@ export type UpdateCallbacks<
80
85
  RT = ValueTypeFromRequest<Resolve, T, I>,
81
86
  > = {
82
87
  /**
83
- * Fires on raw DB change events (added/removed/updated).
84
- * Use if you want low-level inspection of change streams.
88
+ * Fires whenever the iterator detects new work (e.g. push, join, change).
89
+ * Ideal for reactive consumers that need to call `next()` or trigger UI work.
85
90
  */
86
- onChange?: (change: DocumentsChange<T, I>) => void | Promise<void>;
91
+ notify?: (reason: UpdateReason) => void | Promise<void>;
87
92
 
88
93
  /**
89
94
  * Fires whenever the iterator yields a batch to the consumer.
90
95
  * Good for external sync (e.g. React state).
91
96
  */
92
- onResults?: (
97
+ onBatch?: (
93
98
  batch: RT[],
94
- meta: { reason: ResultBatchReason },
99
+ meta: { reason: UpdateReason },
95
100
  ) => void | Promise<void>;
96
101
  };
97
102
 
@@ -108,7 +113,7 @@ export type UpdateOptions<T, I, Resolve extends boolean | undefined> =
108
113
  /** Live update behavior. Only sorted merging is supported; optional filter can mutate/ignore events. */
109
114
  merge?: UpdateMergeStrategy<T, I, Resolve>;
110
115
  /** Request push-style notifications backed by the prefetch channel. */
111
- push?: boolean;
116
+ push?: boolean | types.PushUpdatesMode;
112
117
  } & UpdateCallbacks<T, I, Resolve>);
113
118
 
114
119
  export type JoiningTargets = {
@@ -202,7 +207,7 @@ export type QueryOptions<T, I, D, Resolve extends boolean | undefined> = {
202
207
  closePolicy?: "onEmpty" | "manual";
203
208
  };
204
209
 
205
- export type GetOptions<T, I, D, Resolve extends boolean | undefined> = {
210
+ export type GetOptions<_T, _I, D, Resolve extends boolean | undefined> = {
206
211
  remote?:
207
212
  | boolean
208
213
  | RemoteQueryOptions<
@@ -452,6 +457,7 @@ function isSubclassOf(
452
457
  }
453
458
 
454
459
  const DEFAULT_TIMEOUT = 1e4;
460
+ const DEFAULT_KEEP_REMOTE_ITERATOR_TIMEOUT = 3e5;
455
461
  const DISCOVER_TIMEOUT_FALLBACK = 500;
456
462
 
457
463
  const DEFAULT_INDEX_BY = "id";
@@ -653,6 +659,9 @@ export class DocumentIndex<
653
659
  | types.SearchRequest
654
660
  | types.SearchRequestIndexed
655
661
  | types.IterationRequest;
662
+ resolveResults?: boolean;
663
+ pushMode?: types.PushUpdatesMode;
664
+ pushInFlight?: boolean;
656
665
  }
657
666
  >;
658
667
  private iteratorKeepAliveTimers?: Map<string, ReturnType<typeof setTimeout>>;
@@ -669,6 +678,167 @@ export class DocumentIndex<
669
678
  return this._valueEncoding;
670
679
  }
671
680
 
681
+ private ensurePrefetchAccumulator() {
682
+ if (!this._prefetch) {
683
+ this._prefetch = {
684
+ accumulator: new Prefetch(),
685
+ ttl: 5e3,
686
+ };
687
+ return;
688
+ }
689
+ if (!this._prefetch.accumulator) {
690
+ this._prefetch.accumulator = new Prefetch();
691
+ }
692
+ }
693
+
694
+ private async wrapPushResults(
695
+ matches: Array<WithContext<T> | WithContext<I>>,
696
+ resolve: boolean,
697
+ ): Promise<types.Result[]> {
698
+ if (!matches.length) return [];
699
+ const results: types.Result[] = [];
700
+ for (const match of matches) {
701
+ if (resolve) {
702
+ const doc = match as WithContext<T>;
703
+ const indexedValue = await this.transformer(doc as T, doc.__context);
704
+ const wrappedIndexed = coerceWithContext(indexedValue, doc.__context);
705
+ results.push(
706
+ new types.ResultValue({
707
+ context: doc.__context,
708
+ value: doc as T,
709
+ source: serialize(doc as T),
710
+ indexed: wrappedIndexed,
711
+ }),
712
+ );
713
+ } else {
714
+ const indexed = match as WithContext<I>;
715
+ const head = await this._log.log.get(indexed.__context.head);
716
+ results.push(
717
+ new types.ResultIndexedValue({
718
+ context: indexed.__context,
719
+ source: serialize(indexed as I),
720
+ indexed: indexed as I,
721
+ entries: head ? [head] : [],
722
+ }),
723
+ );
724
+ }
725
+ }
726
+ return results;
727
+ }
728
+
729
+ private async drainQueuedResults(
730
+ queueEntries: indexerTypes.IndexedResult<WithContext<I>>[],
731
+ resolve: boolean,
732
+ ): Promise<types.Result[]> {
733
+ if (!queueEntries.length) {
734
+ return [];
735
+ }
736
+ const drained = queueEntries.splice(0);
737
+ const results: types.Result[] = [];
738
+ for (const entry of drained) {
739
+ const indexedUnwrapped = Object.assign(
740
+ Object.create(this.indexedType.prototype),
741
+ entry.value,
742
+ );
743
+ if (resolve) {
744
+ const value = await this.resolveDocument({
745
+ indexed: entry.value,
746
+ head: entry.value.__context.head,
747
+ });
748
+ if (!value) continue;
749
+ results.push(
750
+ new types.ResultValue({
751
+ context: entry.value.__context,
752
+ value: value.value,
753
+ source: serialize(value.value),
754
+ indexed: indexedUnwrapped,
755
+ }),
756
+ );
757
+ } else {
758
+ const head = await this._log.log.get(entry.value.__context.head);
759
+ results.push(
760
+ new types.ResultIndexedValue({
761
+ context: entry.value.__context,
762
+ source: serialize(indexedUnwrapped),
763
+ indexed: indexedUnwrapped,
764
+ entries: head ? [head] : [],
765
+ }),
766
+ );
767
+ }
768
+ }
769
+ return results;
770
+ }
771
+
772
+ private handleDocumentChange = async (
773
+ event: CustomEvent<DocumentsChange<T, I>>,
774
+ ) => {
775
+ const added = event.detail.added;
776
+ if (!added.length) {
777
+ return;
778
+ }
779
+
780
+ for (const [_iteratorId, queue] of this._resultQueue) {
781
+ if (
782
+ !queue.pushMode ||
783
+ queue.pushMode !== types.PushUpdatesMode.STREAM ||
784
+ queue.pushInFlight
785
+ ) {
786
+ continue;
787
+ }
788
+ if (!(queue.fromQuery instanceof types.IterationRequest)) {
789
+ continue;
790
+ }
791
+ queue.pushInFlight = true;
792
+ try {
793
+ const resolveFlag =
794
+ queue.resolveResults ??
795
+ resolvesDocuments(queue.fromQuery as AnyIterationRequest);
796
+ const batches: types.Result[] = [];
797
+ const queued = await this.drainQueuedResults(queue.queue, resolveFlag);
798
+ if (queued.length) {
799
+ batches.push(...queued);
800
+ }
801
+ // TODO drain only up to the changed document instead of flushing the entire queue
802
+ const matches = await this.updateResults(
803
+ [],
804
+ { added },
805
+ {
806
+ query: queue.fromQuery.query,
807
+ sort: queue.fromQuery.sort,
808
+ },
809
+ resolveFlag,
810
+ );
811
+ if (matches.length) {
812
+ const wrapped = await this.wrapPushResults(matches, resolveFlag);
813
+ if (wrapped.length) {
814
+ batches.push(...wrapped);
815
+ }
816
+ }
817
+ if (!batches.length) {
818
+ continue;
819
+ }
820
+ const pushMessage = new types.PredictedSearchRequest({
821
+ id: queue.fromQuery.id,
822
+ request: queue.fromQuery,
823
+ results: new types.Results({
824
+ results: batches,
825
+ kept: 0n,
826
+ }),
827
+ });
828
+ await this._query.send(pushMessage, {
829
+ mode: new SilentDelivery({
830
+ to: [queue.from],
831
+ redundancy: 1,
832
+ }),
833
+ });
834
+ } catch (error) {
835
+ logger.error("Failed to push iterator update", error);
836
+ } finally {
837
+ queue.pushInFlight = false;
838
+ }
839
+ }
840
+ };
841
+
672
842
  private get nestedProperties() {
673
843
  return {
674
844
  match: (obj: any): obj is types.IDocumentStore<any> =>
@@ -681,6 +851,15 @@ export class DocumentIndex<
681
851
  }
682
852
  async open(properties: OpenOptions<T, I, D>) {
683
853
  this._log = properties.log;
854
+ // Allow reopening with partial options (tests override the index transform)
855
+ const previousEvents = this.documentEvents;
856
+ this.documentEvents =
857
+ properties.documentEvents ?? previousEvents ?? (this.events as any);
858
+ this.compatibility =
859
+ properties.compatibility !== undefined
860
+ ? properties.compatibility
861
+ : this.compatibility;
862
+
684
863
  let prefectOptions =
685
864
  typeof properties.prefetch === "object"
686
865
  ? properties.prefetch
@@ -701,8 +880,6 @@ export class DocumentIndex<
701
880
  this.indexedTypeIsDocumentType =
702
881
  !properties.transform?.type ||
703
882
  properties.transform?.type === properties.documentType;
704
- this.documentEvents = properties.documentEvents;
705
- this.compatibility = properties.compatibility;
706
883
  this.canRead = properties.canRead;
707
884
  this.canSearch = properties.canSearch;
708
885
  this.includeIndexed = properties.includeIndexed;
@@ -731,6 +908,8 @@ export class DocumentIndex<
731
908
  this.isProgramValued = isSubclassOf(this.documentType, Program);
732
909
  this.dbType = properties.dbType;
733
910
  this._resultQueue = new Map();
911
+ const replicateFn =
912
+ properties.replicate ?? this._sync ?? (() => Promise.resolve());
734
913
  this._sync = (request, results) => {
735
914
  let rq:
736
915
  | types.SearchRequest
@@ -760,7 +939,7 @@ export class DocumentIndex<
760
939
  >
761
940
  >;
762
941
  }
763
- return properties.replicate(rq, rs);
942
+ return replicateFn(rq, rs);
764
943
  };
765
944
 
766
945
  const transformOptions = properties.transform;
@@ -802,6 +981,14 @@ export class DocumentIndex<
802
981
  this.index = new CachedIndex(this.index, properties.cache.query);
803
982
  }
804
983
 
984
+ indexLifecycleLogger("opened document index", {
985
+ peer: this.node.identity.publicKey.hashcode(),
986
+ indexBy: this.indexBy,
987
+ includeIndexed: this.includeIndexed === true,
988
+ cacheResolver: Boolean(this._resolverCache),
989
+ prefetch: Boolean(this.prefetch),
990
+ });
991
+
805
992
  this._resumableIterators = new ResumableIterators(this.index);
806
993
  this._maybeOpen = properties.maybeOpen;
807
994
  if (this.isProgramValued) {
@@ -809,6 +996,10 @@ export class DocumentIndex<
809
996
  }
810
997
 
811
998
  if (this.prefetch?.predictor) {
999
+ indexPrefetchLogger("prefetch predictor enabled", {
1000
+ peer: this.node.identity.publicKey.hashcode(),
1001
+ strict: Boolean(this.prefetch?.strict),
1002
+ });
812
1003
  const predictor = this.prefetch.predictor;
813
1004
  this._joinListener = async (e: { detail: PublicSignKey }) => {
814
1005
  // on join we emit predicted search results before peers query us (to save latency but for the price of errornous bandwidth usage)
@@ -817,6 +1008,10 @@ export class DocumentIndex<
817
1008
  return;
818
1009
  }
819
1010
 
1011
+ indexPrefetchLogger("peer join triggered predictor", {
1012
+ target: e.detail.hashcode(),
1013
+ });
1014
+
820
1015
  // TODO
821
1016
  // it only makes sense for use to return predicted results if the peer is to choose us as a replicator
822
1017
  // so we need to calculate the cover set from the peers perspective
@@ -825,8 +1020,15 @@ export class DocumentIndex<
825
1020
  let request = predictor.predictedQuery(e.detail);
826
1021
 
827
1022
  if (!request) {
1023
+ indexPrefetchLogger("predictor had no cached query", {
1024
+ target: e.detail.hashcode(),
1025
+ });
828
1026
  return;
829
1027
  }
1028
+ indexPrefetchLogger("sending predicted results", {
1029
+ target: e.detail.hashcode(),
1030
+ request: request.idString,
1031
+ });
830
1032
  const results = await this.handleSearchRequest(request, {
831
1033
  from: e.detail,
832
1034
  });
@@ -845,7 +1047,9 @@ export class DocumentIndex<
845
1047
  };
846
1048
 
847
1049
  // we do this before _query.open so that we can receive the join event, even immediate ones
848
- this._query.events.addEventListener("join", this._joinListener);
1050
+ if (this._joinListener) {
1051
+ this._query.events.addEventListener("join", this._joinListener);
1052
+ }
849
1053
  }
850
1054
 
851
1055
  await this._query.open({
@@ -856,6 +1060,9 @@ export class DocumentIndex<
856
1060
  responseType: types.AbstractSearchResult,
857
1061
  queryType: types.AbstractSearchRequest,
858
1062
  });
1063
+ if (this.handleDocumentChange) {
1064
+ this.documentEvents.addEventListener("change", this.handleDocumentChange);
1065
+ }
859
1066
  }
860
1067
 
861
1068
  get prefetch() {
@@ -867,9 +1074,14 @@ export class DocumentIndex<
867
1074
  ctx: { from?: PublicSignKey; message: DataMessage },
868
1075
  ) {
869
1076
  if (!ctx.from) {
870
- logger.info("Receieved query without from");
1077
+ logger("Receieved query without from");
871
1078
  return;
872
1079
  }
1080
+ indexRpcLogger("received request", {
1081
+ type: query.constructor.name,
1082
+ from: ctx.from.hashcode(),
1083
+ id: (query as { idString?: string }).idString,
1084
+ });
873
1085
  if (query instanceof types.PredictedSearchRequest) {
874
1086
  // put results in a waiting cache so that we eventually in the future will query a matching thing, we already have results available
875
1087
  this._prefetch?.accumulator.add(
@@ -880,6 +1092,10 @@ export class DocumentIndex<
880
1092
  },
881
1093
  ctx.from!.hashcode(),
882
1094
  );
1095
+ indexPrefetchLogger("cached predicted results", {
1096
+ from: ctx.from.hashcode(),
1097
+ request: query.idString,
1098
+ });
883
1099
  return;
884
1100
  }
885
1101
 
@@ -894,22 +1110,32 @@ export class DocumentIndex<
894
1110
  });
895
1111
 
896
1112
  if (ignore) {
1113
+ indexPrefetchLogger("predictor ignored request", {
1114
+ from: ctx.from!.hashcode(),
1115
+ request: (query as { idString?: string }).idString,
1116
+ strict: Boolean(this.prefetch?.strict),
1117
+ });
897
1118
  if (this.prefetch.strict) {
898
1119
  return;
899
1120
  }
900
1121
  }
901
1122
  }
902
1123
 
903
- return this.handleSearchRequest(
904
- query as
905
- | types.SearchRequest
906
- | types.SearchRequestIndexed
907
- | types.IterationRequest
908
- | types.CollectNextRequest,
909
- {
910
- from: ctx.from!,
911
- },
912
- );
1124
+ try {
1125
+ const out = await this.handleSearchRequest(
1126
+ query as
1127
+ | types.SearchRequest
1128
+ | types.SearchRequestIndexed
1129
+ | types.IterationRequest
1130
+ | types.CollectNextRequest,
1131
+ {
1132
+ from: ctx.from!,
1133
+ },
1134
+ );
1135
+ return out;
1136
+ } catch (error) {
1137
+ throw error;
1138
+ }
913
1139
  }
914
1140
  private async handleSearchRequest(
915
1141
  query:
@@ -919,6 +1145,11 @@ export class DocumentIndex<
919
1145
  | types.CollectNextRequest,
920
1146
  ctx: { from: PublicSignKey },
921
1147
  ) {
1148
+ indexRpcLogger("handling query", {
1149
+ type: query.constructor.name,
1150
+ id: (query as { idString?: string }).idString,
1151
+ from: ctx.from.hashcode(),
1152
+ });
922
1153
  if (
923
1154
  this.canSearch &&
924
1155
  (query instanceof types.SearchRequest ||
@@ -932,6 +1163,10 @@ export class DocumentIndex<
932
1163
  ctx.from,
933
1164
  ))
934
1165
  ) {
1166
+ indexRpcLogger("denied query", {
1167
+ id: (query as { idString?: string }).idString,
1168
+ from: ctx.from.hashcode(),
1169
+ });
935
1170
  return new types.NoAccess();
936
1171
  }
937
1172
 
@@ -962,6 +1197,13 @@ export class DocumentIndex<
962
1197
  canRead: this.canRead,
963
1198
  },
964
1199
  );
1200
+ indexRpcLogger("query results ready", {
1201
+ id: (query as { idString?: string }).idString,
1202
+ from: ctx.from.hashcode(),
1203
+ count: results.results.length,
1204
+ kept: results.kept,
1205
+ includeIndexed: shouldIncludedIndexedResults,
1206
+ });
965
1207
 
966
1208
  if (shouldIncludedIndexedResults) {
967
1209
  let resultsWithIndexed: (
@@ -1040,7 +1282,15 @@ export class DocumentIndex<
1040
1282
  async close(from?: Program): Promise<boolean> {
1041
1283
  const closed = await super.close(from);
1042
1284
  if (closed) {
1043
- this._query.events.removeEventListener("join", this._joinListener);
1285
+ if (this._joinListener) {
1286
+ this._query.events.removeEventListener("join", this._joinListener);
1287
+ }
1288
+ if (this.handleDocumentChange) {
1289
+ this.documentEvents.removeEventListener(
1290
+ "change",
1291
+ this.handleDocumentChange,
1292
+ );
1293
+ }
1044
1294
  this.clearAllResultQueues();
1045
1295
  await this.index.stop?.();
1046
1296
  }
@@ -1050,6 +1300,10 @@ export class DocumentIndex<
1050
1300
  async drop(from?: Program): Promise<boolean> {
1051
1301
  const dropped = await super.drop(from);
1052
1302
  if (dropped) {
1303
+ this.documentEvents.removeEventListener(
1304
+ "change",
1305
+ this.handleDocumentChange,
1306
+ );
1053
1307
  this.clearAllResultQueues();
1054
1308
  await this.index.drop?.();
1055
1309
  await this.index.stop?.();
@@ -1137,7 +1391,7 @@ export class DocumentIndex<
1137
1391
  // Re-query on peer joins (like iterate), scoped to the joining peer
1138
1392
  let joinListener: (() => void) | undefined;
1139
1393
  if (baseRemote) {
1140
- joinListener = this.attachJoinListener({
1394
+ joinListener = this.createReplicatorJoinListener({
1141
1395
  eager: baseRemote.reach?.eager,
1142
1396
  onPeer: async (pk) => {
1143
1397
  if (cleanedUp) return;
@@ -1218,8 +1472,12 @@ export class DocumentIndex<
1218
1472
  ) {
1219
1473
  // TODO make last condition more efficient if there are many docs
1220
1474
  this._resolverProgramCache!.set(idString, value);
1475
+ indexCacheLogger("cache:set:program", { id: idString });
1221
1476
  } else {
1222
- this._resolverCache?.add(idString, value);
1477
+ if (this._resolverCache) {
1478
+ this._resolverCache.add(idString, value);
1479
+ indexCacheLogger("cache:set:value", { id: idString });
1480
+ }
1223
1481
  }
1224
1482
  const valueToIndex = await this.transformer(value, context);
1225
1483
  const wrappedValueToIndex = new this.wrappedIndexedType(
@@ -1238,8 +1496,11 @@ export class DocumentIndex<
1238
1496
  public del(key: indexerTypes.IdKey) {
1239
1497
  if (this.isProgramValued) {
1240
1498
  this._resolverProgramCache!.delete(key.primitive);
1499
+ indexCacheLogger("cache:del:program", { id: key.primitive });
1241
1500
  } else {
1242
- this._resolverCache?.del(key.primitive);
1501
+ if (this._resolverCache?.del(key.primitive)) {
1502
+ indexCacheLogger("cache:del:value", { id: key.primitive });
1503
+ }
1243
1504
  }
1244
1505
  return this.index.del({
1245
1506
  query: [indexerTypes.getMatcher(this.indexBy, key.key)],
@@ -1513,12 +1774,12 @@ export class DocumentIndex<
1513
1774
  ) {
1514
1775
  keepAliveRequest = cachedRequest;
1515
1776
  }
1516
- indexedResult =
1517
- prevQueued?.keptInIndex === 0
1518
- ? []
1519
- : await this._resumableIterators.next(query, {
1520
- keepAlive: keepAliveRequest !== undefined,
1521
- });
1777
+ const hasResumable = this._resumableIterators.has(query.idString);
1778
+ indexedResult = hasResumable
1779
+ ? await this._resumableIterators.next(query, {
1780
+ keepAlive: keepAliveRequest !== undefined,
1781
+ })
1782
+ : [];
1522
1783
  } else {
1523
1784
  throw new Error("Unsupported");
1524
1785
  }
@@ -1545,6 +1806,9 @@ export class DocumentIndex<
1545
1806
  let kept = (await this._resumableIterators.getPending(query.idString)) ?? 0;
1546
1807
 
1547
1808
  if (!isLocal) {
1809
+ const resolveFlag = resolvesDocuments(
1810
+ (fromQuery || query) as AnyIterationRequest,
1811
+ );
1548
1812
  prevQueued = {
1549
1813
  from,
1550
1814
  queue: [],
@@ -1556,7 +1820,14 @@ export class DocumentIndex<
1556
1820
  | types.SearchRequest
1557
1821
  | types.SearchRequestIndexed
1558
1822
  | types.IterationRequest,
1823
+ resolveResults: resolveFlag,
1559
1824
  };
1825
+ if (
1826
+ fromQuery instanceof types.IterationRequest &&
1827
+ fromQuery.pushUpdates
1828
+ ) {
1829
+ prevQueued.pushMode = fromQuery.pushUpdates;
1830
+ }
1560
1831
  this._resultQueue.set(query.idString, prevQueued);
1561
1832
  }
1562
1833
 
@@ -1623,8 +1894,12 @@ export class DocumentIndex<
1623
1894
  results: filteredResults,
1624
1895
  kept: BigInt(kept + (prevQueued?.queue.length || 0)),
1625
1896
  });
1897
+ const keepAliveActive = keepAliveRequest !== undefined;
1898
+ const pushActive =
1899
+ fromQuery instanceof types.IterationRequest &&
1900
+ Boolean(fromQuery.pushUpdates);
1626
1901
 
1627
- if (!isLocal && results.kept === 0n) {
1902
+ if (!isLocal && results.kept === 0n && !keepAliveActive && !pushActive) {
1628
1903
  this.clearResultsQueue(query);
1629
1904
  }
1630
1905
 
@@ -1823,7 +2098,7 @@ export class DocumentIndex<
1823
2098
 
1824
2099
  // Utility: attach a join listener that waits until a peer is a replicator,
1825
2100
  // then invokes the provided callback. Returns a detach function.
1826
- private attachJoinListener(params: {
2101
+ private createReplicatorJoinListener(params: {
1827
2102
  signal?: AbortSignal;
1828
2103
  eager?: boolean;
1829
2104
  onPeer: (pk: PublicSignKey) => Promise<void> | void;
@@ -1837,13 +2112,15 @@ export class DocumentIndex<
1837
2112
  if (active.has(hash)) return;
1838
2113
  active.add(hash);
1839
2114
  try {
1840
- await this._log
2115
+ const isReplicator = await this._log
1841
2116
  .waitForReplicator(pk, {
1842
2117
  signal: params.signal,
1843
2118
  eager: params.eager,
1844
2119
  })
1845
- .catch(() => undefined);
1846
- if (params.signal?.aborted) return;
2120
+ .then(() => true)
2121
+ .catch(() => false);
2122
+ if (!isReplicator || params.signal?.aborted) return;
2123
+ indexIteratorLogger.trace("peer joined as replicator", { peer: hash });
1847
2124
  await params.onPeer(pk);
1848
2125
  } finally {
1849
2126
  active.delete(hash);
@@ -1858,9 +2135,15 @@ export class DocumentIndex<
1858
2135
  query: types.CloseIteratorRequest,
1859
2136
  publicKey: PublicSignKey,
1860
2137
  ): void {
2138
+ indexIteratorLogger.trace("close request", {
2139
+ id: query.idString,
2140
+ from: publicKey.hashcode(),
2141
+ });
1861
2142
  const queueData = this._resultQueue.get(query.idString);
1862
2143
  if (queueData && !queueData.from.equals(publicKey)) {
1863
- logger.info("Ignoring close iterator request from different peer");
2144
+ indexIteratorLogger.trace(
2145
+ "Ignoring close iterator request from different peer",
2146
+ );
1864
2147
  return;
1865
2148
  }
1866
2149
  this.cancelIteratorKeepAlive(query.idString);
@@ -2072,7 +2355,7 @@ export class DocumentIndex<
2072
2355
  ));
2073
2356
  } catch (error) {
2074
2357
  if (error instanceof MissingResponsesError) {
2075
- logger.warn("Did not reciveve responses from all shard");
2358
+ warn("Did not reciveve responses from all shard");
2076
2359
  if (remote?.throwOnMissing) {
2077
2360
  throw error;
2078
2361
  }
@@ -2242,6 +2525,106 @@ export class DocumentIndex<
2242
2525
  this.compatibility,
2243
2526
  );
2244
2527
 
2528
+ const self = this;
2529
+ function normalizeUpdatesOption(u?: UpdateOptions<T, I, Resolve>): {
2530
+ mergePolicy?: {
2531
+ merge?:
2532
+ | {
2533
+ filter?: (
2534
+ evt: DocumentsChange<T, I>,
2535
+ ) => MaybePromise<DocumentsChange<T, I> | void>;
2536
+ }
2537
+ | undefined;
2538
+ };
2539
+ push?: types.PushUpdatesMode;
2540
+ callbacks?: UpdateCallbacks<T, I, Resolve>;
2541
+ } {
2542
+ const identityFilter = (evt: DocumentsChange<T, I>) => evt;
2543
+ const buildMergePolicy = (
2544
+ merge: UpdateMergeStrategy<T, I, Resolve> | undefined,
2545
+ defaultEnabled: boolean,
2546
+ ) => {
2547
+ const effective =
2548
+ merge === undefined ? (defaultEnabled ? true : undefined) : merge;
2549
+ if (effective === undefined || effective === false) {
2550
+ return undefined;
2551
+ }
2552
+ if (effective === true) {
2553
+ return {
2554
+ merge: {
2555
+ filter: identityFilter,
2556
+ },
2557
+ };
2558
+ }
2559
+ return {
2560
+ merge: {
2561
+ filter: effective.filter ?? identityFilter,
2562
+ },
2563
+ };
2564
+ };
2565
+
2566
+ if (u == null || u === false) {
2567
+ return {};
2568
+ }
2569
+
2570
+ if (u === true) {
2571
+ return {
2572
+ mergePolicy: buildMergePolicy(true, true),
2573
+ push: undefined,
2574
+ };
2575
+ }
2576
+
2577
+ if (typeof u === "string") {
2578
+ if (u === "remote") {
2579
+ self.ensurePrefetchAccumulator();
2580
+ return { push: types.PushUpdatesMode.STREAM };
2581
+ }
2582
+ if (u === "local") {
2583
+ return {
2584
+ mergePolicy: buildMergePolicy(true, true),
2585
+ push: undefined,
2586
+ };
2587
+ }
2588
+ if (u === "all") {
2589
+ self.ensurePrefetchAccumulator();
2590
+ return {
2591
+ mergePolicy: buildMergePolicy(true, true),
2592
+ push: types.PushUpdatesMode.STREAM,
2593
+ };
2594
+ }
2595
+ }
2596
+
2597
+ if (typeof u === "object") {
2598
+ const hasMergeProp = Object.prototype.hasOwnProperty.call(u, "merge");
2599
+ const mergeValue = hasMergeProp ? u.merge : undefined;
2600
+ if (u.push) {
2601
+ self.ensurePrefetchAccumulator();
2602
+ }
2603
+ const callbacks =
2604
+ u.notify || u.onBatch
2605
+ ? {
2606
+ notify: u.notify,
2607
+ onBatch: u.onBatch,
2608
+ }
2609
+ : undefined;
2610
+ return {
2611
+ mergePolicy: buildMergePolicy(
2612
+ mergeValue,
2613
+ !hasMergeProp || mergeValue === undefined,
2614
+ ),
2615
+ push:
2616
+ typeof u.push === "number"
2617
+ ? u.push
2618
+ : u.push
2619
+ ? types.PushUpdatesMode.STREAM
2620
+ : undefined,
2621
+ callbacks,
2622
+ };
2623
+ }
2624
+
2625
+ return {};
2626
+ }
2627
+
2245
2628
  const {
2246
2629
  mergePolicy,
2247
2630
  push: pushUpdates,
@@ -2301,6 +2684,11 @@ export class DocumentIndex<
2301
2684
  queryRequestCoerced.replicate = true;
2302
2685
  }
2303
2686
 
2687
+ indexIteratorLogger.trace("Iterate with options", {
2688
+ query: queryRequestCoerced,
2689
+ options,
2690
+ });
2691
+
2304
2692
  let fetchPromise: Promise<any> | undefined = undefined;
2305
2693
  const peerBufferMap: Map<
2306
2694
  string,
@@ -2331,10 +2719,10 @@ export class DocumentIndex<
2331
2719
  let first = false;
2332
2720
 
2333
2721
  // TODO handle join/leave while iterating
2334
- const controller: AbortController | undefined = undefined;
2722
+ let controller: AbortController | undefined = undefined;
2335
2723
  const ensureController = () => {
2336
2724
  if (!controller) {
2337
- return new AbortController();
2725
+ return (controller = new AbortController());
2338
2726
  }
2339
2727
  return controller;
2340
2728
  };
@@ -2347,6 +2735,7 @@ export class DocumentIndex<
2347
2735
  context: types.Context;
2348
2736
  }
2349
2737
  | undefined = undefined;
2738
+ let lastDeliveredIndexed: WithContext<I> | undefined;
2350
2739
 
2351
2740
  const peerBuffers = (): {
2352
2741
  indexed: WithContext<I>;
@@ -2357,6 +2746,47 @@ export class DocumentIndex<
2357
2746
  return [...peerBufferMap.values()].map((x) => x.buffer).flat();
2358
2747
  };
2359
2748
 
2749
+ const toIndexedForOrdering = (
2750
+ value:
2751
+ | ValueTypeFromRequest<Resolve, T, I>
2752
+ | WithContext<I>
2753
+ | WithIndexedContext<T, I>,
2754
+ ): WithContext<I> | undefined => {
2755
+ const candidate = value as any;
2756
+ if (candidate && typeof candidate === "object") {
2757
+ if ("__indexed" in candidate && candidate.__indexed) {
2758
+ return coerceWithContext(candidate.__indexed, candidate.__context);
2759
+ }
2760
+ if ("__context" in candidate) {
2761
+ return candidate as WithContext<I>;
2762
+ }
2763
+ }
2764
+ return undefined;
2765
+ };
2766
+
2767
+ const updateLastDelivered = (
2768
+ batch: ValueTypeFromRequest<Resolve, T, I>[],
2769
+ ) => {
2770
+ if (!batch.length) {
2771
+ return;
2772
+ }
2773
+ const indexed = toIndexedForOrdering(batch[batch.length - 1]);
2774
+ if (indexed) {
2775
+ lastDeliveredIndexed = indexed;
2776
+ }
2777
+ };
2778
+
2779
+ const compareIndexed = (a: WithContext<I>, b: WithContext<I>): number => {
2780
+ return indexerTypes.extractSortCompare(a, b, queryRequestCoerced.sort);
2781
+ };
2782
+
2783
+ const isLateResult = (indexed: WithContext<I>) => {
2784
+ if (!lastDeliveredIndexed) {
2785
+ return false;
2786
+ }
2787
+ return compareIndexed(indexed, lastDeliveredIndexed) < 0;
2788
+ };
2789
+
2360
2790
  let maybeSetDone = () => {
2361
2791
  cleanup();
2362
2792
  done = true;
@@ -2510,13 +2940,21 @@ export class DocumentIndex<
2510
2940
  if (keepRemoteAlive) {
2511
2941
  peerBufferMap.set(from.hashcode(), {
2512
2942
  buffer,
2513
- kept: Number(response.kept),
2943
+ kept: 0,
2514
2944
  });
2515
2945
  }
2516
2946
  return;
2517
2947
  }
2518
2948
 
2519
- if (results.kept > 0n) {
2949
+ const reqFetch = queryRequestCoerced.fetch ?? 0;
2950
+ const inferredMore =
2951
+ reqFetch > 0 && results.results.length > reqFetch;
2952
+ const effectiveKept = Math.max(
2953
+ Number(results.kept),
2954
+ inferredMore ? 1 : 0,
2955
+ );
2956
+
2957
+ if (effectiveKept > 0) {
2520
2958
  hasMore = true;
2521
2959
  }
2522
2960
 
@@ -2577,7 +3015,7 @@ export class DocumentIndex<
2577
3015
 
2578
3016
  peerBufferMap.set(from.hashcode(), {
2579
3017
  buffer,
2580
- kept: Number(response.kept),
3018
+ kept: effectiveKept,
2581
3019
  });
2582
3020
  } else {
2583
3021
  throw new Error(
@@ -3002,8 +3440,9 @@ export class DocumentIndex<
3002
3440
  // no extra queued-first/last in simplified API
3003
3441
 
3004
3442
  const deduped = dedup(coercedBatch, this.indexByResolver);
3005
- const fallbackReason = hasDeliveredResults ? "next" : "initial";
3006
- await emitOnResults(deduped, fallbackReason);
3443
+ const fallbackReason = hasDeliveredResults ? "manual" : "initial";
3444
+ updateLastDelivered(deduped);
3445
+ await emitOnBatch(deduped, fallbackReason);
3007
3446
  return deduped;
3008
3447
  };
3009
3448
 
@@ -3012,6 +3451,7 @@ export class DocumentIndex<
3012
3451
  (controller as AbortController | undefined)?.abort(
3013
3452
  new AbortError("Iterator closed"),
3014
3453
  );
3454
+ controller = undefined;
3015
3455
  this.prefetch?.accumulator.clear(queryRequestCoerced);
3016
3456
  this.processCloseIteratorRequest(
3017
3457
  queryRequestCoerced,
@@ -3057,119 +3497,53 @@ export class DocumentIndex<
3057
3497
  let fetchedFirstForRemote: Set<string> | undefined = undefined;
3058
3498
 
3059
3499
  let updateDeferred: ReturnType<typeof pDefer> | undefined;
3060
- const signalUpdate = (reason?: string) => {
3500
+ const onLateResults =
3501
+ typeof options?.remote === "object" &&
3502
+ typeof options.remote.onLateResults === "function"
3503
+ ? options.remote.onLateResults
3504
+ : undefined;
3505
+ const runNotify = (reason: UpdateReason) => {
3506
+ if (!updateCallbacks?.notify) {
3507
+ return;
3508
+ }
3509
+ Promise.resolve(updateCallbacks.notify(reason)).catch((error) => {
3510
+ warn("Update notify callback failed", error);
3511
+ });
3512
+ };
3513
+ const signalUpdate = (reason?: UpdateReason) => {
3514
+ if (reason) {
3515
+ runNotify(reason);
3516
+ }
3061
3517
  updateDeferred?.resolve();
3062
3518
  };
3063
3519
  const _waitForUpdate = () =>
3064
3520
  updateDeferred ? updateDeferred.promise : Promise.resolve();
3065
3521
 
3066
3522
  // ---------------- Live updates wiring (sorted-only with optional filter) ----------------
3067
- function normalizeUpdatesOption(u?: UpdateOptions<T, I, Resolve>): {
3068
- mergePolicy?: {
3069
- merge?:
3070
- | {
3071
- filter?: (
3072
- evt: DocumentsChange<T, I>,
3073
- ) => MaybePromise<DocumentsChange<T, I> | void>;
3074
- }
3075
- | undefined;
3076
- };
3077
- push: boolean;
3078
- callbacks?: UpdateCallbacks<T, I, Resolve>;
3079
- } {
3080
- const identityFilter = (evt: DocumentsChange<T, I>) => evt;
3081
- const buildMergePolicy = (
3082
- merge: UpdateMergeStrategy<T, I, Resolve> | undefined,
3083
- defaultEnabled: boolean,
3084
- ) => {
3085
- const effective =
3086
- merge === undefined ? (defaultEnabled ? true : undefined) : merge;
3087
- if (effective === undefined || effective === false) {
3088
- return undefined;
3089
- }
3090
- if (effective === true) {
3091
- return {
3092
- merge: {
3093
- filter: identityFilter,
3094
- },
3095
- };
3096
- }
3097
- return {
3098
- merge: {
3099
- filter: effective.filter ?? identityFilter,
3100
- },
3101
- };
3102
- };
3103
-
3104
- if (u == null || u === false) {
3105
- return { push: false };
3106
- }
3107
-
3108
- if (u === true) {
3109
- return {
3110
- mergePolicy: buildMergePolicy(true, true),
3111
- push: false,
3112
- };
3113
- }
3114
-
3115
- if (typeof u === "string") {
3116
- if (u === "remote") {
3117
- return { push: true };
3118
- }
3119
- if (u === "local") {
3120
- return {
3121
- mergePolicy: buildMergePolicy(true, true),
3122
- push: false,
3123
- };
3124
- }
3125
- if (u === "all") {
3126
- return {
3127
- mergePolicy: buildMergePolicy(true, true),
3128
- push: true,
3129
- };
3130
- }
3131
- }
3132
-
3133
- if (typeof u === "object") {
3134
- const hasMergeProp = Object.prototype.hasOwnProperty.call(u, "merge");
3135
- const mergeValue = hasMergeProp ? u.merge : undefined;
3136
- return {
3137
- mergePolicy: buildMergePolicy(
3138
- mergeValue,
3139
- !hasMergeProp || mergeValue === undefined,
3140
- ),
3141
- push: Boolean(u.push),
3142
- callbacks: u,
3143
- };
3144
- }
3145
-
3146
- return { push: false };
3147
- }
3148
-
3149
3523
  const updateCallbacks = updateCallbacksRaw;
3150
- let pendingResultsReason:
3151
- | Extract<ResultBatchReason, "join" | "change">
3524
+ let pendingBatchReason:
3525
+ | Extract<UpdateReason, "join" | "change" | "push">
3152
3526
  | undefined;
3153
3527
  let hasDeliveredResults = false;
3154
3528
 
3155
- const emitOnResults = async (
3529
+ const emitOnBatch = async (
3156
3530
  batch: ValueTypeFromRequest<Resolve, T, I>[],
3157
- defaultReason: Extract<ResultBatchReason, "initial" | "next">,
3531
+ defaultReason: Extract<UpdateReason, "initial" | "manual">,
3158
3532
  ) => {
3159
- if (!updateCallbacks?.onResults || batch.length === 0) {
3533
+ if (!updateCallbacks?.onBatch || batch.length === 0) {
3160
3534
  return;
3161
3535
  }
3162
- let reason: ResultBatchReason;
3163
- if (pendingResultsReason) {
3164
- reason = pendingResultsReason;
3536
+ let reason: UpdateReason;
3537
+ if (pendingBatchReason) {
3538
+ reason = pendingBatchReason;
3165
3539
  } else if (!hasDeliveredResults) {
3166
3540
  reason = "initial";
3167
3541
  } else {
3168
3542
  reason = defaultReason;
3169
3543
  }
3170
- pendingResultsReason = undefined;
3544
+ pendingBatchReason = undefined;
3171
3545
  hasDeliveredResults = true;
3172
- await updateCallbacks.onResults(batch, { reason });
3546
+ await updateCallbacks.onBatch(batch, { reason });
3173
3547
  };
3174
3548
 
3175
3549
  // sorted-only mode: no per-queue handling
@@ -3190,18 +3564,19 @@ export class DocumentIndex<
3190
3564
  queryRequestCoerced.replicate = replicateFlag;
3191
3565
  const ttlSource =
3192
3566
  typeof remoteOptions === "object" &&
3193
- typeof remoteOptions?.wait === "object"
3194
- ? (remoteOptions.wait.timeout ?? DEFAULT_TIMEOUT)
3195
- : DEFAULT_TIMEOUT;
3567
+ typeof remoteOptions?.wait === "object" &&
3568
+ remoteOptions.wait.behavior === "block"
3569
+ ? (remoteOptions.wait.timeout ?? DEFAULT_KEEP_REMOTE_ITERATOR_TIMEOUT)
3570
+ : DEFAULT_KEEP_REMOTE_ITERATOR_TIMEOUT;
3196
3571
  queryRequestCoerced.keepAliveTtl = keepRemoteAlive
3197
3572
  ? BigInt(ttlSource)
3198
3573
  : undefined;
3199
- queryRequestCoerced.pushUpdates = pushUpdates ? true : undefined;
3574
+ queryRequestCoerced.pushUpdates = pushUpdates;
3200
3575
  queryRequestCoerced.mergeUpdates = mergePolicy?.merge ? true : undefined;
3201
3576
  }
3202
3577
 
3203
3578
  if (pushUpdates && this.prefetch?.accumulator) {
3204
- const targetPrefetchKey = idAgnosticQueryKey(queryRequestCoerced);
3579
+ const currentPrefetchKey = () => idAgnosticQueryKey(queryRequestCoerced);
3205
3580
  const mergePrefetchedResults = async (
3206
3581
  from: PublicSignKey,
3207
3582
  results: types.Results<types.ResultTypeFromRequest<R, T, I>>,
@@ -3243,14 +3618,19 @@ export class DocumentIndex<
3243
3618
  continue;
3244
3619
  }
3245
3620
  visited.add(indexKey);
3621
+ const indexed = await this.resolveIndexed<R>(
3622
+ result,
3623
+ results.results as types.ResultTypeFromRequest<R, T, I>[],
3624
+ );
3625
+ if (isLateResult(indexed)) {
3626
+ onLateResults?.({ amount: 1, peer: from });
3627
+ continue;
3628
+ }
3246
3629
  buffer.push({
3247
3630
  value: result.value as types.ResultTypeFromRequest<R, T, I>,
3248
3631
  context: result.context,
3249
3632
  from,
3250
- indexed: await this.resolveIndexed<R>(
3251
- result,
3252
- results.results as types.ResultTypeFromRequest<R, T, I>[],
3253
- ),
3633
+ indexed,
3254
3634
  });
3255
3635
  } else {
3256
3636
  if (visited.has(indexKey) && !indexedPlaceholders?.has(indexKey)) {
@@ -3261,6 +3641,10 @@ export class DocumentIndex<
3261
3641
  result.indexed || result.value,
3262
3642
  result.context,
3263
3643
  );
3644
+ if (isLateResult(indexed)) {
3645
+ onLateResults?.({ amount: 1, peer: from });
3646
+ continue;
3647
+ }
3264
3648
  const placeholder = {
3265
3649
  value: result.value,
3266
3650
  context: result.context,
@@ -3274,7 +3658,9 @@ export class DocumentIndex<
3274
3658
 
3275
3659
  peerBufferMap.set(peerHash, {
3276
3660
  buffer,
3277
- kept: Number(results.kept),
3661
+ // Prefetched batches should not claim remote pending counts;
3662
+ // we'll collect more explicitly if needed.
3663
+ kept: 0,
3278
3664
  });
3279
3665
  };
3280
3666
 
@@ -3285,7 +3671,7 @@ export class DocumentIndex<
3285
3671
  if (!request) {
3286
3672
  return;
3287
3673
  }
3288
- if (idAgnosticQueryKey(request) !== targetPrefetchKey) {
3674
+ if (idAgnosticQueryKey(request) !== currentPrefetchKey()) {
3289
3675
  return;
3290
3676
  }
3291
3677
  try {
@@ -3317,12 +3703,12 @@ export class DocumentIndex<
3317
3703
  );
3318
3704
  }
3319
3705
 
3320
- if (!pendingResultsReason) {
3321
- pendingResultsReason = "change";
3706
+ if (!pendingBatchReason) {
3707
+ pendingBatchReason = "push";
3322
3708
  }
3323
- signalUpdate("prefetch-add");
3709
+ signalUpdate("push");
3324
3710
  } catch (error) {
3325
- logger.warn("Failed to merge prefetched results", error);
3711
+ warn("Failed to merge prefetched results", error);
3326
3712
  }
3327
3713
  };
3328
3714
 
@@ -3393,15 +3779,15 @@ export class DocumentIndex<
3393
3779
 
3394
3780
  const onChange = async (evt: CustomEvent<DocumentsChange<T, I>>) => {
3395
3781
  // Optional filter to mutate/suppress change events
3782
+ indexIteratorLogger.trace(
3783
+ "processing live update change event",
3784
+ evt.detail,
3785
+ );
3396
3786
  let filtered: DocumentsChange<T, I> | void = evt.detail;
3397
3787
  if (mergePolicy?.merge?.filter) {
3398
3788
  filtered = await mergePolicy.merge?.filter(evt.detail);
3399
3789
  }
3400
3790
  if (filtered) {
3401
- const changeForCallback: DocumentsChange<T, I> = {
3402
- added: [],
3403
- removed: [],
3404
- };
3405
3791
  let hasRelevantChange = false;
3406
3792
 
3407
3793
  // Remove entries that were deleted from all pending structures
@@ -3432,14 +3818,6 @@ export class DocumentIndex<
3432
3818
  }
3433
3819
  if (matchedRemovedIds.size > 0) {
3434
3820
  hasRelevantChange = true;
3435
- for (const removed of filtered.removed) {
3436
- const id = indexerTypes.toId(
3437
- this.indexByResolver(removed.__indexed),
3438
- ).primitive;
3439
- if (matchedRemovedIds.has(id)) {
3440
- changeForCallback.removed.push(removed);
3441
- }
3442
- }
3443
3821
  }
3444
3822
  }
3445
3823
 
@@ -3483,6 +3861,10 @@ export class DocumentIndex<
3483
3861
  continue;
3484
3862
  }
3485
3863
  }
3864
+ if (isLateResult(indexedCandidate)) {
3865
+ onLateResults?.({ amount: 1 });
3866
+ continue;
3867
+ }
3486
3868
  const id = indexerTypes.toId(
3487
3869
  this.indexByResolver(indexedCandidate),
3488
3870
  ).primitive;
@@ -3513,21 +3895,15 @@ export class DocumentIndex<
3513
3895
  ensureIndexedPlaceholders().set(id, placeholder);
3514
3896
  }
3515
3897
  hasRelevantChange = true;
3516
- changeForCallback.added.push(added);
3517
3898
  }
3518
3899
  }
3519
3900
 
3520
3901
  if (hasRelevantChange) {
3521
- if (!pendingResultsReason) {
3522
- pendingResultsReason = "change";
3523
- }
3524
- if (
3525
- changeForCallback.added.length > 0 ||
3526
- changeForCallback.removed.length > 0
3527
- ) {
3528
- updateCallbacks?.onChange?.(changeForCallback);
3529
- signalUpdate("change");
3902
+ runNotify("change");
3903
+ if (!pendingBatchReason) {
3904
+ pendingBatchReason = "change";
3530
3905
  }
3906
+ signalUpdate();
3531
3907
  }
3532
3908
  }
3533
3909
  signalUpdate();
@@ -3549,13 +3925,6 @@ export class DocumentIndex<
3549
3925
 
3550
3926
  updateDeferred = pDefer<void>();
3551
3927
 
3552
- // derive optional onMissedResults callback if provided
3553
- let onMissedResults =
3554
- typeof options?.remote?.wait === "object" &&
3555
- typeof options?.remote.onLateResults === "function"
3556
- ? options.remote.onLateResults
3557
- : undefined;
3558
-
3559
3928
  const waitForTime =
3560
3929
  typeof options.remote.wait === "object" && options.remote.wait.timeout;
3561
3930
 
@@ -3572,7 +3941,7 @@ export class DocumentIndex<
3572
3941
  }, waitForTime);
3573
3942
  ensureController().signal.addEventListener("abort", () => signalUpdate());
3574
3943
  fetchedFirstForRemote = new Set<string>();
3575
- joinListener = this.attachJoinListener({
3944
+ joinListener = this.createReplicatorJoinListener({
3576
3945
  signal: ensureController().signal,
3577
3946
  eager: options.remote.reach?.eager,
3578
3947
  onPeer: async (pk) => {
@@ -3587,7 +3956,7 @@ export class DocumentIndex<
3587
3956
  fetchedFirstForRemote,
3588
3957
  });
3589
3958
  await fetchPromise;
3590
- if (onMissedResults) {
3959
+ if (onLateResults) {
3591
3960
  const pending = peerBufferMap.get(hash)?.buffer;
3592
3961
  if (pending && pending.length > 0) {
3593
3962
  if (lastValueInOrder) {
@@ -3604,18 +3973,18 @@ export class DocumentIndex<
3604
3973
  (x) => x === lastValueInOrder,
3605
3974
  );
3606
3975
  if (lateResults > 0) {
3607
- onMissedResults({ amount: lateResults });
3976
+ onLateResults({ amount: lateResults });
3608
3977
  }
3609
3978
  } else {
3610
- onMissedResults({ amount: pending.length });
3979
+ onLateResults({ amount: pending.length });
3611
3980
  }
3612
3981
  }
3613
3982
  }
3614
3983
  }
3615
- if (!pendingResultsReason) {
3616
- pendingResultsReason = "join";
3984
+ if (!pendingBatchReason) {
3985
+ pendingBatchReason = "join";
3617
3986
  }
3618
- signalUpdate();
3987
+ signalUpdate("join");
3619
3988
  },
3620
3989
  });
3621
3990
  const cleanupDefault = cleanup;
@@ -3662,7 +4031,7 @@ export class DocumentIndex<
3662
4031
  await fetchAtLeast(1);
3663
4032
  }
3664
4033
  } catch (error) {
3665
- logger.warn("Failed to refresh iterator pending state", error);
4034
+ warn("Failed to refresh iterator pending state", error);
3666
4035
  }
3667
4036
 
3668
4037
  let pendingCount = 0;
@@ -3679,7 +4048,7 @@ export class DocumentIndex<
3679
4048
  let batch = await next(100);
3680
4049
  c += batch.length;
3681
4050
  if (c > WARNING_WHEN_ITERATING_FOR_MORE_THAN) {
3682
- logger.warn(
4051
+ warn(
3683
4052
  "Iterating for more than " +
3684
4053
  WARNING_WHEN_ITERATING_FOR_MORE_THAN +
3685
4054
  " results",
@@ -3709,7 +4078,7 @@ export class DocumentIndex<
3709
4078
  const batch = await next(100);
3710
4079
  c += batch.length;
3711
4080
  if (c > WARNING_WHEN_ITERATING_FOR_MORE_THAN) {
3712
- logger.warn(
4081
+ warn(
3713
4082
  "Iterating for more than " +
3714
4083
  WARNING_WHEN_ITERATING_FOR_MORE_THAN +
3715
4084
  " results",