@peerbit/indexer-cache 0.0.1-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.
@@ -0,0 +1,50 @@
1
+ import { type IndexIterator, type IterateOptions } from "@peerbit/indexer-interface";
2
+ export type QueryCacheOptions = {
3
+ strategy: "auto";
4
+ maxTotalSize: number;
5
+ maxSize: number;
6
+ prefetchThreshold?: number;
7
+ keepAlive?: number;
8
+ } | {
9
+ strategy: "manual";
10
+ queries: {
11
+ iterate: IterateOptions;
12
+ maxSize: number;
13
+ }[];
14
+ };
15
+ type CachedIter<I extends Record<string, any>> = {
16
+ it: IndexIterator<I, undefined>;
17
+ size: number;
18
+ hits: number;
19
+ ts: number;
20
+ };
21
+ type Entry<I extends Record<string, any>> = {
22
+ cached: CachedIter<I>;
23
+ opts: IterateOptions;
24
+ };
25
+ export declare class IteratorCache<I extends Record<string, any>> {
26
+ private readonly factory;
27
+ private readonly map;
28
+ private readonly pendingJob;
29
+ private total;
30
+ private readonly cfg;
31
+ constructor(opts: QueryCacheOptions, factory: (o: IterateOptions, max: number) => IndexIterator<I, undefined>);
32
+ acquire(opts?: IterateOptions): IndexIterator<I, undefined>;
33
+ refresh(): Promise<void>;
34
+ clear(): Promise<void>;
35
+ private _ensureEntry;
36
+ private _startWarmup;
37
+ private _evictIfNeeded;
38
+ /** prune cold entries that exceweded keepAlive */
39
+ pruneStale(): void;
40
+ get _debugStats(): {
41
+ prefetchedRows: number;
42
+ cachedQueries: number;
43
+ activeQueries: string[];
44
+ pending: string[];
45
+ queryIsActive: (options?: IterateOptions) => boolean;
46
+ getCached: (key: string) => Entry<I> | undefined;
47
+ };
48
+ }
49
+ export {};
50
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,KAAK,aAAa,EAElB,KAAK,cAAc,EAGnB,MAAM,4BAA4B,CAAC;AAgBpC,MAAM,MAAM,iBAAiB,GAC1B;IACA,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CAClB,GACD;IACA,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE;QAAE,OAAO,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACvD,CAAC;AAiCL,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI;IAChD,EAAE,EAAE,aAAa,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACX,CAAC;AACF,KAAK,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI;IAC3C,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IACtB,IAAI,EAAE,cAAc,CAAC;CACrB,CAAC;AAGF,qBAAa,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAStD,OAAO,CAAC,QAAQ,CAAC,OAAO;IARzB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA+B;IACnD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoC;IAC/D,OAAO,CAAC,KAAK,CAAK;IAElB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAmB;gBAGtC,IAAI,EAAE,iBAAiB,EACN,OAAO,EAAE,CACzB,CAAC,EAAE,cAAc,EACjB,GAAG,EAAE,MAAM,KACP,aAAa,CAAC,CAAC,EAAE,SAAS,CAAC;IA2BjC,OAAO,CAAC,IAAI,GAAE,cAAmB,GAAG,aAAa,CAAC,CAAC,EAAE,SAAS,CAAC;IAgCzD,OAAO;IAMP,KAAK;IASX,OAAO,CAAC,YAAY;IAiBpB,OAAO,CAAC,YAAY;YAkBN,cAAc;IAgB5B,kDAAkD;IAClD,UAAU;IAYV,IAAI,WAAW;;;;;kCAQa,cAAc;yBAIvB,MAAM;MAIxB;CACD"}
@@ -0,0 +1,205 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ /* ---------------------------------------------------------------- imports */
11
+ import { field, serialize, vec } from "@dao-xyz/borsh";
12
+ import { toBase64 } from "@peerbit/crypto";
13
+ import { Query, Sort, } from "@peerbit/indexer-interface";
14
+ import { toQuery, toSort } from "@peerbit/indexer-interface";
15
+ /* -------------------------------------------------------- key helpers */
16
+ class KeyableIterate {
17
+ query;
18
+ sort;
19
+ constructor(query, sort) {
20
+ this.query = query;
21
+ this.sort = sort;
22
+ }
23
+ }
24
+ __decorate([
25
+ field({ type: vec(Query) }),
26
+ __metadata("design:type", Array)
27
+ ], KeyableIterate.prototype, "query", void 0);
28
+ __decorate([
29
+ field({ type: vec(Sort) }),
30
+ __metadata("design:type", Array)
31
+ ], KeyableIterate.prototype, "sort", void 0);
32
+ const iterateKey = (o) => toBase64(serialize(new KeyableIterate(toQuery(o?.query), toSort(o?.sort))));
33
+ /* ----------------------------------------------------- buffer wrapper */
34
+ function wrapWithBuffer(src, warm) {
35
+ const buf = [...warm];
36
+ return {
37
+ async next(n) {
38
+ const out = buf.splice(0, n);
39
+ if (out.length < n)
40
+ out.push(...(await src.next(n - out.length)));
41
+ return out;
42
+ },
43
+ async all() {
44
+ const rest = await src.all();
45
+ return [...buf.splice(0, buf.length), ...rest];
46
+ },
47
+ done: () => (buf.length ? false : (src.done?.() ?? false)),
48
+ pending: async () => (buf.length ? buf.length : (src.pending?.() ?? 0)),
49
+ close: () => src.close(),
50
+ };
51
+ }
52
+ /* ----------------------------------------------------------- main class */
53
+ export class IteratorCache {
54
+ factory;
55
+ map = new Map();
56
+ pendingJob = new Map();
57
+ total = 0;
58
+ cfg;
59
+ constructor(opts, factory) {
60
+ this.factory = factory;
61
+ if (opts.strategy === "manual") {
62
+ const wl = new Map(opts.queries.map((q) => [iterateKey(q.iterate), q.maxSize]));
63
+ this.cfg = {
64
+ shouldCache: (k) => wl.has(k),
65
+ entryMax: Math.max(...opts.queries.map((q) => q.maxSize)),
66
+ totalMax: Number.MAX_SAFE_INTEGER,
67
+ prefetchThreshold: 1,
68
+ keepAlive: Number.MAX_SAFE_INTEGER,
69
+ };
70
+ opts.queries.forEach((q) => this._ensureEntry(q.iterate));
71
+ }
72
+ else {
73
+ this.cfg = {
74
+ shouldCache: () => true,
75
+ entryMax: opts.maxSize,
76
+ totalMax: opts.maxTotalSize,
77
+ prefetchThreshold: opts.prefetchThreshold ?? 1,
78
+ keepAlive: opts.keepAlive ?? 1e4,
79
+ };
80
+ }
81
+ }
82
+ /* --------------------------------------------------------- public API */
83
+ acquire(opts = {}) {
84
+ if (this.cfg.keepAlive)
85
+ this.pruneStale();
86
+ const key = iterateKey(opts);
87
+ const entry = this._ensureEntry(opts);
88
+ /* ------------------------- warm path ------------------------- */
89
+ if (entry.cached.size > 0) {
90
+ const warmIt = entry.cached.it;
91
+ /* replace keeper & restart warm-up */
92
+ entry.cached = {
93
+ it: this.factory(opts, this.cfg.entryMax),
94
+ size: 0,
95
+ hits: entry.cached.hits + 1,
96
+ ts: Date.now(),
97
+ };
98
+ return warmIt; // caller gets prefetched rows
99
+ }
100
+ /* ------------------ cold / warming path ---------------------- */
101
+ entry.cached.hits++;
102
+ entry.cached.ts = Date.now();
103
+ if (entry.cached.hits >= this.cfg.prefetchThreshold &&
104
+ !this.pendingJob.has(key)) {
105
+ this._startWarmup(key, opts);
106
+ }
107
+ return this.factory(opts, this.cfg.entryMax); // caller gets cold iterator
108
+ }
109
+ async refresh() {
110
+ const keep = [...this.map.values()].map((e) => e.opts);
111
+ await this.clear();
112
+ for (const o of keep)
113
+ this.acquire(o);
114
+ }
115
+ async clear() {
116
+ await Promise.all([...this.map.values()].map((e) => e.cached.it.close()));
117
+ this.map.clear();
118
+ this.pendingJob.clear();
119
+ this.total = 0;
120
+ }
121
+ /* ------------------------------------------------------- internals */
122
+ _ensureEntry(opts) {
123
+ const key = iterateKey(opts);
124
+ let e = this.map.get(key);
125
+ if (e)
126
+ return e;
127
+ e = {
128
+ cached: {
129
+ it: this.factory(opts, this.cfg.entryMax),
130
+ size: 0,
131
+ hits: 0,
132
+ ts: Date.now(),
133
+ },
134
+ opts,
135
+ };
136
+ this.map.set(key, e);
137
+ return e;
138
+ }
139
+ _startWarmup(key, opt) {
140
+ if (this.pendingJob.has(key))
141
+ return;
142
+ const job = (async () => {
143
+ try {
144
+ const e = this.map.get(key);
145
+ if (!e || e.cached.size > 0)
146
+ return;
147
+ const warm = await e.cached.it.next(this.cfg.entryMax);
148
+ e.cached.it = wrapWithBuffer(e.cached.it, warm);
149
+ e.cached.size = warm.length;
150
+ this.total += warm.length;
151
+ await this._evictIfNeeded();
152
+ }
153
+ finally {
154
+ this.pendingJob.delete(key);
155
+ }
156
+ })();
157
+ this.pendingJob.set(key, job);
158
+ }
159
+ async _evictIfNeeded() {
160
+ if (this.total <= this.cfg.totalMax)
161
+ return;
162
+ const victims = [...this.map.entries()].sort(([, a], [, b]) => a.cached.hits === b.cached.hits
163
+ ? a.cached.ts - b.cached.ts
164
+ : a.cached.hits - b.cached.hits);
165
+ for (const [k, v] of victims) {
166
+ if (this.pendingJob.has(k))
167
+ continue;
168
+ await v.cached.it.close();
169
+ this.total -= v.cached.size;
170
+ this.map.delete(k);
171
+ if (this.total <= this.cfg.totalMax)
172
+ break;
173
+ }
174
+ }
175
+ /** prune cold entries that exceweded keepAlive */
176
+ pruneStale() {
177
+ const now = Date.now();
178
+ for (const [k, e] of this.map) {
179
+ // if cached size > 0 then it is "active" and should not be pruned
180
+ // cached.size === 0 means that the entry is "cold" and can be pruned
181
+ if (e.cached.size === 0 && now - e.cached.ts > this.cfg.keepAlive) {
182
+ this.map.delete(k);
183
+ }
184
+ }
185
+ }
186
+ /* ------------------------------------------------ diagnostics helper */
187
+ get _debugStats() {
188
+ return {
189
+ prefetchedRows: this.total,
190
+ cachedQueries: this.map.size,
191
+ activeQueries: [...this.map]
192
+ .filter(([, e]) => e.cached.size > 0)
193
+ .map(([k]) => k),
194
+ pending: [...this.pendingJob.keys()],
195
+ queryIsActive: (options) => {
196
+ const key = iterateKey(options);
197
+ return (this.map.get(key)?.cached.size ?? 0) > 0 || false;
198
+ },
199
+ getCached: (key) => {
200
+ return this.map.get(key);
201
+ },
202
+ };
203
+ }
204
+ }
205
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":";;;;;;;;;AAAA,8EAA8E;AAC9E,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAIN,KAAK,EACL,IAAI,GACJ,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAE7D,0EAA0E;AAC1E,MAAM,cAAc;IACU,KAAK,CAAU;IAChB,IAAI,CAAS;IACzC,YAAY,KAAc,EAAE,IAAY;QACvC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;CACD;AAN6B;IAA5B,KAAK,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;;6CAAgB;AAChB;IAA3B,KAAK,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;;4CAAc;AAM1C,MAAM,UAAU,GAAG,CAAC,CAAkB,EAAE,EAAE,CACzC,QAAQ,CAAC,SAAS,CAAC,IAAI,cAAc,CAAC,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAwB7E,0EAA0E;AAC1E,SAAS,cAAc,CACtB,GAAgC,EAChC,IAAuB;IAEvB,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IACtB,OAAO;QACN,KAAK,CAAC,IAAI,CAAC,CAAC;YACX,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAC7B,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAClE,OAAO,GAAG,CAAC;QACZ,CAAC;QACD,KAAK,CAAC,GAAG;YACR,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC;YAC7B,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;QAChD,CAAC;QACD,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,KAAK,CAAC,CAAC;QAC1D,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;QACvE,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE;KACxB,CAAC;AACH,CAAC;AAcD,4EAA4E;AAC5E,MAAM,OAAO,aAAa;IASP;IARD,GAAG,GAAG,IAAI,GAAG,EAAoB,CAAC;IAClC,UAAU,GAAG,IAAI,GAAG,EAAyB,CAAC;IACvD,KAAK,GAAG,CAAC,CAAC;IAED,GAAG,CAAmB;IAEvC,YACC,IAAuB,EACN,OAGe;QAHf,YAAO,GAAP,OAAO,CAGQ;QAEhC,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAChC,MAAM,EAAE,GAAG,IAAI,GAAG,CACjB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAC3D,CAAC;YACF,IAAI,CAAC,GAAG,GAAG;gBACV,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC7B,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;gBACzD,QAAQ,EAAE,MAAM,CAAC,gBAAgB;gBACjC,iBAAiB,EAAE,CAAC;gBACpB,SAAS,EAAE,MAAM,CAAC,gBAAgB;aAClC,CAAC;YACF,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QAC3D,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,GAAG,GAAG;gBACV,WAAW,EAAE,GAAG,EAAE,CAAC,IAAI;gBACvB,QAAQ,EAAE,IAAI,CAAC,OAAO;gBACtB,QAAQ,EAAE,IAAI,CAAC,YAAY;gBAC3B,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,IAAI,CAAC;gBAC9C,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,GAAG;aAChC,CAAC;QACH,CAAC;IACF,CAAC;IAED,0EAA0E;IAE1E,OAAO,CAAC,OAAuB,EAAE;QAChC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS;YAAE,IAAI,CAAC,UAAU,EAAE,CAAC;QAE1C,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;QAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QAEtC,mEAAmE;QACnE,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YAE/B,sCAAsC;YACtC,KAAK,CAAC,MAAM,GAAG;gBACd,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;gBACzC,IAAI,EAAE,CAAC;gBACP,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC;gBAC3B,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;aACd,CAAC;YACF,OAAO,MAAM,CAAC,CAAC,8BAA8B;QAC9C,CAAC;QAED,mEAAmE;QACnE,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,IACC,KAAK,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,iBAAiB;YAC/C,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,EACxB,CAAC;YACF,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC9B,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,4BAA4B;IAC3E,CAAC;IAED,KAAK,CAAC,OAAO;QACZ,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC,IAAI,IAAI;YAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC;IAED,KAAK,CAAC,KAAK;QACV,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAC1E,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QACjB,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACxB,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;IAChB,CAAC;IAED,uEAAuE;IAE/D,YAAY,CAAC,IAAoB;QACxC,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC;QAChB,CAAC,GAAG;YACH,MAAM,EAAE;gBACP,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;gBACzC,IAAI,EAAE,CAAC;gBACP,IAAI,EAAE,CAAC;gBACP,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;aACd;YACD,IAAI;SACJ,CAAC;QACF,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,CAAC,CAAC;IACV,CAAC;IAEO,YAAY,CAAC,GAAW,EAAE,GAAmB;QACpD,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO;QACrC,MAAM,GAAG,GAAG,CAAC,KAAK,IAAI,EAAE;YACvB,IAAI,CAAC;gBACJ,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAC5B,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC;oBAAE,OAAO;gBACpC,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBACvD,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;gBAChD,CAAC,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC;gBAC5B,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC;gBAC1B,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;YAC7B,CAAC;oBAAS,CAAC;gBACV,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7B,CAAC;QACF,CAAC,CAAC,EAAE,CAAC;QACL,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC/B,CAAC;IAEO,KAAK,CAAC,cAAc;QAC3B,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ;YAAE,OAAO;QAC5C,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAC7D,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI;YAC9B,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE;YAC3B,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAChC,CAAC;QACF,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;YAC9B,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;gBAAE,SAAS;YACrC,MAAM,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;YAC1B,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC;YAC5B,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YACnB,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ;gBAAE,MAAM;QAC5C,CAAC;IACF,CAAC;IAED,kDAAkD;IAClD,UAAU;QACT,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YAC/B,kEAAkE;YAClE,qEAAqE;YACrE,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,SAAU,EAAE,CAAC;gBACpE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YACpB,CAAC;QACF,CAAC;IACF,CAAC;IAED,yEAAyE;IACzE,IAAI,WAAW;QACd,OAAO;YACN,cAAc,EAAE,IAAI,CAAC,KAAK;YAC1B,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI;YAC5B,aAAa,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC;iBAC1B,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;iBACpC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;YACjB,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;YACpC,aAAa,EAAE,CAAC,OAAwB,EAAE,EAAE;gBAC3C,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;gBAChC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC;YAC3D,CAAC;YACD,SAAS,EAAE,CAAC,GAAW,EAAE,EAAE;gBAC1B,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC1B,CAAC;SACD,CAAC;IACH,CAAC;CACD"}
@@ -0,0 +1,29 @@
1
+ import type { CountOptions, DeleteOptions, Index, IndexEngineInitProperties, IndexIterator, IterateOptions, Shape, SumOptions } from "@peerbit/indexer-interface";
2
+ import { IteratorCache, type QueryCacheOptions } from "./cache.js";
3
+ export { type QueryCacheOptions };
4
+ export declare class CachedIndex<T extends Record<string, any>, Nested = unknown> implements Index<T, Nested> {
5
+ /** the real index implementation */
6
+ private readonly origin;
7
+ private _cache;
8
+ constructor(
9
+ /** the real index implementation */
10
+ origin: Index<T, Nested>, opts?: QueryCacheOptions);
11
+ init(props: IndexEngineInitProperties<T, Nested>): Index<T, Nested> | Promise<Index<T, Nested>>;
12
+ start(): void | Promise<void>;
13
+ stop(): Promise<void>;
14
+ drop(): Promise<void>;
15
+ get(id: any, o?: {
16
+ shape: Shape;
17
+ }): import("@peerbit/indexer-interface").IndexedResult<T> | Promise<import("@peerbit/indexer-interface").IndexedResult<T> | undefined> | undefined;
18
+ sum(opts: SumOptions): number | bigint | Promise<number | bigint>;
19
+ count(opts?: CountOptions): number | Promise<number>;
20
+ getSize(): number | Promise<number>;
21
+ put(value: T, id?: any): Promise<void>;
22
+ del(q: DeleteOptions): Promise<import("@peerbit/indexer-interface").IdKey[]>;
23
+ iterate<S extends Shape | undefined = undefined>(iter?: IterateOptions, options?: {
24
+ shape?: S;
25
+ reference?: boolean;
26
+ }): IndexIterator<T, S>;
27
+ get iteratorCache(): IteratorCache<T>;
28
+ }
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,YAAY,EACZ,aAAa,EACb,KAAK,EACL,yBAAyB,EACzB,aAAa,EACb,cAAc,EACd,KAAK,EACL,UAAU,EACV,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEnE,OAAO,EAAE,KAAK,iBAAiB,EAAE,CAAC;AAClC,qBAAa,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CACvE,YAAW,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC;IAK1B,oCAAoC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJxB,OAAO,CAAC,MAAM,CAAmB;;IAGhC,oCAAoC;IACnB,MAAM,EAAE,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,EACzC,IAAI,GAAE,iBAKL;IAWF,IAAI,CAAC,KAAK,EAAE,yBAAyB,CAAC,CAAC,EAAE,MAAM,CAAC;IAGhD,KAAK;IAGC,IAAI;IAIJ,IAAI;IAOV,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,KAAK,CAAA;KAAE;IAGjC,GAAG,CAAC,IAAI,EAAE,UAAU;IAGpB,KAAK,CAAC,IAAI,CAAC,EAAE,YAAY;IAGzB,OAAO;IAID,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,GAAG;IAKtB,GAAG,CAAC,CAAC,EAAE,aAAa;IAM1B,OAAO,CAAC,CAAC,SAAS,KAAK,GAAG,SAAS,GAAG,SAAS,EAC9C,IAAI,CAAC,EAAE,cAAc,EACrB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,CAAC,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAC1C,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC;IAMtB,IAAI,aAAa,qBAEhB;CACD"}
@@ -0,0 +1,65 @@
1
+ import { IteratorCache } from "./cache.js";
2
+ export {};
3
+ export class CachedIndex {
4
+ origin;
5
+ _cache;
6
+ constructor(
7
+ /** the real index implementation */
8
+ origin, opts = {
9
+ strategy: "auto",
10
+ maxSize: 50,
11
+ maxTotalSize: 150,
12
+ prefetchThreshold: 2,
13
+ }) {
14
+ this.origin = origin;
15
+ this._cache = new IteratorCache(opts, (iterate, maxSize) => this.origin.iterate(iterate, {
16
+ reference: true,
17
+ }));
18
+ }
19
+ /* -------------------------- normal Index life-cycle -------------------- */
20
+ init(props) {
21
+ return this.origin.init(props);
22
+ }
23
+ start() {
24
+ return this.origin.start?.();
25
+ }
26
+ async stop() {
27
+ await this._cache?.clear();
28
+ return this.origin.stop?.();
29
+ }
30
+ async drop() {
31
+ await this._cache?.clear();
32
+ return this.origin.drop();
33
+ }
34
+ /* --------------------- read operations (may use cache) ------------------ */
35
+ get(id, o) {
36
+ return this.origin.get(id, o);
37
+ }
38
+ sum(opts) {
39
+ return this.origin.sum(opts);
40
+ }
41
+ count(opts) {
42
+ return this.origin.count(opts);
43
+ }
44
+ getSize() {
45
+ return this.origin.getSize();
46
+ }
47
+ async put(value, id) {
48
+ await this.origin.put(value, id);
49
+ await this._cache.refresh();
50
+ }
51
+ async del(q) {
52
+ const res = await this.origin.del(q);
53
+ await this._cache.refresh();
54
+ return res;
55
+ }
56
+ iterate(iter, options) {
57
+ if (!this._cache || options?.reference === false)
58
+ return this.origin.iterate(iter, options);
59
+ return this._cache.acquire(iter);
60
+ }
61
+ get iteratorCache() {
62
+ return this._cache;
63
+ }
64
+ }
65
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,aAAa,EAA0B,MAAM,YAAY,CAAC;AAEnE,OAAO,EAA0B,CAAC;AAClC,MAAM,OAAO,WAAW;IAOL;IAJV,MAAM,CAAmB;IAEjC;IACC,oCAAoC;IACnB,MAAwB,EACzC,OAA0B;QACzB,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,EAAE;QACX,YAAY,EAAE,GAAG;QACjB,iBAAiB,EAAE,CAAC;KACpB;QANgB,WAAM,GAAN,MAAM,CAAkB;QAQzC,IAAI,CAAC,MAAM,GAAG,IAAI,aAAa,CAAI,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,CAC7D,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE;YAC5B,SAAS,EAAE,IAAI;SACf,CAAC,CACF,CAAC;IACH,CAAC;IAED,6EAA6E;IAE7E,IAAI,CAAC,KAA2C;QAC/C,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IACD,KAAK;QACJ,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;IAC9B,CAAC;IACD,KAAK,CAAC,IAAI;QACT,MAAM,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;IAC7B,CAAC;IACD,KAAK,CAAC,IAAI;QACT,MAAM,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED,8EAA8E;IAE9E,GAAG,CAAC,EAAO,EAAE,CAAoB;QAChC,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC/B,CAAC;IACD,GAAG,CAAC,IAAgB;QACnB,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IACD,KAAK,CAAC,IAAmB;QACxB,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IACD,OAAO;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAAQ,EAAE,EAAQ;QAC3B,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACjC,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,CAAgB;QACzB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QAC5B,OAAO,GAAG,CAAC;IACZ,CAAC;IAED,OAAO,CACN,IAAqB,EACrB,OAA4C;QAE5C,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,EAAE,SAAS,KAAK,KAAK;YAC/C,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAwB,CAAC;IACzD,CAAC;IAED,IAAI,aAAa;QAChB,OAAO,IAAI,CAAC,MAAM,CAAC;IACpB,CAAC;CACD"}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@peerbit/indexer-cache",
3
+ "version": "0.0.1-fb47029",
4
+ "description": "Indexer caching utility",
5
+ "sideEffects": false,
6
+ "type": "module",
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",
56
+ "lint": "aegir lint"
57
+ },
58
+ "author": "dao.xyz",
59
+ "license": "MIT",
60
+ "dependencies": {
61
+ "@dao-xyz/borsh": "^5.2.3",
62
+ "@peerbit/crypto": "2.3.8-fb47029",
63
+ "@peerbit/indexer-interface": "2.0.9-fb47029"
64
+ },
65
+ "devDependencies": {
66
+ "@peerbit/indexer-simple": "1.1.14-fb47029"
67
+ }
68
+ }
package/src/cache.ts ADDED
@@ -0,0 +1,249 @@
1
+ /* ---------------------------------------------------------------- imports */
2
+ import { field, serialize, vec } from "@dao-xyz/borsh";
3
+ import { toBase64 } from "@peerbit/crypto";
4
+ import {
5
+ type IndexIterator,
6
+ type IndexedResults,
7
+ type IterateOptions,
8
+ Query,
9
+ Sort,
10
+ } from "@peerbit/indexer-interface";
11
+ import { toQuery, toSort } from "@peerbit/indexer-interface";
12
+
13
+ /* -------------------------------------------------------- key helpers */
14
+ class KeyableIterate {
15
+ @field({ type: vec(Query) }) query: Query[];
16
+ @field({ type: vec(Sort) }) sort: Sort[];
17
+ constructor(query: Query[], sort: Sort[]) {
18
+ this.query = query;
19
+ this.sort = sort;
20
+ }
21
+ }
22
+ const iterateKey = (o?: IterateOptions) =>
23
+ toBase64(serialize(new KeyableIterate(toQuery(o?.query), toSort(o?.sort))));
24
+
25
+ /* ------------------------------------------------------ cache options */
26
+ export type QueryCacheOptions =
27
+ | {
28
+ strategy: "auto";
29
+ maxTotalSize: number;
30
+ maxSize: number;
31
+ prefetchThreshold?: number;
32
+ keepAlive?: number;
33
+ }
34
+ | {
35
+ strategy: "manual";
36
+ queries: { iterate: IterateOptions; maxSize: number }[];
37
+ };
38
+
39
+ type QueryCacheConfig = {
40
+ shouldCache: (k: string) => boolean;
41
+ entryMax: number;
42
+ totalMax: number;
43
+ prefetchThreshold: number;
44
+ keepAlive?: number; // ms before a COLD entry is pruned
45
+ };
46
+
47
+ /* ----------------------------------------------------- buffer wrapper */
48
+ function wrapWithBuffer<T extends Record<string, any>>(
49
+ src: IndexIterator<T, undefined>,
50
+ warm: IndexedResults<T>,
51
+ ): IndexIterator<T, undefined> {
52
+ const buf = [...warm];
53
+ return {
54
+ async next(n) {
55
+ const out = buf.splice(0, n);
56
+ if (out.length < n) out.push(...(await src.next(n - out.length)));
57
+ return out;
58
+ },
59
+ async all() {
60
+ const rest = await src.all();
61
+ return [...buf.splice(0, buf.length), ...rest];
62
+ },
63
+ done: () => (buf.length ? false : (src.done?.() ?? false)),
64
+ pending: async () => (buf.length ? buf.length : (src.pending?.() ?? 0)),
65
+ close: () => src.close(),
66
+ };
67
+ }
68
+
69
+ /* -------------------------------------------------------- data shapes */
70
+ type CachedIter<I extends Record<string, any>> = {
71
+ it: IndexIterator<I, undefined>;
72
+ size: number;
73
+ hits: number;
74
+ ts: number;
75
+ };
76
+ type Entry<I extends Record<string, any>> = {
77
+ cached: CachedIter<I>;
78
+ opts: IterateOptions;
79
+ };
80
+
81
+ /* ----------------------------------------------------------- main class */
82
+ export class IteratorCache<I extends Record<string, any>> {
83
+ private readonly map = new Map<string, Entry<I>>();
84
+ private readonly pendingJob = new Map<string, Promise<void>>();
85
+ private total = 0;
86
+
87
+ private readonly cfg: QueryCacheConfig;
88
+
89
+ constructor(
90
+ opts: QueryCacheOptions,
91
+ private readonly factory: (
92
+ o: IterateOptions,
93
+ max: number,
94
+ ) => IndexIterator<I, undefined>,
95
+ ) {
96
+ if (opts.strategy === "manual") {
97
+ const wl = new Map(
98
+ opts.queries.map((q) => [iterateKey(q.iterate), q.maxSize]),
99
+ );
100
+ this.cfg = {
101
+ shouldCache: (k) => wl.has(k),
102
+ entryMax: Math.max(...opts.queries.map((q) => q.maxSize)),
103
+ totalMax: Number.MAX_SAFE_INTEGER,
104
+ prefetchThreshold: 1,
105
+ keepAlive: Number.MAX_SAFE_INTEGER,
106
+ };
107
+ opts.queries.forEach((q) => this._ensureEntry(q.iterate));
108
+ } else {
109
+ this.cfg = {
110
+ shouldCache: () => true,
111
+ entryMax: opts.maxSize,
112
+ totalMax: opts.maxTotalSize,
113
+ prefetchThreshold: opts.prefetchThreshold ?? 1,
114
+ keepAlive: opts.keepAlive ?? 1e4,
115
+ };
116
+ }
117
+ }
118
+
119
+ /* --------------------------------------------------------- public API */
120
+
121
+ acquire(opts: IterateOptions = {}): IndexIterator<I, undefined> {
122
+ if (this.cfg.keepAlive) this.pruneStale();
123
+
124
+ const key = iterateKey(opts);
125
+ const entry = this._ensureEntry(opts);
126
+
127
+ /* ------------------------- warm path ------------------------- */
128
+ if (entry.cached.size > 0) {
129
+ const warmIt = entry.cached.it;
130
+
131
+ /* replace keeper & restart warm-up */
132
+ entry.cached = {
133
+ it: this.factory(opts, this.cfg.entryMax),
134
+ size: 0,
135
+ hits: entry.cached.hits + 1,
136
+ ts: Date.now(),
137
+ };
138
+ return warmIt; // caller gets prefetched rows
139
+ }
140
+
141
+ /* ------------------ cold / warming path ---------------------- */
142
+ entry.cached.hits++;
143
+ entry.cached.ts = Date.now();
144
+ if (
145
+ entry.cached.hits >= this.cfg.prefetchThreshold &&
146
+ !this.pendingJob.has(key)
147
+ ) {
148
+ this._startWarmup(key, opts);
149
+ }
150
+ return this.factory(opts, this.cfg.entryMax); // caller gets cold iterator
151
+ }
152
+
153
+ async refresh() {
154
+ const keep = [...this.map.values()].map((e) => e.opts);
155
+ await this.clear();
156
+ for (const o of keep) this.acquire(o);
157
+ }
158
+
159
+ async clear() {
160
+ await Promise.all([...this.map.values()].map((e) => e.cached.it.close()));
161
+ this.map.clear();
162
+ this.pendingJob.clear();
163
+ this.total = 0;
164
+ }
165
+
166
+ /* ------------------------------------------------------- internals */
167
+
168
+ private _ensureEntry(opts: IterateOptions): Entry<I> {
169
+ const key = iterateKey(opts);
170
+ let e = this.map.get(key);
171
+ if (e) return e;
172
+ e = {
173
+ cached: {
174
+ it: this.factory(opts, this.cfg.entryMax),
175
+ size: 0,
176
+ hits: 0,
177
+ ts: Date.now(),
178
+ },
179
+ opts,
180
+ };
181
+ this.map.set(key, e);
182
+ return e;
183
+ }
184
+
185
+ private _startWarmup(key: string, opt: IterateOptions) {
186
+ if (this.pendingJob.has(key)) return;
187
+ const job = (async () => {
188
+ try {
189
+ const e = this.map.get(key);
190
+ if (!e || e.cached.size > 0) return;
191
+ const warm = await e.cached.it.next(this.cfg.entryMax);
192
+ e.cached.it = wrapWithBuffer(e.cached.it, warm);
193
+ e.cached.size = warm.length;
194
+ this.total += warm.length;
195
+ await this._evictIfNeeded();
196
+ } finally {
197
+ this.pendingJob.delete(key);
198
+ }
199
+ })();
200
+ this.pendingJob.set(key, job);
201
+ }
202
+
203
+ private async _evictIfNeeded() {
204
+ if (this.total <= this.cfg.totalMax) return;
205
+ const victims = [...this.map.entries()].sort(([, a], [, b]) =>
206
+ a.cached.hits === b.cached.hits
207
+ ? a.cached.ts - b.cached.ts
208
+ : a.cached.hits - b.cached.hits,
209
+ );
210
+ for (const [k, v] of victims) {
211
+ if (this.pendingJob.has(k)) continue;
212
+ await v.cached.it.close();
213
+ this.total -= v.cached.size;
214
+ this.map.delete(k);
215
+ if (this.total <= this.cfg.totalMax) break;
216
+ }
217
+ }
218
+
219
+ /** prune cold entries that exceweded keepAlive */
220
+ pruneStale() {
221
+ const now = Date.now();
222
+ for (const [k, e] of this.map) {
223
+ // if cached size > 0 then it is "active" and should not be pruned
224
+ // cached.size === 0 means that the entry is "cold" and can be pruned
225
+ if (e.cached.size === 0 && now - e.cached.ts > this.cfg.keepAlive!) {
226
+ this.map.delete(k);
227
+ }
228
+ }
229
+ }
230
+
231
+ /* ------------------------------------------------ diagnostics helper */
232
+ get _debugStats() {
233
+ return {
234
+ prefetchedRows: this.total,
235
+ cachedQueries: this.map.size,
236
+ activeQueries: [...this.map]
237
+ .filter(([, e]) => e.cached.size > 0)
238
+ .map(([k]) => k),
239
+ pending: [...this.pendingJob.keys()],
240
+ queryIsActive: (options?: IterateOptions) => {
241
+ const key = iterateKey(options);
242
+ return (this.map.get(key)?.cached.size ?? 0) > 0 || false;
243
+ },
244
+ getCached: (key: string) => {
245
+ return this.map.get(key);
246
+ },
247
+ };
248
+ }
249
+ }
package/src/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ /* ---------------------------------------------------------------- imports */
2
+ import type {
3
+ CountOptions,
4
+ DeleteOptions,
5
+ Index,
6
+ IndexEngineInitProperties,
7
+ IndexIterator,
8
+ IterateOptions,
9
+ Shape,
10
+ SumOptions,
11
+ } from "@peerbit/indexer-interface";
12
+ import { IteratorCache, type QueryCacheOptions } from "./cache.js";
13
+
14
+ export { type QueryCacheOptions };
15
+ export class CachedIndex<T extends Record<string, any>, Nested = unknown>
16
+ implements Index<T, Nested>
17
+ {
18
+ private _cache: IteratorCache<T>;
19
+
20
+ constructor(
21
+ /** the real index implementation */
22
+ private readonly origin: Index<T, Nested>,
23
+ opts: QueryCacheOptions = {
24
+ strategy: "auto",
25
+ maxSize: 50,
26
+ maxTotalSize: 150,
27
+ prefetchThreshold: 2,
28
+ }, // default options,
29
+ ) {
30
+ this._cache = new IteratorCache<T>(opts, (iterate, maxSize) =>
31
+ this.origin.iterate(iterate, {
32
+ reference: true,
33
+ }),
34
+ );
35
+ }
36
+
37
+ /* -------------------------- normal Index life-cycle -------------------- */
38
+
39
+ init(props: IndexEngineInitProperties<T, Nested>) {
40
+ return this.origin.init(props);
41
+ }
42
+ start() {
43
+ return this.origin.start?.();
44
+ }
45
+ async stop() {
46
+ await this._cache?.clear();
47
+ return this.origin.stop?.();
48
+ }
49
+ async drop() {
50
+ await this._cache?.clear();
51
+ return this.origin.drop();
52
+ }
53
+
54
+ /* --------------------- read operations (may use cache) ------------------ */
55
+
56
+ get(id: any, o?: { shape: Shape }) {
57
+ return this.origin.get(id, o);
58
+ }
59
+ sum(opts: SumOptions) {
60
+ return this.origin.sum(opts);
61
+ }
62
+ count(opts?: CountOptions) {
63
+ return this.origin.count(opts);
64
+ }
65
+ getSize() {
66
+ return this.origin.getSize();
67
+ }
68
+
69
+ async put(value: T, id?: any) {
70
+ await this.origin.put(value, id);
71
+ await this._cache.refresh();
72
+ }
73
+
74
+ async del(q: DeleteOptions) {
75
+ const res = await this.origin.del(q);
76
+ await this._cache.refresh();
77
+ return res;
78
+ }
79
+
80
+ iterate<S extends Shape | undefined = undefined>(
81
+ iter?: IterateOptions,
82
+ options?: { shape?: S; reference?: boolean },
83
+ ): IndexIterator<T, S> {
84
+ if (!this._cache || options?.reference === false)
85
+ return this.origin.iterate(iter, options);
86
+ return this._cache.acquire(iter) as IndexIterator<T, S>;
87
+ }
88
+
89
+ get iteratorCache() {
90
+ return this._cache;
91
+ }
92
+ }