@peerbit/document 9.4.3 → 9.5.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/src/most-common-query-predictor.d.ts +38 -0
- package/dist/src/most-common-query-predictor.d.ts.map +1 -0
- package/dist/src/most-common-query-predictor.js +115 -0
- package/dist/src/most-common-query-predictor.js.map +1 -0
- package/dist/src/prefetch.d.ts +22 -0
- package/dist/src/prefetch.d.ts.map +1 -0
- package/dist/src/prefetch.js +47 -0
- package/dist/src/prefetch.js.map +1 -0
- package/dist/src/program.d.ts +7 -2
- package/dist/src/program.d.ts.map +1 -1
- package/dist/src/program.js +2 -1
- package/dist/src/program.js.map +1 -1
- package/dist/src/resumable-iterator.d.ts +1 -1
- package/dist/src/resumable-iterator.d.ts.map +1 -1
- package/dist/src/resumable-iterator.js +10 -2
- package/dist/src/resumable-iterator.js.map +1 -1
- package/dist/src/search.d.ts +24 -3
- package/dist/src/search.d.ts.map +1 -1
- package/dist/src/search.js +209 -46
- package/dist/src/search.js.map +1 -1
- package/package.json +8 -7
- package/src/most-common-query-predictor.ts +161 -0
- package/src/operation.ts +9 -9
- package/src/prefetch.ts +80 -0
- package/src/program.ts +9 -2
- package/src/resumable-iterator.ts +10 -2
- package/src/search.ts +343 -68
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { type AbstractType, deserialize, serialize } from "@dao-xyz/borsh";
|
|
2
|
+
import { type PublicSignKey, randomBytes, toBase64 } from "@peerbit/crypto";
|
|
3
|
+
import type * as types from "@peerbit/document-interface";
|
|
4
|
+
|
|
5
|
+
/* ───────────────────── helpers ───────────────────── */
|
|
6
|
+
|
|
7
|
+
const nullifyQuery = (
|
|
8
|
+
query: types.SearchRequest | types.SearchRequestIndexed,
|
|
9
|
+
) => {
|
|
10
|
+
const cloned = deserialize(serialize(query), query.constructor) as
|
|
11
|
+
| types.SearchRequest
|
|
12
|
+
| types.SearchRequestIndexed;
|
|
13
|
+
cloned.id = new Uint8Array(32);
|
|
14
|
+
return cloned;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const idAgnosticQueryKey = (
|
|
18
|
+
query: types.SearchRequest | types.SearchRequestIndexed,
|
|
19
|
+
) => toBase64(serialize(nullifyQuery(query)));
|
|
20
|
+
|
|
21
|
+
/* ───────────────────── predictor ───────────────────── */
|
|
22
|
+
|
|
23
|
+
export interface QueryPredictor {
|
|
24
|
+
onRequest: (
|
|
25
|
+
request: types.SearchRequest | types.SearchRequestIndexed,
|
|
26
|
+
ctx: { from: PublicSignKey },
|
|
27
|
+
) => { ignore: boolean };
|
|
28
|
+
|
|
29
|
+
predictedQuery: (
|
|
30
|
+
from: PublicSignKey,
|
|
31
|
+
) => types.SearchRequest | types.SearchRequestIndexed | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface QueryStats {
|
|
35
|
+
count: number;
|
|
36
|
+
lastSeen: number;
|
|
37
|
+
queryBytes: Uint8Array;
|
|
38
|
+
queryClazz: AbstractType<types.SearchRequest | types.SearchRequestIndexed>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Learns the most common recent queries and predicts the most frequent one.
|
|
43
|
+
* If we just pre-empted a peer with some query, the *first* matching request
|
|
44
|
+
* that arrives from that peer within `ignoreWindow` ms is ignored.
|
|
45
|
+
*/
|
|
46
|
+
export default class MostCommonQueryPredictor implements QueryPredictor {
|
|
47
|
+
private readonly queries = new Map<string, QueryStats>();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* predicted:
|
|
51
|
+
* requestKey → Map<peerHash, timestamp>
|
|
52
|
+
*/
|
|
53
|
+
private readonly predicted = new Map<string, Map<string, number>>();
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
private readonly threshold: number,
|
|
57
|
+
private readonly ttl = 10 * 60 * 1000, // 10 min
|
|
58
|
+
private readonly ignoreWindow = 5_000, // 5 s
|
|
59
|
+
) {}
|
|
60
|
+
|
|
61
|
+
/* ───────── housekeeping ───────── */
|
|
62
|
+
private cleanupQueries(now: number) {
|
|
63
|
+
for (const [key, stats] of this.queries) {
|
|
64
|
+
if (now - stats.lastSeen > this.ttl) {
|
|
65
|
+
this.queries.delete(key);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private cleanupPredictions(now: number) {
|
|
71
|
+
for (const [key, peerMap] of this.predicted) {
|
|
72
|
+
for (const [peer, ts] of peerMap) {
|
|
73
|
+
if (now - ts > this.ignoreWindow) {
|
|
74
|
+
peerMap.delete(peer);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (peerMap.size === 0) {
|
|
78
|
+
this.predicted.delete(key);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ───────── public API ───────── */
|
|
84
|
+
|
|
85
|
+
onRequest(
|
|
86
|
+
request: types.SearchRequest | types.SearchRequestIndexed,
|
|
87
|
+
{ from }: { from: PublicSignKey },
|
|
88
|
+
): { ignore: boolean } {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const peerHash = from.hashcode();
|
|
91
|
+
const key = idAgnosticQueryKey(request);
|
|
92
|
+
|
|
93
|
+
/* — 1. Ignore if this (key, peer) pair was just predicted — */
|
|
94
|
+
const peerMap = this.predicted.get(key);
|
|
95
|
+
const ts = peerMap?.get(peerHash);
|
|
96
|
+
let ignore = false;
|
|
97
|
+
if (ts !== undefined && now - ts <= this.ignoreWindow) {
|
|
98
|
+
peerMap!.delete(peerHash); // one-shot
|
|
99
|
+
if (peerMap!.size === 0) {
|
|
100
|
+
this.predicted.delete(key);
|
|
101
|
+
}
|
|
102
|
+
ignore = true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* — 2. Learn from the request — */
|
|
106
|
+
const stats = this.queries.get(key);
|
|
107
|
+
if (stats) {
|
|
108
|
+
stats.count += 1;
|
|
109
|
+
stats.lastSeen = now;
|
|
110
|
+
} else {
|
|
111
|
+
this.queries.set(key, {
|
|
112
|
+
queryBytes: serialize(request),
|
|
113
|
+
queryClazz: request.constructor,
|
|
114
|
+
count: 1,
|
|
115
|
+
lastSeen: now,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* — 3. Maintenance — */
|
|
120
|
+
this.cleanupQueries(now);
|
|
121
|
+
this.cleanupPredictions(now);
|
|
122
|
+
|
|
123
|
+
return { ignore };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
predictedQuery(
|
|
127
|
+
from: PublicSignKey,
|
|
128
|
+
): types.SearchRequest | types.SearchRequestIndexed | undefined {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
this.cleanupQueries(now);
|
|
131
|
+
this.cleanupPredictions(now);
|
|
132
|
+
|
|
133
|
+
/* pick the most frequent query meeting the threshold */
|
|
134
|
+
let winnerKey: string | undefined;
|
|
135
|
+
let winnerCount = 0;
|
|
136
|
+
for (const [key, { count }] of this.queries) {
|
|
137
|
+
if (count > winnerCount) {
|
|
138
|
+
winnerKey = key;
|
|
139
|
+
winnerCount = count;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!winnerKey || winnerCount < this.threshold) return undefined;
|
|
143
|
+
|
|
144
|
+
const winner = this.queries.get(winnerKey)!;
|
|
145
|
+
const cloned = deserialize(winner.queryBytes, winner.queryClazz) as
|
|
146
|
+
| types.SearchRequest
|
|
147
|
+
| types.SearchRequestIndexed;
|
|
148
|
+
cloned.id = randomBytes(32);
|
|
149
|
+
|
|
150
|
+
/* remember that we pre-empted `from` with this query */
|
|
151
|
+
const peerHash = from.hashcode();
|
|
152
|
+
let peerMap = this.predicted.get(winnerKey);
|
|
153
|
+
if (!peerMap) {
|
|
154
|
+
peerMap = new Map<string, number>();
|
|
155
|
+
this.predicted.set(winnerKey, peerMap);
|
|
156
|
+
}
|
|
157
|
+
peerMap.set(peerHash, now);
|
|
158
|
+
|
|
159
|
+
return cloned;
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/operation.ts
CHANGED
|
@@ -26,15 +26,15 @@ export class PutWithKeyOperation extends Operation {
|
|
|
26
26
|
// @deprecated
|
|
27
27
|
/* @variant(1)
|
|
28
28
|
export class PutAllOperation<T> extends Operation<T> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
@field({ type: vec(PutOperation) })
|
|
30
|
+
docs: PutOperation<T>[];
|
|
31
|
+
|
|
32
|
+
constructor(props?: { docs: PutOperation<T>[] }) {
|
|
33
|
+
super();
|
|
34
|
+
if (props) {
|
|
35
|
+
this.docs = props.docs;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
38
|
}
|
|
39
39
|
*/
|
|
40
40
|
|
package/src/prefetch.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { TypedEventEmitter } from "@libp2p/interface";
|
|
2
|
+
import { Cache } from "@peerbit/cache";
|
|
3
|
+
import type * as types from "@peerbit/document-interface";
|
|
4
|
+
import type { RPCResponse } from "@peerbit/rpc/dist/src";
|
|
5
|
+
import { idAgnosticQueryKey } from "./most-common-query-predictor";
|
|
6
|
+
|
|
7
|
+
// --- typed helper ---------------------------------------------------------
|
|
8
|
+
type AddEvent = {
|
|
9
|
+
consumable: RPCResponse<types.PredictedSearchRequest<any>>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const prefetchKey = (
|
|
13
|
+
request: types.SearchRequest | types.SearchRequestIndexed,
|
|
14
|
+
keyHash: string,
|
|
15
|
+
) => `${idAgnosticQueryKey(request)} - ${keyHash}`;
|
|
16
|
+
|
|
17
|
+
// --------------------------------------------------------------------------
|
|
18
|
+
export class Prefetch extends TypedEventEmitter<{
|
|
19
|
+
add: CustomEvent<AddEvent>;
|
|
20
|
+
}> {
|
|
21
|
+
constructor(
|
|
22
|
+
private prefetch: Cache<
|
|
23
|
+
RPCResponse<types.PredictedSearchRequest<any>>
|
|
24
|
+
> = new Cache({
|
|
25
|
+
max: 100,
|
|
26
|
+
ttl: 1e4,
|
|
27
|
+
}),
|
|
28
|
+
private searchIdTranslationMap: Map<
|
|
29
|
+
string,
|
|
30
|
+
Map<string, Uint8Array>
|
|
31
|
+
> = new Map(),
|
|
32
|
+
) {
|
|
33
|
+
super();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Store the prediction **and** notify listeners */
|
|
37
|
+
public add(
|
|
38
|
+
request: RPCResponse<types.PredictedSearchRequest<any>>,
|
|
39
|
+
keyHash: string,
|
|
40
|
+
): void {
|
|
41
|
+
const key = prefetchKey(request.response.request, keyHash);
|
|
42
|
+
this.prefetch.add(key, request);
|
|
43
|
+
this.dispatchEvent(
|
|
44
|
+
new CustomEvent("add", { detail: { consumable: request } }),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public consume(
|
|
49
|
+
request: types.SearchRequest | types.SearchRequestIndexed,
|
|
50
|
+
keyHash: string,
|
|
51
|
+
): RPCResponse<types.PredictedSearchRequest<any>> | undefined {
|
|
52
|
+
const key = prefetchKey(request, keyHash);
|
|
53
|
+
const pre = this.prefetch.get(key);
|
|
54
|
+
if (!pre) return;
|
|
55
|
+
|
|
56
|
+
this.prefetch.del(key);
|
|
57
|
+
|
|
58
|
+
let peerMap = this.searchIdTranslationMap.get(request.idString);
|
|
59
|
+
if (!peerMap) {
|
|
60
|
+
peerMap = new Map();
|
|
61
|
+
this.searchIdTranslationMap.set(request.idString, peerMap);
|
|
62
|
+
}
|
|
63
|
+
peerMap.set(keyHash, pre.response.request.id);
|
|
64
|
+
return pre;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
clear(request: types.SearchRequest | types.SearchRequestIndexed) {
|
|
68
|
+
this.searchIdTranslationMap.delete(request.idString);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getTranslationMap(
|
|
72
|
+
request: types.SearchRequest | types.SearchRequestIndexed,
|
|
73
|
+
): Map<string, Uint8Array> | undefined {
|
|
74
|
+
return this.searchIdTranslationMap.get(request.idString);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get size() {
|
|
78
|
+
return this.prefetch.size;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/program.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@dao-xyz/borsh";
|
|
8
8
|
import { AccessError } from "@peerbit/crypto";
|
|
9
9
|
import { ResultIndexedValue } from "@peerbit/document-interface";
|
|
10
|
+
import type { QueryCacheOptions } from "@peerbit/indexer-cache";
|
|
10
11
|
import * as indexerTypes from "@peerbit/indexer-interface";
|
|
11
12
|
import {
|
|
12
13
|
type Change,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
type CanRead,
|
|
40
41
|
type CanSearch,
|
|
41
42
|
DocumentIndex,
|
|
43
|
+
type PrefetchOptions,
|
|
42
44
|
type TransformOptions,
|
|
43
45
|
type WithContext,
|
|
44
46
|
coerceWithContext,
|
|
@@ -95,7 +97,11 @@ export type SetupOptions<
|
|
|
95
97
|
canSearch?: CanSearch;
|
|
96
98
|
canRead?: CanRead<I>;
|
|
97
99
|
idProperty?: string | string[];
|
|
98
|
-
|
|
100
|
+
cache?: {
|
|
101
|
+
resolver?: number;
|
|
102
|
+
query?: QueryCacheOptions;
|
|
103
|
+
};
|
|
104
|
+
prefetch?: boolean | Partial<PrefetchOptions>;
|
|
99
105
|
} & TransformOptions<T, I>;
|
|
100
106
|
log?: {
|
|
101
107
|
trim?: TrimOptions;
|
|
@@ -192,7 +198,7 @@ export class Documents<
|
|
|
192
198
|
transform: options.index,
|
|
193
199
|
indexBy: idProperty,
|
|
194
200
|
compatibility: options.compatibility,
|
|
195
|
-
|
|
201
|
+
cache: options?.index?.cache,
|
|
196
202
|
replicate: async (query, results) => {
|
|
197
203
|
// here we arrive for all the results we want to persist.
|
|
198
204
|
|
|
@@ -210,6 +216,7 @@ export class Documents<
|
|
|
210
216
|
},
|
|
211
217
|
dbType: this.constructor,
|
|
212
218
|
maybeOpen: this.maybeSubprogramOpen.bind(this),
|
|
219
|
+
prefetch: options.index?.prefetch,
|
|
213
220
|
});
|
|
214
221
|
|
|
215
222
|
// document v6 and below need log compatibility of v8 or below
|
|
@@ -62,7 +62,15 @@ export class ResumableIterators<T extends Record<string, any>> {
|
|
|
62
62
|
this.queues.del(id);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
getPending(id: string) {
|
|
66
|
-
|
|
65
|
+
async getPending(id: string) {
|
|
66
|
+
let iterator = this.queues.get(id);
|
|
67
|
+
if (!iterator) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const pending = await iterator.iterator.pending();
|
|
71
|
+
if (pending === 0 && iterator.iterator.done()) {
|
|
72
|
+
this.clear(id);
|
|
73
|
+
}
|
|
74
|
+
return pending;
|
|
67
75
|
}
|
|
68
76
|
}
|