@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.
- package/dist/benchmark/index.js +114 -59
- package/dist/benchmark/index.js.map +1 -1
- package/dist/benchmark/iterate-replicate-2.js +117 -63
- package/dist/benchmark/iterate-replicate-2.js.map +1 -1
- package/dist/benchmark/iterate-replicate.js +106 -56
- package/dist/benchmark/iterate-replicate.js.map +1 -1
- package/dist/benchmark/memory/child.js +114 -59
- package/dist/benchmark/memory/child.js.map +1 -1
- package/dist/benchmark/replication.js +106 -52
- package/dist/benchmark/replication.js.map +1 -1
- package/dist/src/domain.d.ts.map +1 -1
- package/dist/src/domain.js +1 -3
- package/dist/src/domain.js.map +1 -1
- package/dist/src/events.d.ts +1 -1
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/most-common-query-predictor.d.ts +3 -3
- package/dist/src/most-common-query-predictor.d.ts.map +1 -1
- package/dist/src/most-common-query-predictor.js.map +1 -1
- package/dist/src/operation.js +175 -81
- package/dist/src/operation.js.map +1 -1
- package/dist/src/prefetch.d.ts +2 -2
- package/dist/src/prefetch.d.ts.map +1 -1
- package/dist/src/prefetch.js.map +1 -1
- package/dist/src/program.d.ts +2 -2
- package/dist/src/program.d.ts.map +1 -1
- package/dist/src/program.js +550 -508
- package/dist/src/program.js.map +1 -1
- package/dist/src/resumable-iterator.d.ts.map +1 -1
- package/dist/src/resumable-iterator.js +44 -0
- package/dist/src/resumable-iterator.js.map +1 -1
- package/dist/src/search.d.ts +14 -10
- package/dist/src/search.d.ts.map +1 -1
- package/dist/src/search.js +2477 -2120
- package/dist/src/search.js.map +1 -1
- package/package.json +21 -19
- package/src/domain.ts +1 -3
- package/src/events.ts +1 -1
- package/src/index.ts +1 -0
- package/src/most-common-query-predictor.ts +19 -5
- package/src/prefetch.ts +12 -3
- package/src/program.ts +7 -5
- package/src/resumable-iterator.ts +44 -0
- 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
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
84
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
97
|
+
onBatch?: (
|
|
93
98
|
batch: RT[],
|
|
94
|
-
meta: { reason:
|
|
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<
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
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
|
|
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
|
-
.
|
|
1846
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
2943
|
+
kept: 0,
|
|
2514
2944
|
});
|
|
2515
2945
|
}
|
|
2516
2946
|
return;
|
|
2517
2947
|
}
|
|
2518
2948
|
|
|
2519
|
-
|
|
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:
|
|
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 ? "
|
|
3006
|
-
|
|
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
|
|
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
|
|
3152
|
-
| Extract<
|
|
3524
|
+
let pendingBatchReason:
|
|
3525
|
+
| Extract<UpdateReason, "join" | "change" | "push">
|
|
3153
3526
|
| undefined;
|
|
3154
3527
|
let hasDeliveredResults = false;
|
|
3155
3528
|
|
|
3156
|
-
const
|
|
3529
|
+
const emitOnBatch = async (
|
|
3157
3530
|
batch: ValueTypeFromRequest<Resolve, T, I>[],
|
|
3158
|
-
defaultReason: Extract<
|
|
3531
|
+
defaultReason: Extract<UpdateReason, "initial" | "manual">,
|
|
3159
3532
|
) => {
|
|
3160
|
-
if (!updateCallbacks?.
|
|
3533
|
+
if (!updateCallbacks?.onBatch || batch.length === 0) {
|
|
3161
3534
|
return;
|
|
3162
3535
|
}
|
|
3163
|
-
let reason:
|
|
3164
|
-
if (
|
|
3165
|
-
reason =
|
|
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
|
-
|
|
3544
|
+
pendingBatchReason = undefined;
|
|
3172
3545
|
hasDeliveredResults = true;
|
|
3173
|
-
await updateCallbacks.
|
|
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
|
-
|
|
3196
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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) !==
|
|
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 (!
|
|
3322
|
-
|
|
3706
|
+
if (!pendingBatchReason) {
|
|
3707
|
+
pendingBatchReason = "push";
|
|
3323
3708
|
}
|
|
3324
|
-
signalUpdate("
|
|
3709
|
+
signalUpdate("push");
|
|
3325
3710
|
} catch (error) {
|
|
3326
|
-
|
|
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
|
-
|
|
3523
|
-
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
3976
|
+
onLateResults({ amount: lateResults });
|
|
3609
3977
|
}
|
|
3610
3978
|
} else {
|
|
3611
|
-
|
|
3979
|
+
onLateResults({ amount: pending.length });
|
|
3612
3980
|
}
|
|
3613
3981
|
}
|
|
3614
3982
|
}
|
|
3615
3983
|
}
|
|
3616
|
-
if (!
|
|
3617
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4081
|
+
warn(
|
|
3714
4082
|
"Iterating for more than " +
|
|
3715
4083
|
WARNING_WHEN_ITERATING_FOR_MORE_THAN +
|
|
3716
4084
|
" results",
|