@peerbit/document 10.0.4 → 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 -2120
  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 +564 -196
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,
@@ -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
 
@@ -3058,119 +3497,53 @@ export class DocumentIndex<
3058
3497
  let fetchedFirstForRemote: Set<string> | undefined = undefined;
3059
3498
 
3060
3499
  let updateDeferred: ReturnType<typeof pDefer> | undefined;
3061
- 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
+ }
3062
3517
  updateDeferred?.resolve();
3063
3518
  };
3064
3519
  const _waitForUpdate = () =>
3065
3520
  updateDeferred ? updateDeferred.promise : Promise.resolve();
3066
3521
 
3067
3522
  // ---------------- Live updates wiring (sorted-only with optional filter) ----------------
3068
- function normalizeUpdatesOption(u?: UpdateOptions<T, I, Resolve>): {
3069
- mergePolicy?: {
3070
- merge?:
3071
- | {
3072
- filter?: (
3073
- evt: DocumentsChange<T, I>,
3074
- ) => MaybePromise<DocumentsChange<T, I> | void>;
3075
- }
3076
- | undefined;
3077
- };
3078
- push: boolean;
3079
- callbacks?: UpdateCallbacks<T, I, Resolve>;
3080
- } {
3081
- const identityFilter = (evt: DocumentsChange<T, I>) => evt;
3082
- const buildMergePolicy = (
3083
- merge: UpdateMergeStrategy<T, I, Resolve> | undefined,
3084
- defaultEnabled: boolean,
3085
- ) => {
3086
- const effective =
3087
- merge === undefined ? (defaultEnabled ? true : undefined) : merge;
3088
- if (effective === undefined || effective === false) {
3089
- return undefined;
3090
- }
3091
- if (effective === true) {
3092
- return {
3093
- merge: {
3094
- filter: identityFilter,
3095
- },
3096
- };
3097
- }
3098
- return {
3099
- merge: {
3100
- filter: effective.filter ?? identityFilter,
3101
- },
3102
- };
3103
- };
3104
-
3105
- if (u == null || u === false) {
3106
- return { push: false };
3107
- }
3108
-
3109
- if (u === true) {
3110
- return {
3111
- mergePolicy: buildMergePolicy(true, true),
3112
- push: false,
3113
- };
3114
- }
3115
-
3116
- if (typeof u === "string") {
3117
- if (u === "remote") {
3118
- return { push: true };
3119
- }
3120
- if (u === "local") {
3121
- return {
3122
- mergePolicy: buildMergePolicy(true, true),
3123
- push: false,
3124
- };
3125
- }
3126
- if (u === "all") {
3127
- return {
3128
- mergePolicy: buildMergePolicy(true, true),
3129
- push: true,
3130
- };
3131
- }
3132
- }
3133
-
3134
- if (typeof u === "object") {
3135
- const hasMergeProp = Object.prototype.hasOwnProperty.call(u, "merge");
3136
- const mergeValue = hasMergeProp ? u.merge : undefined;
3137
- return {
3138
- mergePolicy: buildMergePolicy(
3139
- mergeValue,
3140
- !hasMergeProp || mergeValue === undefined,
3141
- ),
3142
- push: Boolean(u.push),
3143
- callbacks: u,
3144
- };
3145
- }
3146
-
3147
- return { push: false };
3148
- }
3149
-
3150
3523
  const updateCallbacks = updateCallbacksRaw;
3151
- let pendingResultsReason:
3152
- | Extract<ResultBatchReason, "join" | "change">
3524
+ let pendingBatchReason:
3525
+ | Extract<UpdateReason, "join" | "change" | "push">
3153
3526
  | undefined;
3154
3527
  let hasDeliveredResults = false;
3155
3528
 
3156
- const emitOnResults = async (
3529
+ const emitOnBatch = async (
3157
3530
  batch: ValueTypeFromRequest<Resolve, T, I>[],
3158
- defaultReason: Extract<ResultBatchReason, "initial" | "next">,
3531
+ defaultReason: Extract<UpdateReason, "initial" | "manual">,
3159
3532
  ) => {
3160
- if (!updateCallbacks?.onResults || batch.length === 0) {
3533
+ if (!updateCallbacks?.onBatch || batch.length === 0) {
3161
3534
  return;
3162
3535
  }
3163
- let reason: ResultBatchReason;
3164
- if (pendingResultsReason) {
3165
- reason = pendingResultsReason;
3536
+ let reason: UpdateReason;
3537
+ if (pendingBatchReason) {
3538
+ reason = pendingBatchReason;
3166
3539
  } else if (!hasDeliveredResults) {
3167
3540
  reason = "initial";
3168
3541
  } else {
3169
3542
  reason = defaultReason;
3170
3543
  }
3171
- pendingResultsReason = undefined;
3544
+ pendingBatchReason = undefined;
3172
3545
  hasDeliveredResults = true;
3173
- await updateCallbacks.onResults(batch, { reason });
3546
+ await updateCallbacks.onBatch(batch, { reason });
3174
3547
  };
3175
3548
 
3176
3549
  // sorted-only mode: no per-queue handling
@@ -3191,18 +3564,19 @@ export class DocumentIndex<
3191
3564
  queryRequestCoerced.replicate = replicateFlag;
3192
3565
  const ttlSource =
3193
3566
  typeof remoteOptions === "object" &&
3194
- typeof remoteOptions?.wait === "object"
3195
- ? (remoteOptions.wait.timeout ?? DEFAULT_TIMEOUT)
3196
- : 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;
3197
3571
  queryRequestCoerced.keepAliveTtl = keepRemoteAlive
3198
3572
  ? BigInt(ttlSource)
3199
3573
  : undefined;
3200
- queryRequestCoerced.pushUpdates = pushUpdates ? true : undefined;
3574
+ queryRequestCoerced.pushUpdates = pushUpdates;
3201
3575
  queryRequestCoerced.mergeUpdates = mergePolicy?.merge ? true : undefined;
3202
3576
  }
3203
3577
 
3204
3578
  if (pushUpdates && this.prefetch?.accumulator) {
3205
- const targetPrefetchKey = idAgnosticQueryKey(queryRequestCoerced);
3579
+ const currentPrefetchKey = () => idAgnosticQueryKey(queryRequestCoerced);
3206
3580
  const mergePrefetchedResults = async (
3207
3581
  from: PublicSignKey,
3208
3582
  results: types.Results<types.ResultTypeFromRequest<R, T, I>>,
@@ -3244,14 +3618,19 @@ export class DocumentIndex<
3244
3618
  continue;
3245
3619
  }
3246
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
+ }
3247
3629
  buffer.push({
3248
3630
  value: result.value as types.ResultTypeFromRequest<R, T, I>,
3249
3631
  context: result.context,
3250
3632
  from,
3251
- indexed: await this.resolveIndexed<R>(
3252
- result,
3253
- results.results as types.ResultTypeFromRequest<R, T, I>[],
3254
- ),
3633
+ indexed,
3255
3634
  });
3256
3635
  } else {
3257
3636
  if (visited.has(indexKey) && !indexedPlaceholders?.has(indexKey)) {
@@ -3262,6 +3641,10 @@ export class DocumentIndex<
3262
3641
  result.indexed || result.value,
3263
3642
  result.context,
3264
3643
  );
3644
+ if (isLateResult(indexed)) {
3645
+ onLateResults?.({ amount: 1, peer: from });
3646
+ continue;
3647
+ }
3265
3648
  const placeholder = {
3266
3649
  value: result.value,
3267
3650
  context: result.context,
@@ -3275,7 +3658,9 @@ export class DocumentIndex<
3275
3658
 
3276
3659
  peerBufferMap.set(peerHash, {
3277
3660
  buffer,
3278
- kept: Number(results.kept),
3661
+ // Prefetched batches should not claim remote pending counts;
3662
+ // we'll collect more explicitly if needed.
3663
+ kept: 0,
3279
3664
  });
3280
3665
  };
3281
3666
 
@@ -3286,7 +3671,7 @@ export class DocumentIndex<
3286
3671
  if (!request) {
3287
3672
  return;
3288
3673
  }
3289
- if (idAgnosticQueryKey(request) !== targetPrefetchKey) {
3674
+ if (idAgnosticQueryKey(request) !== currentPrefetchKey()) {
3290
3675
  return;
3291
3676
  }
3292
3677
  try {
@@ -3318,12 +3703,12 @@ export class DocumentIndex<
3318
3703
  );
3319
3704
  }
3320
3705
 
3321
- if (!pendingResultsReason) {
3322
- pendingResultsReason = "change";
3706
+ if (!pendingBatchReason) {
3707
+ pendingBatchReason = "push";
3323
3708
  }
3324
- signalUpdate("prefetch-add");
3709
+ signalUpdate("push");
3325
3710
  } catch (error) {
3326
- logger.warn("Failed to merge prefetched results", error);
3711
+ warn("Failed to merge prefetched results", error);
3327
3712
  }
3328
3713
  };
3329
3714
 
@@ -3394,15 +3779,15 @@ export class DocumentIndex<
3394
3779
 
3395
3780
  const onChange = async (evt: CustomEvent<DocumentsChange<T, I>>) => {
3396
3781
  // Optional filter to mutate/suppress change events
3782
+ indexIteratorLogger.trace(
3783
+ "processing live update change event",
3784
+ evt.detail,
3785
+ );
3397
3786
  let filtered: DocumentsChange<T, I> | void = evt.detail;
3398
3787
  if (mergePolicy?.merge?.filter) {
3399
3788
  filtered = await mergePolicy.merge?.filter(evt.detail);
3400
3789
  }
3401
3790
  if (filtered) {
3402
- const changeForCallback: DocumentsChange<T, I> = {
3403
- added: [],
3404
- removed: [],
3405
- };
3406
3791
  let hasRelevantChange = false;
3407
3792
 
3408
3793
  // Remove entries that were deleted from all pending structures
@@ -3433,14 +3818,6 @@ export class DocumentIndex<
3433
3818
  }
3434
3819
  if (matchedRemovedIds.size > 0) {
3435
3820
  hasRelevantChange = true;
3436
- for (const removed of filtered.removed) {
3437
- const id = indexerTypes.toId(
3438
- this.indexByResolver(removed.__indexed),
3439
- ).primitive;
3440
- if (matchedRemovedIds.has(id)) {
3441
- changeForCallback.removed.push(removed);
3442
- }
3443
- }
3444
3821
  }
3445
3822
  }
3446
3823
 
@@ -3484,6 +3861,10 @@ export class DocumentIndex<
3484
3861
  continue;
3485
3862
  }
3486
3863
  }
3864
+ if (isLateResult(indexedCandidate)) {
3865
+ onLateResults?.({ amount: 1 });
3866
+ continue;
3867
+ }
3487
3868
  const id = indexerTypes.toId(
3488
3869
  this.indexByResolver(indexedCandidate),
3489
3870
  ).primitive;
@@ -3514,21 +3895,15 @@ export class DocumentIndex<
3514
3895
  ensureIndexedPlaceholders().set(id, placeholder);
3515
3896
  }
3516
3897
  hasRelevantChange = true;
3517
- changeForCallback.added.push(added);
3518
3898
  }
3519
3899
  }
3520
3900
 
3521
3901
  if (hasRelevantChange) {
3522
- if (!pendingResultsReason) {
3523
- pendingResultsReason = "change";
3524
- }
3525
- if (
3526
- changeForCallback.added.length > 0 ||
3527
- changeForCallback.removed.length > 0
3528
- ) {
3529
- updateCallbacks?.onChange?.(changeForCallback);
3530
- signalUpdate("change");
3902
+ runNotify("change");
3903
+ if (!pendingBatchReason) {
3904
+ pendingBatchReason = "change";
3531
3905
  }
3906
+ signalUpdate();
3532
3907
  }
3533
3908
  }
3534
3909
  signalUpdate();
@@ -3550,13 +3925,6 @@ export class DocumentIndex<
3550
3925
 
3551
3926
  updateDeferred = pDefer<void>();
3552
3927
 
3553
- // derive optional onMissedResults callback if provided
3554
- let onMissedResults =
3555
- typeof options?.remote?.wait === "object" &&
3556
- typeof options?.remote.onLateResults === "function"
3557
- ? options.remote.onLateResults
3558
- : undefined;
3559
-
3560
3928
  const waitForTime =
3561
3929
  typeof options.remote.wait === "object" && options.remote.wait.timeout;
3562
3930
 
@@ -3573,7 +3941,7 @@ export class DocumentIndex<
3573
3941
  }, waitForTime);
3574
3942
  ensureController().signal.addEventListener("abort", () => signalUpdate());
3575
3943
  fetchedFirstForRemote = new Set<string>();
3576
- joinListener = this.attachJoinListener({
3944
+ joinListener = this.createReplicatorJoinListener({
3577
3945
  signal: ensureController().signal,
3578
3946
  eager: options.remote.reach?.eager,
3579
3947
  onPeer: async (pk) => {
@@ -3588,7 +3956,7 @@ export class DocumentIndex<
3588
3956
  fetchedFirstForRemote,
3589
3957
  });
3590
3958
  await fetchPromise;
3591
- if (onMissedResults) {
3959
+ if (onLateResults) {
3592
3960
  const pending = peerBufferMap.get(hash)?.buffer;
3593
3961
  if (pending && pending.length > 0) {
3594
3962
  if (lastValueInOrder) {
@@ -3605,18 +3973,18 @@ export class DocumentIndex<
3605
3973
  (x) => x === lastValueInOrder,
3606
3974
  );
3607
3975
  if (lateResults > 0) {
3608
- onMissedResults({ amount: lateResults });
3976
+ onLateResults({ amount: lateResults });
3609
3977
  }
3610
3978
  } else {
3611
- onMissedResults({ amount: pending.length });
3979
+ onLateResults({ amount: pending.length });
3612
3980
  }
3613
3981
  }
3614
3982
  }
3615
3983
  }
3616
- if (!pendingResultsReason) {
3617
- pendingResultsReason = "join";
3984
+ if (!pendingBatchReason) {
3985
+ pendingBatchReason = "join";
3618
3986
  }
3619
- signalUpdate();
3987
+ signalUpdate("join");
3620
3988
  },
3621
3989
  });
3622
3990
  const cleanupDefault = cleanup;
@@ -3663,7 +4031,7 @@ export class DocumentIndex<
3663
4031
  await fetchAtLeast(1);
3664
4032
  }
3665
4033
  } catch (error) {
3666
- logger.warn("Failed to refresh iterator pending state", error);
4034
+ warn("Failed to refresh iterator pending state", error);
3667
4035
  }
3668
4036
 
3669
4037
  let pendingCount = 0;
@@ -3680,7 +4048,7 @@ export class DocumentIndex<
3680
4048
  let batch = await next(100);
3681
4049
  c += batch.length;
3682
4050
  if (c > WARNING_WHEN_ITERATING_FOR_MORE_THAN) {
3683
- logger.warn(
4051
+ warn(
3684
4052
  "Iterating for more than " +
3685
4053
  WARNING_WHEN_ITERATING_FOR_MORE_THAN +
3686
4054
  " results",
@@ -3710,7 +4078,7 @@ export class DocumentIndex<
3710
4078
  const batch = await next(100);
3711
4079
  c += batch.length;
3712
4080
  if (c > WARNING_WHEN_ITERATING_FOR_MORE_THAN) {
3713
- logger.warn(
4081
+ warn(
3714
4082
  "Iterating for more than " +
3715
4083
  WARNING_WHEN_ITERATING_FOR_MORE_THAN +
3716
4084
  " results",