@peerbit/document 9.4.2 → 9.4.3-fb47029

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/package.json CHANGED
@@ -1,76 +1,77 @@
1
1
  {
2
- "name": "@peerbit/document",
3
- "version": "9.4.2",
4
- "description": "Document store implementation",
5
- "type": "module",
6
- "sideEffects": false,
7
- "types": "./dist/src/index.d.ts",
8
- "typesVersions": {
9
- "*": {
10
- "*": [
11
- "*",
12
- "dist/*",
13
- "dist/src/*",
14
- "dist/src/*/index"
15
- ],
16
- "src/*": [
17
- "*",
18
- "dist/*",
19
- "dist/src/*",
20
- "dist/src/*/index"
21
- ]
22
- }
23
- },
24
- "files": [
25
- "src",
26
- "dist",
27
- "!dist/e2e",
28
- "!dist/test",
29
- "!**/*.tsbuildinfo"
30
- ],
31
- "exports": {
32
- ".": {
33
- "types": "./dist/src/index.d.ts",
34
- "import": "./dist/src/index.js"
35
- }
36
- },
37
- "eslintConfig": {
38
- "extends": "peerbit",
39
- "parserOptions": {
40
- "project": true,
41
- "sourceType": "module"
42
- },
43
- "ignorePatterns": [
44
- "!.aegir.js",
45
- "test/ts-use",
46
- "*.d.ts"
47
- ]
48
- },
49
- "publishConfig": {
50
- "access": "public"
51
- },
52
- "scripts": {
53
- "clean": "aegir clean",
54
- "build": "aegir build --no-bundle",
55
- "test": "aegir test --target node",
56
- "lint": "aegir lint"
57
- },
58
- "author": "dao.xyz",
59
- "license": "MIT",
60
- "dependencies": {
61
- "@dao-xyz/borsh": "^5.2.3",
62
- "@peerbit/program": "5.2.7",
63
- "@peerbit/rpc": "5.2.9",
64
- "@peerbit/shared-log": "11.0.7",
65
- "@peerbit/indexer-interface": "^2.0.9",
66
- "@peerbit/indexer-simple": "^1.1.14",
67
- "@peerbit/indexer-sqlite3": "^1.2.18",
68
- "@peerbit/document-interface": "^2.0.28"
69
- },
70
- "devDependencies": {
71
- "@peerbit/test-utils": "2.1.42",
72
- "@peerbit/time": "2.1.0",
73
- "@types/pidusage": "^2.0.5",
74
- "pidusage": "^3.0.2"
75
- }
2
+ "name": "@peerbit/document",
3
+ "version": "9.4.3-fb47029",
4
+ "description": "Document store implementation",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "types": "./dist/src/index.d.ts",
8
+ "typesVersions": {
9
+ "*": {
10
+ "*": [
11
+ "*",
12
+ "dist/*",
13
+ "dist/src/*",
14
+ "dist/src/*/index"
15
+ ],
16
+ "src/*": [
17
+ "*",
18
+ "dist/*",
19
+ "dist/src/*",
20
+ "dist/src/*/index"
21
+ ]
22
+ }
23
+ },
24
+ "files": [
25
+ "src",
26
+ "dist",
27
+ "!dist/e2e",
28
+ "!dist/test",
29
+ "!**/*.tsbuildinfo"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/src/index.d.ts",
34
+ "import": "./dist/src/index.js"
35
+ }
36
+ },
37
+ "eslintConfig": {
38
+ "extends": "peerbit",
39
+ "parserOptions": {
40
+ "project": true,
41
+ "sourceType": "module"
42
+ },
43
+ "ignorePatterns": [
44
+ "!.aegir.js",
45
+ "test/ts-use",
46
+ "*.d.ts"
47
+ ]
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "scripts": {
53
+ "clean": "aegir clean",
54
+ "build": "aegir build --no-bundle",
55
+ "test": "aegir test --target node",
56
+ "lint": "aegir lint"
57
+ },
58
+ "author": "dao.xyz",
59
+ "license": "MIT",
60
+ "dependencies": {
61
+ "@dao-xyz/borsh": "^5.2.3",
62
+ "@peerbit/program": "5.2.7-fb47029",
63
+ "@peerbit/rpc": "5.2.9-fb47029",
64
+ "@peerbit/shared-log": "11.0.8-fb47029",
65
+ "@peerbit/indexer-interface": "2.0.9-fb47029",
66
+ "@peerbit/indexer-simple": "1.1.14-fb47029",
67
+ "@peerbit/indexer-sqlite3": "1.2.18-fb47029",
68
+ "@peerbit/document-interface": "2.0.28-fb47029",
69
+ "@peerbit/indexer-cache": "0.0.1-fb47029"
70
+ },
71
+ "devDependencies": {
72
+ "@peerbit/test-utils": "2.1.42-fb47029",
73
+ "@peerbit/time": "2.1.0-fb47029",
74
+ "@types/pidusage": "^2.0.5",
75
+ "pidusage": "^3.0.2"
76
+ }
76
77
  }
@@ -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
- @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
- }
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
 
@@ -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
- cacheSize?: number;
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
- cacheSize: options?.index?.cacheSize,
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
- return this.queues.get(id)?.iterator.pending();
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
  }