@fjell/cache 4.6.22 → 4.7.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/CACHE_IMPLEMENTATIONS.md +198 -0
- package/CONFIGURATION_GUIDE.md +167 -0
- package/CRITICAL_FIXES.md +68 -0
- package/README.md +506 -2
- package/debug_test2.js +0 -0
- package/debug_test3.js +0 -0
- package/dist/Cache.d.ts +4 -1
- package/dist/CacheContext.d.ts +27 -0
- package/dist/CacheMap.d.ts +89 -14
- package/dist/Instance.d.ts +4 -2
- package/dist/InstanceFactory.d.ts +3 -2
- package/dist/Operations.d.ts +2 -1
- package/dist/Options.d.ts +100 -0
- package/dist/browser/AsyncIndexDBCacheMap.d.ts +38 -0
- package/dist/browser/IndexDBCacheMap.d.ts +54 -0
- package/dist/browser/LocalStorageCacheMap.d.ts +43 -0
- package/dist/browser/SessionStorageCacheMap.d.ts +35 -0
- package/dist/eviction/EvictionStrategy.d.ts +50 -0
- package/dist/eviction/EvictionStrategyConfig.d.ts +97 -0
- package/dist/eviction/EvictionStrategyFactory.d.ts +12 -0
- package/dist/eviction/EvictionStrategyValidation.d.ts +36 -0
- package/dist/eviction/index.d.ts +9 -0
- package/dist/eviction/strategies/ARCEvictionStrategy.d.ts +68 -0
- package/dist/eviction/strategies/FIFOEvictionStrategy.d.ts +11 -0
- package/dist/eviction/strategies/LFUEvictionStrategy.d.ts +37 -0
- package/dist/eviction/strategies/LRUEvictionStrategy.d.ts +11 -0
- package/dist/eviction/strategies/MRUEvictionStrategy.d.ts +11 -0
- package/dist/eviction/strategies/RandomEvictionStrategy.d.ts +11 -0
- package/dist/eviction/strategies/TwoQueueEvictionStrategy.d.ts +53 -0
- package/dist/index.d.ts +24 -7
- package/dist/index.js +3879 -446
- package/dist/index.js.map +4 -4
- package/dist/memory/EnhancedMemoryCacheMap.d.ts +75 -0
- package/dist/memory/MemoryCacheMap.d.ts +33 -0
- package/dist/normalization.d.ts +20 -0
- package/dist/ops/action.d.ts +2 -3
- package/dist/ops/all.d.ts +2 -3
- package/dist/ops/allAction.d.ts +2 -3
- package/dist/ops/allFacet.d.ts +2 -3
- package/dist/ops/create.d.ts +2 -3
- package/dist/ops/facet.d.ts +2 -3
- package/dist/ops/find.d.ts +2 -3
- package/dist/ops/findOne.d.ts +2 -3
- package/dist/ops/get.d.ts +2 -3
- package/dist/ops/one.d.ts +2 -3
- package/dist/ops/remove.d.ts +2 -3
- package/dist/ops/reset.d.ts +2 -1
- package/dist/ops/retrieve.d.ts +2 -3
- package/dist/ops/set.d.ts +2 -2
- package/dist/ops/update.d.ts +2 -3
- package/dist/utils/CacheSize.d.ts +37 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,227 +1,38 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
var LibLogger = Logging.getLogger("@fjell/cache");
|
|
4
|
-
var logger_default = LibLogger;
|
|
5
|
-
|
|
6
|
-
// src/Aggregator.ts
|
|
7
|
-
var logger = logger_default.get("ItemAggregator");
|
|
8
|
-
var toCacheConfig = (config) => {
|
|
9
|
-
let cacheConfig;
|
|
10
|
-
if (config.optional === void 0) {
|
|
11
|
-
cacheConfig = { cache: config, optional: false };
|
|
12
|
-
} else {
|
|
13
|
-
cacheConfig = config;
|
|
14
|
-
}
|
|
15
|
-
return cacheConfig;
|
|
16
|
-
};
|
|
17
|
-
var createAggregator = async (cache, { aggregates = {}, events = {} }) => {
|
|
18
|
-
const populate = async (item) => {
|
|
19
|
-
logger.default("populate", { item });
|
|
20
|
-
for (const key in aggregates) {
|
|
21
|
-
await populateAggregate(key, item);
|
|
22
|
-
}
|
|
23
|
-
for (const key in events) {
|
|
24
|
-
await populateEvent(key, item);
|
|
25
|
-
}
|
|
26
|
-
logger.default("populate done", { item });
|
|
27
|
-
return item;
|
|
28
|
-
};
|
|
29
|
-
const populateAggregate = async (key, item) => {
|
|
30
|
-
logger.default("populate aggregate key", { key });
|
|
31
|
-
const cacheConfig = toCacheConfig(aggregates[key]);
|
|
32
|
-
if (item.refs === void 0) {
|
|
33
|
-
if (cacheConfig.optional === false) {
|
|
34
|
-
logger.error("Item does not have refs an is not optional " + JSON.stringify(item));
|
|
35
|
-
throw new Error("Item does not have refs an is not optional " + JSON.stringify(item));
|
|
36
|
-
} else {
|
|
37
|
-
if (item.events && Object.prototype.hasOwnProperty.call(item.events, key)) {
|
|
38
|
-
delete item.events[key];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
} else if (item.refs[key] === void 0) {
|
|
42
|
-
if (cacheConfig.optional === false) {
|
|
43
|
-
logger.error("Item does not have mandatory ref with key, not optional " + key + " " + JSON.stringify(item));
|
|
44
|
-
throw new Error("Item does not have mandatory ref with key, not optional " + key + " " + JSON.stringify(item));
|
|
45
|
-
} else {
|
|
46
|
-
if (item.events && Object.prototype.hasOwnProperty.call(item.events, key)) {
|
|
47
|
-
delete item.events[key];
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
const ref = item.refs[key];
|
|
52
|
-
logger.default("AGG Retrieving Item in Populate", { key: ref });
|
|
53
|
-
const [, newItem] = await cacheConfig.cache.operations.retrieve(ref);
|
|
54
|
-
if (newItem) {
|
|
55
|
-
if (item.aggs === void 0) {
|
|
56
|
-
item.aggs = {};
|
|
57
|
-
}
|
|
58
|
-
item.aggs[key] = {
|
|
59
|
-
key: ref,
|
|
60
|
-
item: newItem
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
const populateEvent = async (key, item) => {
|
|
66
|
-
logger.default("populate event key", { key });
|
|
67
|
-
const cacheConfig = toCacheConfig(events[key]);
|
|
68
|
-
if (item.events === void 0) {
|
|
69
|
-
throw new Error("Item does not have events " + JSON.stringify(item));
|
|
70
|
-
} else if (item.events[key] === void 0) {
|
|
71
|
-
if (cacheConfig.optional === false) {
|
|
72
|
-
logger.error("Item does not have mandatory event with key " + key + " " + JSON.stringify(item));
|
|
73
|
-
throw new Error("Item does not have mandatory event with key " + key + " " + JSON.stringify(item));
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
const event = item.events[key];
|
|
77
|
-
if (event.by === void 0) {
|
|
78
|
-
logger.error(
|
|
79
|
-
"populateEvent with an Event that does not have by",
|
|
80
|
-
{ event, ik: item.key, eventKey: key }
|
|
81
|
-
);
|
|
82
|
-
throw new Error("populateEvent with an Event that does not have by: " + JSON.stringify({ key, event }));
|
|
83
|
-
}
|
|
84
|
-
logger.default("EVENT Retrieving Item in Populate", { key: event.by });
|
|
85
|
-
const [, newItem] = await cacheConfig.cache.operations.retrieve(event.by);
|
|
86
|
-
if (newItem) {
|
|
87
|
-
event.agg = newItem;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
const all2 = async (query = {}, locations = []) => {
|
|
92
|
-
logger.default("all", { query, locations });
|
|
93
|
-
const [cacheMap, items] = await cache.operations.all(query, locations);
|
|
94
|
-
const populatedItems = await Promise.all(items.map(async (item) => populate(item)));
|
|
95
|
-
return [cacheMap, populatedItems];
|
|
96
|
-
};
|
|
97
|
-
const one2 = async (query = {}, locations = []) => {
|
|
98
|
-
logger.default("one", { query, locations });
|
|
99
|
-
const [cacheMap, item] = await cache.operations.one(query, locations);
|
|
100
|
-
let populatedItem = null;
|
|
101
|
-
if (item) {
|
|
102
|
-
populatedItem = await populate(item);
|
|
103
|
-
}
|
|
104
|
-
return [cacheMap, populatedItem];
|
|
105
|
-
};
|
|
106
|
-
const action2 = async (key, action3, body = {}) => {
|
|
107
|
-
logger.default("action", { key, action: action3, body });
|
|
108
|
-
const [cacheMap, item] = await cache.operations.action(key, action3, body);
|
|
109
|
-
const populatedItem = await populate(item);
|
|
110
|
-
return [cacheMap, populatedItem];
|
|
111
|
-
};
|
|
112
|
-
const allAction2 = async (action3, body = {}, locations = []) => {
|
|
113
|
-
logger.default("action", { action: action3, body, locations });
|
|
114
|
-
const [cacheMap, items] = await cache.operations.allAction(action3, body, locations);
|
|
115
|
-
const populatedItems = await Promise.all(items.map(async (item) => populate(item)));
|
|
116
|
-
return [cacheMap, populatedItems];
|
|
117
|
-
};
|
|
118
|
-
const allFacet2 = async (facet3, params = {}, locations = []) => {
|
|
119
|
-
logger.default("allFacet", { facet: facet3, params, locations });
|
|
120
|
-
const [cacheMap, response] = await cache.operations.allFacet(facet3, params, locations);
|
|
121
|
-
return [cacheMap, response];
|
|
122
|
-
};
|
|
123
|
-
const create2 = async (v, locations = []) => {
|
|
124
|
-
logger.default("create", { v, locations });
|
|
125
|
-
const [cacheMap, item] = await cache.operations.create(v, locations);
|
|
126
|
-
const populatedItem = await populate(item);
|
|
127
|
-
return [cacheMap, populatedItem];
|
|
128
|
-
};
|
|
129
|
-
const get2 = async (key) => {
|
|
130
|
-
logger.default("get", { key });
|
|
131
|
-
const [cacheMap, item] = await cache.operations.get(key);
|
|
132
|
-
let populatedItem = null;
|
|
133
|
-
if (item) {
|
|
134
|
-
populatedItem = await populate(item);
|
|
135
|
-
}
|
|
136
|
-
return [cacheMap, populatedItem];
|
|
137
|
-
};
|
|
138
|
-
const retrieve2 = async (key) => {
|
|
139
|
-
logger.default("retrieve", { key });
|
|
140
|
-
const [cacheMap, item] = await cache.operations.retrieve(key);
|
|
141
|
-
let populatedItem = null;
|
|
142
|
-
if (item) {
|
|
143
|
-
populatedItem = await populate(item);
|
|
144
|
-
}
|
|
145
|
-
return [cacheMap, populatedItem];
|
|
146
|
-
};
|
|
147
|
-
const remove2 = async (key) => {
|
|
148
|
-
logger.default("remove", { key });
|
|
149
|
-
const cacheMap = await cache.operations.remove(key);
|
|
150
|
-
return cacheMap;
|
|
151
|
-
};
|
|
152
|
-
const update2 = async (key, v) => {
|
|
153
|
-
logger.default("update", { key, v });
|
|
154
|
-
const [cacheMap, item] = await cache.operations.update(key, v);
|
|
155
|
-
const populatedItem = await populate(item);
|
|
156
|
-
return [cacheMap, populatedItem];
|
|
157
|
-
};
|
|
158
|
-
const facet2 = async (key, facet3) => {
|
|
159
|
-
logger.default("facet", { key, facet: facet3 });
|
|
160
|
-
const [cacheMap, response] = await cache.operations.facet(key, facet3);
|
|
161
|
-
return [cacheMap, response];
|
|
162
|
-
};
|
|
163
|
-
const find2 = async (finder, finderParams = {}, locations = []) => {
|
|
164
|
-
logger.default("find", { finder, finderParams, locations });
|
|
165
|
-
const [cacheMap, items] = await cache.operations.find(finder, finderParams, locations);
|
|
166
|
-
const populatedItems = await Promise.all(items.map(async (item) => populate(item)));
|
|
167
|
-
return [cacheMap, populatedItems];
|
|
168
|
-
};
|
|
169
|
-
const findOne2 = async (finder, finderParams = {}, locations = []) => {
|
|
170
|
-
logger.default("find", { finder, finderParams, locations });
|
|
171
|
-
const [cacheMap, item] = await cache.operations.findOne(finder, finderParams, locations);
|
|
172
|
-
const populatedItem = await populate(item);
|
|
173
|
-
return [cacheMap, populatedItem];
|
|
174
|
-
};
|
|
175
|
-
const set2 = async (key, v) => {
|
|
176
|
-
logger.default("set", { key, v });
|
|
177
|
-
const [cacheMap, item] = await cache.operations.set(key, v);
|
|
178
|
-
const populatedItem = await populate(item);
|
|
179
|
-
return [cacheMap, populatedItem];
|
|
180
|
-
};
|
|
181
|
-
const reset2 = async () => {
|
|
182
|
-
const cacheMap = await cache.operations.reset();
|
|
183
|
-
return cacheMap;
|
|
184
|
-
};
|
|
1
|
+
// src/CacheContext.ts
|
|
2
|
+
var createCacheContext = (api, cacheMap, pkType, options) => {
|
|
185
3
|
return {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// Cache operations exposed directly
|
|
193
|
-
all: all2,
|
|
194
|
-
one: one2,
|
|
195
|
-
action: action2,
|
|
196
|
-
allAction: allAction2,
|
|
197
|
-
allFacet: allFacet2,
|
|
198
|
-
create: create2,
|
|
199
|
-
get: get2,
|
|
200
|
-
retrieve: retrieve2,
|
|
201
|
-
remove: remove2,
|
|
202
|
-
update: update2,
|
|
203
|
-
facet: facet2,
|
|
204
|
-
find: find2,
|
|
205
|
-
findOne: findOne2,
|
|
206
|
-
reset: reset2,
|
|
207
|
-
set: set2,
|
|
208
|
-
// Aggregator-specific operations
|
|
209
|
-
populate,
|
|
210
|
-
populateAggregate,
|
|
211
|
-
populateEvent
|
|
4
|
+
api,
|
|
5
|
+
cacheMap,
|
|
6
|
+
pkType,
|
|
7
|
+
options,
|
|
8
|
+
itemTtl: options.memoryConfig?.ttl || options.ttl,
|
|
9
|
+
queryTtl: options.memoryConfig?.ttl || options.ttl
|
|
212
10
|
};
|
|
213
11
|
};
|
|
214
12
|
|
|
215
|
-
// src/
|
|
13
|
+
// src/ops/all.ts
|
|
216
14
|
import {
|
|
217
|
-
|
|
218
|
-
isComKey,
|
|
219
|
-
isQueryMatch
|
|
15
|
+
validatePK
|
|
220
16
|
} from "@fjell/core";
|
|
221
|
-
|
|
17
|
+
import { NotFoundError } from "@fjell/http-api";
|
|
18
|
+
|
|
19
|
+
// src/normalization.ts
|
|
222
20
|
var normalizeKeyValue = (value) => {
|
|
223
21
|
return String(value);
|
|
224
22
|
};
|
|
23
|
+
var deterministicStringify = (obj) => {
|
|
24
|
+
if (obj === null || typeof obj !== "object") {
|
|
25
|
+
return JSON.stringify(obj);
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(obj)) {
|
|
28
|
+
return "[" + obj.map(deterministicStringify).join(",") + "]";
|
|
29
|
+
}
|
|
30
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
31
|
+
const keyValuePairs = sortedKeys.map((key) => {
|
|
32
|
+
return JSON.stringify(key) + ":" + deterministicStringify(obj[key]);
|
|
33
|
+
});
|
|
34
|
+
return "{" + keyValuePairs.join(",") + "}";
|
|
35
|
+
};
|
|
225
36
|
var createNormalizedHashFunction = () => {
|
|
226
37
|
return (key) => {
|
|
227
38
|
if (typeof key === "object" && key !== null) {
|
|
@@ -240,7 +51,7 @@ var createNormalizedHashFunction = () => {
|
|
|
240
51
|
return locItem;
|
|
241
52
|
});
|
|
242
53
|
}
|
|
243
|
-
return
|
|
54
|
+
return deterministicStringify(normalizedKey);
|
|
244
55
|
}
|
|
245
56
|
return JSON.stringify(key);
|
|
246
57
|
};
|
|
@@ -252,7 +63,7 @@ var isLocKeyArrayEqual = (a, b) => {
|
|
|
252
63
|
for (let i = 0; i < a.length; i++) {
|
|
253
64
|
const normalizedA = normalizeLocKeyItem(a[i]);
|
|
254
65
|
const normalizedB = normalizeLocKeyItem(b[i]);
|
|
255
|
-
if (
|
|
66
|
+
if (deterministicStringify(normalizedA) !== deterministicStringify(normalizedB)) {
|
|
256
67
|
return false;
|
|
257
68
|
}
|
|
258
69
|
}
|
|
@@ -268,88 +79,92 @@ var normalizeLocKeyItem = (item) => {
|
|
|
268
79
|
}
|
|
269
80
|
return item;
|
|
270
81
|
};
|
|
271
|
-
var
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const hashedKey = this.normalizedHashFunction(key);
|
|
288
|
-
const entry = this.map[hashedKey];
|
|
289
|
-
return entry ? this.normalizedHashFunction(entry.originalKey) === this.normalizedHashFunction(key) : false;
|
|
290
|
-
}
|
|
291
|
-
delete(key) {
|
|
292
|
-
logger2.trace("delete", { key });
|
|
293
|
-
const hashedKey = this.normalizedHashFunction(key);
|
|
294
|
-
delete this.map[hashedKey];
|
|
295
|
-
}
|
|
296
|
-
allIn(locations) {
|
|
297
|
-
if (locations.length === 0) {
|
|
298
|
-
logger2.debug("Returning all items, LocKeys is empty");
|
|
299
|
-
return this.values();
|
|
300
|
-
} else {
|
|
301
|
-
const locKeys = locations;
|
|
302
|
-
logger2.debug("allIn", { locKeys, keys: this.keys().length });
|
|
303
|
-
return this.keys().filter((key) => key && isComKey(key)).filter((key) => {
|
|
304
|
-
const ComKey8 = key;
|
|
305
|
-
logger2.debug("Comparing Location Keys", {
|
|
306
|
-
locKeys,
|
|
307
|
-
ComKey: ComKey8
|
|
308
|
-
});
|
|
309
|
-
return isLocKeyArrayEqual(locKeys, ComKey8.loc);
|
|
310
|
-
}).map((key) => this.get(key));
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
// TODO: Can we do case insensitive matching?
|
|
314
|
-
contains(query, locations) {
|
|
315
|
-
logger2.debug("contains", { query, locations });
|
|
316
|
-
const items = this.allIn(locations);
|
|
317
|
-
return items.some((item) => isQueryMatch(item, query));
|
|
318
|
-
}
|
|
319
|
-
queryIn(query, locations = []) {
|
|
320
|
-
logger2.debug("queryIn", { query, locations });
|
|
321
|
-
const items = this.allIn(locations);
|
|
322
|
-
return items.filter((item) => isQueryMatch(item, query));
|
|
323
|
-
}
|
|
324
|
-
clone() {
|
|
325
|
-
const clone = new _CacheMap(this.types);
|
|
326
|
-
clone.map = this.map;
|
|
327
|
-
clone.normalizedHashFunction = this.normalizedHashFunction;
|
|
328
|
-
return clone;
|
|
329
|
-
}
|
|
82
|
+
var createQueryHash = (pkType, query, locations) => {
|
|
83
|
+
const normalizedQuery = JSON.parse(JSON.stringify(query || {}));
|
|
84
|
+
const sortedQueryKeys = Object.keys(normalizedQuery).sort();
|
|
85
|
+
const sortedQuery = {};
|
|
86
|
+
sortedQueryKeys.forEach((key) => {
|
|
87
|
+
sortedQuery[key] = normalizedQuery[key];
|
|
88
|
+
});
|
|
89
|
+
const locationsArray = Array.isArray(locations) ? locations : [];
|
|
90
|
+
const normalizedLocations = locationsArray.map(normalizeLocKeyItem);
|
|
91
|
+
const hashInput = {
|
|
92
|
+
type: "query",
|
|
93
|
+
pkType,
|
|
94
|
+
query: sortedQuery,
|
|
95
|
+
locations: normalizedLocations
|
|
96
|
+
};
|
|
97
|
+
return deterministicStringify(hashInput);
|
|
330
98
|
};
|
|
99
|
+
var createFinderHash = (finder, params, locations) => {
|
|
100
|
+
const normalizedParams = JSON.parse(JSON.stringify(params || {}));
|
|
101
|
+
const sortedParamKeys = Object.keys(normalizedParams).sort();
|
|
102
|
+
const sortedParams = {};
|
|
103
|
+
sortedParamKeys.forEach((key) => {
|
|
104
|
+
sortedParams[key] = normalizedParams[key];
|
|
105
|
+
});
|
|
106
|
+
const locationsArray = Array.isArray(locations) ? locations : [];
|
|
107
|
+
const normalizedLocations = locationsArray.map(normalizeLocKeyItem);
|
|
108
|
+
const hashInput = {
|
|
109
|
+
type: "finder",
|
|
110
|
+
finder,
|
|
111
|
+
params: sortedParams,
|
|
112
|
+
locations: normalizedLocations
|
|
113
|
+
};
|
|
114
|
+
return deterministicStringify(hashInput);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/logger.ts
|
|
118
|
+
import Logging from "@fjell/logging";
|
|
119
|
+
var LibLogger = Logging.getLogger("@fjell/cache");
|
|
120
|
+
var logger_default = LibLogger;
|
|
331
121
|
|
|
332
122
|
// src/ops/all.ts
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
123
|
+
var logger = logger_default.get("all");
|
|
124
|
+
var all = async (query = {}, locations = [], context) => {
|
|
125
|
+
const { api, cacheMap, pkType, queryTtl } = context;
|
|
126
|
+
logger.default("all", { query, locations });
|
|
127
|
+
const queryHash = createQueryHash(pkType, query, locations);
|
|
128
|
+
logger.debug("Generated query hash for all", { queryHash });
|
|
129
|
+
const cachedItemKeys = cacheMap.getQueryResult(queryHash);
|
|
130
|
+
if (cachedItemKeys) {
|
|
131
|
+
logger.debug("Using cached query results", { cachedKeyCount: cachedItemKeys.length });
|
|
132
|
+
const cachedItems = [];
|
|
133
|
+
let allItemsAvailable = true;
|
|
134
|
+
for (const itemKey of cachedItemKeys) {
|
|
135
|
+
const item = cacheMap.get(itemKey);
|
|
136
|
+
if (item) {
|
|
137
|
+
cachedItems.push(item);
|
|
138
|
+
} else {
|
|
139
|
+
allItemsAvailable = false;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (allItemsAvailable) {
|
|
144
|
+
return [context, validatePK(cachedItems, pkType)];
|
|
145
|
+
} else {
|
|
146
|
+
logger.debug("Some cached items missing, invalidating query cache");
|
|
147
|
+
cacheMap.deleteQueryResult(queryHash);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
340
150
|
let ret = [];
|
|
341
151
|
try {
|
|
342
152
|
ret = await api.all(query, locations);
|
|
343
153
|
ret.forEach((v) => {
|
|
344
154
|
cacheMap.set(v.key, v);
|
|
345
155
|
});
|
|
156
|
+
const itemKeys = ret.map((item) => item.key);
|
|
157
|
+
cacheMap.setQueryResult(queryHash, itemKeys, queryTtl);
|
|
158
|
+
logger.debug("Cached query result", { queryHash, itemKeyCount: itemKeys.length, ttl: queryTtl });
|
|
346
159
|
} catch (e) {
|
|
347
160
|
if (e instanceof NotFoundError) {
|
|
161
|
+
cacheMap.setQueryResult(queryHash, [], queryTtl);
|
|
162
|
+
logger.debug("Cached empty query result for not found", { queryHash });
|
|
348
163
|
} else {
|
|
349
164
|
throw e;
|
|
350
165
|
}
|
|
351
166
|
}
|
|
352
|
-
return [
|
|
167
|
+
return [context, validatePK(ret, pkType)];
|
|
353
168
|
};
|
|
354
169
|
|
|
355
170
|
// src/ops/one.ts
|
|
@@ -357,23 +172,47 @@ import {
|
|
|
357
172
|
validatePK as validatePK2
|
|
358
173
|
} from "@fjell/core";
|
|
359
174
|
import { NotFoundError as NotFoundError2 } from "@fjell/http-api";
|
|
360
|
-
var
|
|
361
|
-
var one = async (
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
175
|
+
var logger2 = logger_default.get("one");
|
|
176
|
+
var one = async (query = {}, locations = [], context) => {
|
|
177
|
+
const { api, cacheMap, pkType, queryTtl } = context;
|
|
178
|
+
logger2.default("one", { query, locations });
|
|
179
|
+
const queryHash = createQueryHash(pkType, query, locations);
|
|
180
|
+
logger2.debug("Generated query hash for one", { queryHash });
|
|
181
|
+
const cachedItemKeys = cacheMap.getQueryResult(queryHash);
|
|
182
|
+
if (cachedItemKeys) {
|
|
183
|
+
logger2.debug("Using cached query results", { cachedKeyCount: cachedItemKeys.length });
|
|
184
|
+
if (cachedItemKeys.length === 0) {
|
|
185
|
+
return [context, null];
|
|
186
|
+
}
|
|
187
|
+
const item = cacheMap.get(cachedItemKeys[0]);
|
|
188
|
+
if (item) {
|
|
189
|
+
return [context, validatePK2(item, pkType)];
|
|
190
|
+
} else {
|
|
191
|
+
logger2.debug("Cached item missing, invalidating query cache");
|
|
192
|
+
cacheMap.deleteQueryResult(queryHash);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
let retItem = null;
|
|
196
|
+
try {
|
|
365
197
|
retItem = await api.one(query, locations);
|
|
366
198
|
if (retItem) {
|
|
367
199
|
cacheMap.set(retItem.key, retItem);
|
|
200
|
+
cacheMap.setQueryResult(queryHash, [retItem.key], queryTtl);
|
|
201
|
+
logger2.debug("Cached query result", { queryHash, itemKey: retItem.key, ttl: queryTtl });
|
|
202
|
+
} else {
|
|
203
|
+
cacheMap.setQueryResult(queryHash, [], queryTtl);
|
|
204
|
+
logger2.debug("Cached empty query result", { queryHash, ttl: queryTtl });
|
|
368
205
|
}
|
|
369
206
|
} catch (e) {
|
|
370
207
|
if (e instanceof NotFoundError2) {
|
|
208
|
+
cacheMap.setQueryResult(queryHash, [], queryTtl);
|
|
209
|
+
logger2.debug("Cached empty query result for not found", { queryHash });
|
|
371
210
|
} else {
|
|
372
211
|
throw e;
|
|
373
212
|
}
|
|
374
213
|
}
|
|
375
214
|
return [
|
|
376
|
-
|
|
215
|
+
context,
|
|
377
216
|
retItem ? validatePK2(retItem, pkType) : null
|
|
378
217
|
];
|
|
379
218
|
};
|
|
@@ -382,12 +221,13 @@ var one = async (api, cacheMap, pkType, query = {}, locations = []) => {
|
|
|
382
221
|
import {
|
|
383
222
|
validatePK as validatePK3
|
|
384
223
|
} from "@fjell/core";
|
|
385
|
-
var
|
|
386
|
-
var create = async (
|
|
387
|
-
|
|
224
|
+
var logger3 = logger_default.get("create");
|
|
225
|
+
var create = async (v, locations = [], context) => {
|
|
226
|
+
const { api, cacheMap, pkType } = context;
|
|
227
|
+
logger3.default("create", { v, locations });
|
|
388
228
|
const created = await api.create(v, locations);
|
|
389
229
|
cacheMap.set(created.key, created);
|
|
390
|
-
return [
|
|
230
|
+
return [context, validatePK3(created, pkType)];
|
|
391
231
|
};
|
|
392
232
|
|
|
393
233
|
// src/ops/get.ts
|
|
@@ -395,25 +235,53 @@ import {
|
|
|
395
235
|
isValidItemKey,
|
|
396
236
|
validatePK as validatePK4
|
|
397
237
|
} from "@fjell/core";
|
|
398
|
-
var
|
|
399
|
-
var
|
|
400
|
-
|
|
238
|
+
var logger4 = logger_default.get("get");
|
|
239
|
+
var inFlightRequests = /* @__PURE__ */ new Map();
|
|
240
|
+
var keyToString = (key) => {
|
|
241
|
+
return JSON.stringify(key);
|
|
242
|
+
};
|
|
243
|
+
var get = async (key, context) => {
|
|
244
|
+
const { api, cacheMap, pkType, itemTtl } = context;
|
|
245
|
+
logger4.default("get", { key, itemTtl });
|
|
401
246
|
if (!isValidItemKey(key)) {
|
|
402
|
-
|
|
247
|
+
logger4.error("Key for Get is not a valid ItemKey: %j", key);
|
|
403
248
|
throw new Error("Key for Get is not a valid ItemKey");
|
|
404
249
|
}
|
|
250
|
+
if (typeof itemTtl === "number" && itemTtl > 0) {
|
|
251
|
+
const cachedItem = cacheMap.getWithTTL(key, itemTtl);
|
|
252
|
+
if (cachedItem) {
|
|
253
|
+
logger4.debug("Cache hit with TTL", { key, itemTtl });
|
|
254
|
+
return [context, validatePK4(cachedItem, pkType)];
|
|
255
|
+
}
|
|
256
|
+
logger4.debug("Cache miss or expired", { key, itemTtl });
|
|
257
|
+
}
|
|
405
258
|
let ret;
|
|
259
|
+
const keyStr = keyToString(key);
|
|
406
260
|
try {
|
|
407
|
-
|
|
261
|
+
let apiRequest = inFlightRequests.get(keyStr);
|
|
262
|
+
if (!apiRequest) {
|
|
263
|
+
apiRequest = api.get(key);
|
|
264
|
+
inFlightRequests.set(keyStr, apiRequest);
|
|
265
|
+
if (apiRequest && typeof apiRequest.finally === "function") {
|
|
266
|
+
apiRequest.finally(() => {
|
|
267
|
+
inFlightRequests.delete(keyStr);
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
inFlightRequests.delete(keyStr);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
logger4.debug("Using in-flight request for key", { key });
|
|
274
|
+
}
|
|
275
|
+
ret = await apiRequest;
|
|
408
276
|
if (ret) {
|
|
409
277
|
cacheMap.set(ret.key, ret);
|
|
410
278
|
}
|
|
411
279
|
} catch (e) {
|
|
412
|
-
|
|
280
|
+
logger4.error("Error getting item for key", { key, message: e.message, stack: e.stack });
|
|
413
281
|
throw e;
|
|
414
282
|
}
|
|
415
283
|
return [
|
|
416
|
-
|
|
284
|
+
context,
|
|
417
285
|
ret ? validatePK4(ret, pkType) : null
|
|
418
286
|
];
|
|
419
287
|
};
|
|
@@ -423,24 +291,25 @@ import {
|
|
|
423
291
|
isValidItemKey as isValidItemKey2,
|
|
424
292
|
validatePK as validatePK5
|
|
425
293
|
} from "@fjell/core";
|
|
426
|
-
var
|
|
427
|
-
var retrieve = async (
|
|
428
|
-
|
|
294
|
+
var logger5 = logger_default.get("retrieve");
|
|
295
|
+
var retrieve = async (key, context) => {
|
|
296
|
+
const { cacheMap, pkType } = context;
|
|
297
|
+
logger5.default("retrieve", { key });
|
|
429
298
|
if (!isValidItemKey2(key)) {
|
|
430
|
-
|
|
299
|
+
logger5.error("Key for Retrieve is not a valid ItemKey: %j", key);
|
|
431
300
|
throw new Error("Key for Retrieve is not a valid ItemKey");
|
|
432
301
|
}
|
|
433
302
|
const containsItemKey = cacheMap.includesKey(key);
|
|
434
303
|
let retrieved;
|
|
435
304
|
if (containsItemKey) {
|
|
436
|
-
|
|
305
|
+
logger5.default("Looking for Object in Cache", key);
|
|
437
306
|
retrieved = cacheMap.get(key);
|
|
438
307
|
} else {
|
|
439
|
-
|
|
440
|
-
[, retrieved] = await get(
|
|
308
|
+
logger5.default("Object Not Found in Cache, Retrieving from Server API", { key });
|
|
309
|
+
[, retrieved] = await get(key, context);
|
|
441
310
|
}
|
|
442
311
|
const retValue = [
|
|
443
|
-
containsItemKey ? null :
|
|
312
|
+
containsItemKey ? null : context,
|
|
444
313
|
retrieved ? validatePK5(retrieved, pkType) : null
|
|
445
314
|
];
|
|
446
315
|
return retValue;
|
|
@@ -450,21 +319,23 @@ var retrieve = async (api, cacheMap, pkType, key) => {
|
|
|
450
319
|
import {
|
|
451
320
|
isValidItemKey as isValidItemKey3
|
|
452
321
|
} from "@fjell/core";
|
|
453
|
-
var
|
|
454
|
-
var remove = async (
|
|
455
|
-
|
|
322
|
+
var logger6 = logger_default.get("remove");
|
|
323
|
+
var remove = async (key, context) => {
|
|
324
|
+
const { api, cacheMap } = context;
|
|
325
|
+
logger6.default("remove", { key });
|
|
456
326
|
if (!isValidItemKey3(key)) {
|
|
457
|
-
|
|
327
|
+
logger6.error("Key for Remove is not a valid ItemKey: %j", key);
|
|
458
328
|
throw new Error("Key for Remove is not a valid ItemKey");
|
|
459
329
|
}
|
|
460
330
|
try {
|
|
461
331
|
await api.remove(key);
|
|
462
332
|
cacheMap.delete(key);
|
|
333
|
+
logger6.debug("Successfully removed item from API and cache", { key });
|
|
463
334
|
} catch (e) {
|
|
464
|
-
|
|
335
|
+
logger6.error("Error deleting item", { error: e });
|
|
465
336
|
throw e;
|
|
466
337
|
}
|
|
467
|
-
return
|
|
338
|
+
return context;
|
|
468
339
|
};
|
|
469
340
|
|
|
470
341
|
// src/ops/update.ts
|
|
@@ -472,19 +343,23 @@ import {
|
|
|
472
343
|
isValidItemKey as isValidItemKey4,
|
|
473
344
|
validatePK as validatePK6
|
|
474
345
|
} from "@fjell/core";
|
|
475
|
-
var
|
|
476
|
-
var update = async (
|
|
477
|
-
|
|
346
|
+
var logger7 = logger_default.get("update");
|
|
347
|
+
var update = async (key, v, context) => {
|
|
348
|
+
const { api, cacheMap, pkType } = context;
|
|
349
|
+
logger7.default("update", { key, v });
|
|
478
350
|
if (!isValidItemKey4(key)) {
|
|
479
|
-
|
|
351
|
+
logger7.error("Key for Update is not a valid ItemKey: %j", key);
|
|
480
352
|
throw new Error("Key for Update is not a valid ItemKey");
|
|
481
353
|
}
|
|
354
|
+
logger7.debug("Invalidating item key before update", { key });
|
|
355
|
+
cacheMap.invalidateItemKeys([key]);
|
|
482
356
|
try {
|
|
483
357
|
const updated = await api.update(key, v);
|
|
358
|
+
logger7.debug("Caching update result", { updatedKey: updated.key });
|
|
484
359
|
cacheMap.set(updated.key, updated);
|
|
485
|
-
return [
|
|
360
|
+
return [context, validatePK6(updated, pkType)];
|
|
486
361
|
} catch (e) {
|
|
487
|
-
|
|
362
|
+
logger7.error("Error updating item", { error: e });
|
|
488
363
|
throw e;
|
|
489
364
|
}
|
|
490
365
|
};
|
|
@@ -494,16 +369,20 @@ import {
|
|
|
494
369
|
isValidItemKey as isValidItemKey5,
|
|
495
370
|
validatePK as validatePK7
|
|
496
371
|
} from "@fjell/core";
|
|
497
|
-
var
|
|
498
|
-
var action = async (
|
|
499
|
-
|
|
372
|
+
var logger8 = logger_default.get("action");
|
|
373
|
+
var action = async (key, action2, body = {}, context) => {
|
|
374
|
+
const { api, cacheMap, pkType } = context;
|
|
375
|
+
logger8.default("action", { key, action: action2, body });
|
|
500
376
|
if (!isValidItemKey5(key)) {
|
|
501
|
-
|
|
377
|
+
logger8.error("Key for Action is not a valid ItemKey: %j", key);
|
|
502
378
|
throw new Error("Key for Action is not a valid ItemKey");
|
|
503
379
|
}
|
|
380
|
+
logger8.debug("Invalidating item key before action", { key });
|
|
381
|
+
cacheMap.invalidateItemKeys([key]);
|
|
504
382
|
const updated = await api.action(key, action2, body);
|
|
383
|
+
logger8.debug("Caching action result", { updatedKey: updated.key });
|
|
505
384
|
cacheMap.set(updated.key, updated);
|
|
506
|
-
return [
|
|
385
|
+
return [context, validatePK7(updated, pkType)];
|
|
507
386
|
};
|
|
508
387
|
|
|
509
388
|
// src/ops/allAction.ts
|
|
@@ -511,12 +390,16 @@ import {
|
|
|
511
390
|
validatePK as validatePK8
|
|
512
391
|
} from "@fjell/core";
|
|
513
392
|
import { NotFoundError as NotFoundError3 } from "@fjell/http-api";
|
|
514
|
-
var
|
|
515
|
-
var allAction = async (
|
|
516
|
-
|
|
393
|
+
var logger9 = logger_default.get("allAction");
|
|
394
|
+
var allAction = async (action2, body = {}, locations = [], context) => {
|
|
395
|
+
const { api, cacheMap, pkType } = context;
|
|
396
|
+
logger9.default("allAction", { action: action2, body, locations });
|
|
397
|
+
logger9.debug("Invalidating location before allAction", { locations });
|
|
398
|
+
cacheMap.invalidateLocation(locations);
|
|
517
399
|
let ret = [];
|
|
518
400
|
try {
|
|
519
401
|
ret = await api.allAction(action2, body, locations);
|
|
402
|
+
logger9.debug("Caching allAction results", { resultCount: ret.length });
|
|
520
403
|
ret.forEach((v) => {
|
|
521
404
|
cacheMap.set(v.key, v);
|
|
522
405
|
});
|
|
@@ -526,49 +409,94 @@ var allAction = async (api, cacheMap, pkType, action2, body = {}, locations = []
|
|
|
526
409
|
throw e;
|
|
527
410
|
}
|
|
528
411
|
}
|
|
529
|
-
return [
|
|
412
|
+
return [context, validatePK8(ret, pkType)];
|
|
530
413
|
};
|
|
531
414
|
|
|
532
415
|
// src/ops/facet.ts
|
|
533
|
-
var
|
|
534
|
-
var facet = async (
|
|
535
|
-
|
|
416
|
+
var logger10 = logger_default.get("facet");
|
|
417
|
+
var facet = async (key, facet2, params = {}, context) => {
|
|
418
|
+
const { api } = context;
|
|
419
|
+
logger10.default("facet", { key, facet: facet2 });
|
|
536
420
|
const ret = await api.facet(key, facet2, params);
|
|
537
|
-
return
|
|
421
|
+
return ret;
|
|
538
422
|
};
|
|
539
423
|
|
|
540
424
|
// src/ops/allFacet.ts
|
|
541
|
-
var
|
|
542
|
-
var allFacet = async (
|
|
543
|
-
|
|
425
|
+
var logger11 = logger_default.get("allFacet");
|
|
426
|
+
var allFacet = async (facet2, params = {}, locations = [], context) => {
|
|
427
|
+
const { api } = context;
|
|
428
|
+
logger11.default("allFacet", { facet: facet2, params, locations });
|
|
544
429
|
const ret = await api.allFacet(facet2, params, locations);
|
|
545
|
-
return
|
|
430
|
+
return ret;
|
|
546
431
|
};
|
|
547
432
|
|
|
548
433
|
// src/ops/find.ts
|
|
549
434
|
import {
|
|
550
435
|
validatePK as validatePK9
|
|
551
436
|
} from "@fjell/core";
|
|
552
|
-
var
|
|
553
|
-
var find = async (
|
|
554
|
-
|
|
437
|
+
var logger12 = logger_default.get("find");
|
|
438
|
+
var find = async (finder, params = {}, locations = [], context) => {
|
|
439
|
+
const { api, cacheMap, pkType, queryTtl } = context;
|
|
440
|
+
logger12.default("find", { finder, params, locations });
|
|
441
|
+
const queryHash = createFinderHash(finder, params, locations);
|
|
442
|
+
logger12.debug("Generated query hash for find", { queryHash });
|
|
443
|
+
const cachedItemKeys = cacheMap.getQueryResult(queryHash);
|
|
444
|
+
if (cachedItemKeys) {
|
|
445
|
+
logger12.debug("Using cached query results", { cachedKeyCount: cachedItemKeys.length });
|
|
446
|
+
const cachedItems = [];
|
|
447
|
+
let allItemsAvailable = true;
|
|
448
|
+
for (const itemKey of cachedItemKeys) {
|
|
449
|
+
const item = cacheMap.get(itemKey);
|
|
450
|
+
if (item) {
|
|
451
|
+
cachedItems.push(item);
|
|
452
|
+
} else {
|
|
453
|
+
allItemsAvailable = false;
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (allItemsAvailable) {
|
|
458
|
+
return [context, validatePK9(cachedItems, pkType)];
|
|
459
|
+
} else {
|
|
460
|
+
logger12.debug("Some cached items missing, invalidating query cache");
|
|
461
|
+
cacheMap.deleteQueryResult(queryHash);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
555
464
|
const ret = await api.find(finder, params, locations);
|
|
556
465
|
ret.forEach((v) => {
|
|
557
466
|
cacheMap.set(v.key, v);
|
|
558
467
|
});
|
|
559
|
-
|
|
468
|
+
const itemKeys = ret.map((item) => item.key);
|
|
469
|
+
cacheMap.setQueryResult(queryHash, itemKeys, queryTtl);
|
|
470
|
+
logger12.debug("Cached query result", { queryHash, itemKeyCount: itemKeys.length, ttl: queryTtl });
|
|
471
|
+
return [context, validatePK9(ret, pkType)];
|
|
560
472
|
};
|
|
561
473
|
|
|
562
474
|
// src/ops/findOne.ts
|
|
563
475
|
import {
|
|
564
476
|
validatePK as validatePK10
|
|
565
477
|
} from "@fjell/core";
|
|
566
|
-
var
|
|
567
|
-
var findOne = async (
|
|
568
|
-
|
|
478
|
+
var logger13 = logger_default.get("findOne");
|
|
479
|
+
var findOne = async (finder, finderParams = {}, locations = [], context) => {
|
|
480
|
+
const { api, cacheMap, pkType, queryTtl } = context;
|
|
481
|
+
logger13.default("findOne", { finder, finderParams, locations });
|
|
482
|
+
const queryHash = createFinderHash(finder, finderParams, locations);
|
|
483
|
+
logger13.debug("Generated query hash for findOne", { queryHash });
|
|
484
|
+
const cachedItemKeys = cacheMap.getQueryResult(queryHash);
|
|
485
|
+
if (cachedItemKeys && cachedItemKeys.length > 0) {
|
|
486
|
+
logger13.debug("Using cached query results", { cachedKeyCount: cachedItemKeys.length });
|
|
487
|
+
const item = cacheMap.get(cachedItemKeys[0]);
|
|
488
|
+
if (item) {
|
|
489
|
+
return [context, validatePK10(item, pkType)];
|
|
490
|
+
} else {
|
|
491
|
+
logger13.debug("Cached item missing, invalidating query cache");
|
|
492
|
+
cacheMap.deleteQueryResult(queryHash);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
569
495
|
const ret = await api.findOne(finder, finderParams, locations);
|
|
570
496
|
cacheMap.set(ret.key, ret);
|
|
571
|
-
|
|
497
|
+
cacheMap.setQueryResult(queryHash, [ret.key], queryTtl);
|
|
498
|
+
logger13.debug("Cached query result", { queryHash, itemKey: ret.key, ttl: queryTtl });
|
|
499
|
+
return [context, validatePK10(ret, pkType)];
|
|
572
500
|
};
|
|
573
501
|
|
|
574
502
|
// src/ops/set.ts
|
|
@@ -577,7 +505,7 @@ import {
|
|
|
577
505
|
isValidItemKey as isValidItemKey6,
|
|
578
506
|
validatePK as validatePK11
|
|
579
507
|
} from "@fjell/core";
|
|
580
|
-
var
|
|
508
|
+
var logger14 = logger_default.get("set");
|
|
581
509
|
var normalizeKeyValue2 = (value) => {
|
|
582
510
|
return String(value);
|
|
583
511
|
};
|
|
@@ -588,146 +516,3651 @@ var isItemKeyEqualNormalized = (a, b) => {
|
|
|
588
516
|
};
|
|
589
517
|
var normalizeKey = (key) => {
|
|
590
518
|
if (typeof key === "object" && key !== null) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
519
|
+
let needsNormalization = false;
|
|
520
|
+
let normalizedKey = key;
|
|
521
|
+
if ("pk" in key && key.pk !== null && typeof key.pk !== "string") {
|
|
522
|
+
needsNormalization = true;
|
|
594
523
|
}
|
|
595
|
-
if ("lk" in
|
|
596
|
-
|
|
524
|
+
if ("lk" in key && key.lk !== null && typeof key.lk !== "string") {
|
|
525
|
+
needsNormalization = true;
|
|
597
526
|
}
|
|
598
|
-
if ("loc" in
|
|
599
|
-
|
|
600
|
-
if (locItem && "lk" in locItem && locItem.lk !== null) {
|
|
601
|
-
|
|
527
|
+
if ("loc" in key && Array.isArray(key.loc)) {
|
|
528
|
+
for (const locItem of key.loc) {
|
|
529
|
+
if (locItem && "lk" in locItem && locItem.lk !== null && typeof locItem.lk !== "string") {
|
|
530
|
+
needsNormalization = true;
|
|
531
|
+
break;
|
|
602
532
|
}
|
|
603
|
-
|
|
604
|
-
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (needsNormalization) {
|
|
536
|
+
normalizedKey = { ...key };
|
|
537
|
+
if ("pk" in normalizedKey && normalizedKey.pk !== null) {
|
|
538
|
+
normalizedKey.pk = normalizeKeyValue2(normalizedKey.pk);
|
|
539
|
+
}
|
|
540
|
+
if ("lk" in normalizedKey && normalizedKey.lk !== null) {
|
|
541
|
+
normalizedKey.lk = normalizeKeyValue2(normalizedKey.lk);
|
|
542
|
+
}
|
|
543
|
+
if ("loc" in normalizedKey && Array.isArray(normalizedKey.loc)) {
|
|
544
|
+
normalizedKey.loc = normalizedKey.loc.map((locItem) => {
|
|
545
|
+
if (locItem && "lk" in locItem && locItem.lk !== null && typeof locItem.lk !== "string") {
|
|
546
|
+
return { ...locItem, lk: normalizeKeyValue2(locItem.lk) };
|
|
547
|
+
}
|
|
548
|
+
return locItem;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
605
551
|
}
|
|
606
552
|
return normalizedKey;
|
|
607
553
|
}
|
|
608
554
|
return key;
|
|
609
555
|
};
|
|
610
|
-
var set = async (
|
|
611
|
-
|
|
556
|
+
var set = async (key, v, context) => {
|
|
557
|
+
const { cacheMap, pkType } = context;
|
|
558
|
+
logger14.default("set", { key, v });
|
|
612
559
|
if (!isValidItemKey6(key)) {
|
|
613
|
-
|
|
560
|
+
logger14.error("Key for Set is not a valid ItemKey: %j", key);
|
|
614
561
|
throw new Error("Key for Set is not a valid ItemKey");
|
|
615
562
|
}
|
|
616
563
|
validatePK11(v, pkType);
|
|
617
564
|
if (!isItemKeyEqualNormalized(key, v.key)) {
|
|
618
|
-
|
|
565
|
+
logger14.error("Key does not match item key: %j != %j", key, v.key);
|
|
619
566
|
throw new Error("Key does not match item key");
|
|
620
567
|
}
|
|
621
568
|
cacheMap.set(key, v);
|
|
622
|
-
return [
|
|
569
|
+
return [context, validatePK11(v, pkType)];
|
|
623
570
|
};
|
|
624
571
|
|
|
625
|
-
// src/
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
};
|
|
572
|
+
// src/memory/MemoryCacheMap.ts
|
|
573
|
+
import {
|
|
574
|
+
isComKey,
|
|
575
|
+
isQueryMatch
|
|
576
|
+
} from "@fjell/core";
|
|
630
577
|
|
|
631
|
-
// src/
|
|
632
|
-
var
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
get: (key) => get(api, cacheMap, pkType, key),
|
|
638
|
-
retrieve: (key) => retrieve(api, cacheMap, pkType, key),
|
|
639
|
-
remove: (key) => remove(api, cacheMap, key),
|
|
640
|
-
update: (key, item) => update(api, cacheMap, pkType, key, item),
|
|
641
|
-
action: (key, actionName, body) => action(api, cacheMap, pkType, key, actionName, body),
|
|
642
|
-
allAction: (actionName, body, locations) => allAction(api, cacheMap, pkType, actionName, body, locations),
|
|
643
|
-
facet: (key, facetName, params) => facet(api, cacheMap, key, facetName, params),
|
|
644
|
-
allFacet: (facetName, params, locations) => allFacet(api, cacheMap, facetName, params, locations),
|
|
645
|
-
find: (finder, params, locations) => find(api, cacheMap, pkType, finder, params, locations),
|
|
646
|
-
findOne: (finder, params, locations) => findOne(api, cacheMap, pkType, finder, params, locations),
|
|
647
|
-
set: (key, item) => set(cacheMap, pkType, key, item),
|
|
648
|
-
reset: () => reset(coordinate)
|
|
649
|
-
};
|
|
578
|
+
// src/CacheMap.ts
|
|
579
|
+
var CacheMap = class {
|
|
580
|
+
types;
|
|
581
|
+
constructor(types) {
|
|
582
|
+
this.types = types;
|
|
583
|
+
}
|
|
650
584
|
};
|
|
651
585
|
|
|
652
|
-
// src/
|
|
653
|
-
var
|
|
654
|
-
var
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
586
|
+
// src/memory/MemoryCacheMap.ts
|
|
587
|
+
var logger15 = logger_default.get("MemoryCacheMap");
|
|
588
|
+
var MemoryCacheMap = class _MemoryCacheMap extends CacheMap {
|
|
589
|
+
map = {};
|
|
590
|
+
normalizedHashFunction;
|
|
591
|
+
// Query result cache: maps query hash to cache entry with expiration
|
|
592
|
+
queryResultCache = {};
|
|
593
|
+
constructor(types, initialData) {
|
|
594
|
+
super(types);
|
|
595
|
+
this.normalizedHashFunction = createNormalizedHashFunction();
|
|
596
|
+
if (initialData) {
|
|
597
|
+
for (const [keyStr, value] of Object.entries(initialData)) {
|
|
598
|
+
try {
|
|
599
|
+
const key = JSON.parse(keyStr);
|
|
600
|
+
this.set(key, value);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
logger15.error("Failed to parse initial data key", { keyStr, error });
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
get(key) {
|
|
608
|
+
logger15.trace("get", { key });
|
|
609
|
+
const hashedKey = this.normalizedHashFunction(key);
|
|
610
|
+
const entry = this.map[hashedKey];
|
|
611
|
+
return entry && this.normalizedHashFunction(entry.originalKey) === hashedKey ? entry.value : null;
|
|
612
|
+
}
|
|
613
|
+
getWithTTL(key, ttl) {
|
|
614
|
+
logger15.trace("getWithTTL", { key, ttl });
|
|
615
|
+
if (ttl === 0) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
const hashedKey = this.normalizedHashFunction(key);
|
|
619
|
+
const entry = this.map[hashedKey];
|
|
620
|
+
if (!entry || this.normalizedHashFunction(entry.originalKey) !== hashedKey) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
const now = Date.now();
|
|
624
|
+
const age = now - entry.timestamp;
|
|
625
|
+
if (age >= ttl) {
|
|
626
|
+
logger15.trace("Item expired, removing from cache", { key, age, ttl });
|
|
627
|
+
delete this.map[hashedKey];
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
return entry.value;
|
|
631
|
+
}
|
|
632
|
+
set(key, value) {
|
|
633
|
+
logger15.trace("set", { key, value });
|
|
634
|
+
const hashedKey = this.normalizedHashFunction(key);
|
|
635
|
+
this.map[hashedKey] = { originalKey: key, value, timestamp: Date.now() };
|
|
636
|
+
}
|
|
637
|
+
includesKey(key) {
|
|
638
|
+
const hashedKey = this.normalizedHashFunction(key);
|
|
639
|
+
const entry = this.map[hashedKey];
|
|
640
|
+
return !!entry && this.normalizedHashFunction(entry.originalKey) === hashedKey;
|
|
641
|
+
}
|
|
642
|
+
delete(key) {
|
|
643
|
+
logger15.trace("delete", { key });
|
|
644
|
+
const hashedKey = this.normalizedHashFunction(key);
|
|
645
|
+
delete this.map[hashedKey];
|
|
646
|
+
}
|
|
647
|
+
keys() {
|
|
648
|
+
return Object.values(this.map).map((entry) => entry.originalKey);
|
|
649
|
+
}
|
|
650
|
+
values() {
|
|
651
|
+
return Object.values(this.map).map((entry) => entry.value);
|
|
652
|
+
}
|
|
653
|
+
clear() {
|
|
654
|
+
this.map = {};
|
|
655
|
+
}
|
|
656
|
+
allIn(locations) {
|
|
657
|
+
const allValues = this.values();
|
|
658
|
+
if (locations.length === 0) {
|
|
659
|
+
logger15.debug("Returning all items, LocKeys is empty");
|
|
660
|
+
return allValues;
|
|
661
|
+
} else {
|
|
662
|
+
logger15.debug("allIn", { locations, count: allValues.length });
|
|
663
|
+
return allValues.filter((item) => {
|
|
664
|
+
const key = item.key;
|
|
665
|
+
if (key && isComKey(key)) {
|
|
666
|
+
const comKey = key;
|
|
667
|
+
return isLocKeyArrayEqual(locations, comKey.loc);
|
|
668
|
+
}
|
|
669
|
+
return false;
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
contains(query, locations) {
|
|
674
|
+
logger15.debug("contains", { query, locations });
|
|
675
|
+
const items = this.allIn(locations);
|
|
676
|
+
return items.some((item) => isQueryMatch(item, query));
|
|
677
|
+
}
|
|
678
|
+
queryIn(query, locations = []) {
|
|
679
|
+
logger15.debug("queryIn", { query, locations });
|
|
680
|
+
const items = this.allIn(locations);
|
|
681
|
+
return items.filter((item) => isQueryMatch(item, query));
|
|
682
|
+
}
|
|
683
|
+
clone() {
|
|
684
|
+
const clone = new _MemoryCacheMap(this.types);
|
|
685
|
+
for (const key of this.keys()) {
|
|
686
|
+
const value = this.get(key);
|
|
687
|
+
if (value) {
|
|
688
|
+
clone.set(key, value);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
for (const [queryHash, entry] of Object.entries(this.queryResultCache)) {
|
|
692
|
+
clone.queryResultCache[queryHash] = {
|
|
693
|
+
itemKeys: [...entry.itemKeys],
|
|
694
|
+
// Shallow copy of the array
|
|
695
|
+
expiresAt: entry.expiresAt
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
return clone;
|
|
699
|
+
}
|
|
700
|
+
// Query result caching methods implementation
|
|
701
|
+
setQueryResult(queryHash, itemKeys, ttl) {
|
|
702
|
+
logger15.trace("setQueryResult", { queryHash, itemKeys, ttl });
|
|
703
|
+
const entry = {
|
|
704
|
+
itemKeys: [...itemKeys]
|
|
705
|
+
// Create a copy to avoid external mutations
|
|
706
|
+
};
|
|
707
|
+
if (ttl) {
|
|
708
|
+
entry.expiresAt = Date.now() + ttl;
|
|
709
|
+
}
|
|
710
|
+
this.queryResultCache[queryHash] = entry;
|
|
711
|
+
}
|
|
712
|
+
getQueryResult(queryHash) {
|
|
713
|
+
logger15.trace("getQueryResult", { queryHash });
|
|
714
|
+
const entry = this.queryResultCache[queryHash];
|
|
715
|
+
if (!entry) {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
719
|
+
logger15.trace("Query result expired, removing", { queryHash, expiresAt: entry.expiresAt });
|
|
720
|
+
delete this.queryResultCache[queryHash];
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
return [...entry.itemKeys];
|
|
724
|
+
}
|
|
725
|
+
hasQueryResult(queryHash) {
|
|
726
|
+
const entry = this.queryResultCache[queryHash];
|
|
727
|
+
if (!entry) {
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
731
|
+
logger15.trace("Query result expired, removing", { queryHash, expiresAt: entry.expiresAt });
|
|
732
|
+
delete this.queryResultCache[queryHash];
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
deleteQueryResult(queryHash) {
|
|
738
|
+
logger15.trace("deleteQueryResult", { queryHash });
|
|
739
|
+
delete this.queryResultCache[queryHash];
|
|
740
|
+
}
|
|
741
|
+
invalidateItemKeys(keys) {
|
|
742
|
+
logger15.debug("invalidateItemKeys", { keys });
|
|
743
|
+
keys.forEach((key) => {
|
|
744
|
+
this.delete(key);
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
invalidateLocation(locations) {
|
|
748
|
+
logger15.debug("invalidateLocation", { locations });
|
|
749
|
+
if (locations.length === 0) {
|
|
750
|
+
const allKeys = this.keys();
|
|
751
|
+
const primaryKeys = allKeys.filter((key) => !isComKey(key));
|
|
752
|
+
this.invalidateItemKeys(primaryKeys);
|
|
753
|
+
} else {
|
|
754
|
+
const itemsInLocation = this.allIn(locations);
|
|
755
|
+
const keysToInvalidate = itemsInLocation.map((item) => item.key);
|
|
756
|
+
this.invalidateItemKeys(keysToInvalidate);
|
|
757
|
+
}
|
|
758
|
+
this.clearQueryResults();
|
|
759
|
+
}
|
|
760
|
+
clearQueryResults() {
|
|
761
|
+
logger15.trace("clearQueryResults");
|
|
762
|
+
this.queryResultCache = {};
|
|
763
|
+
}
|
|
666
764
|
};
|
|
667
|
-
|
|
668
|
-
|
|
765
|
+
|
|
766
|
+
// src/memory/EnhancedMemoryCacheMap.ts
|
|
767
|
+
import {
|
|
768
|
+
isComKey as isComKey2,
|
|
769
|
+
isQueryMatch as isQueryMatch2
|
|
770
|
+
} from "@fjell/core";
|
|
771
|
+
|
|
772
|
+
// src/eviction/EvictionStrategy.ts
|
|
773
|
+
var EvictionStrategy = class {
|
|
669
774
|
};
|
|
670
775
|
|
|
671
|
-
// src/
|
|
672
|
-
var
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
776
|
+
// src/eviction/EvictionStrategyConfig.ts
|
|
777
|
+
var DEFAULT_LFU_CONFIG = {
|
|
778
|
+
type: "lfu",
|
|
779
|
+
decayFactor: 0.1,
|
|
780
|
+
decayInterval: 6e4,
|
|
781
|
+
// 1 minute
|
|
782
|
+
sketchWidth: 1024,
|
|
783
|
+
sketchDepth: 4,
|
|
784
|
+
useProbabilisticCounting: true,
|
|
785
|
+
minFrequencyThreshold: 1
|
|
676
786
|
};
|
|
677
|
-
var
|
|
678
|
-
|
|
787
|
+
var DEFAULT_ARC_CONFIG = {
|
|
788
|
+
type: "arc",
|
|
789
|
+
maxCacheSize: 1e3,
|
|
790
|
+
frequencyThreshold: 2,
|
|
791
|
+
useEnhancedFrequency: true,
|
|
792
|
+
frequencyDecayFactor: 0.05,
|
|
793
|
+
frequencyDecayInterval: 6e5,
|
|
794
|
+
// 10 minutes
|
|
795
|
+
useFrequencyWeightedSelection: true,
|
|
796
|
+
adaptiveLearningRate: 1
|
|
797
|
+
};
|
|
798
|
+
var DEFAULT_TWO_QUEUE_CONFIG = {
|
|
799
|
+
type: "2q",
|
|
800
|
+
maxCacheSize: 1e3,
|
|
801
|
+
useFrequencyPromotion: true,
|
|
802
|
+
promotionThreshold: 2,
|
|
803
|
+
hotQueueDecayFactor: 0.05,
|
|
804
|
+
hotQueueDecayInterval: 3e5,
|
|
805
|
+
// 5 minutes
|
|
806
|
+
useFrequencyWeightedLRU: true
|
|
679
807
|
};
|
|
680
808
|
|
|
681
|
-
// src/
|
|
682
|
-
var
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
809
|
+
// src/eviction/strategies/LRUEvictionStrategy.ts
|
|
810
|
+
var LRUEvictionStrategy = class extends EvictionStrategy {
|
|
811
|
+
selectForEviction(items) {
|
|
812
|
+
if (items.size === 0) return null;
|
|
813
|
+
let oldestKey = null;
|
|
814
|
+
let oldestTime = Infinity;
|
|
815
|
+
for (const [key, metadata] of items) {
|
|
816
|
+
if (metadata.lastAccessedAt < oldestTime) {
|
|
817
|
+
oldestTime = metadata.lastAccessedAt;
|
|
818
|
+
oldestKey = key;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return oldestKey;
|
|
822
|
+
}
|
|
823
|
+
onItemAccessed(_key, metadata) {
|
|
824
|
+
metadata.lastAccessedAt = Date.now();
|
|
825
|
+
metadata.accessCount++;
|
|
826
|
+
}
|
|
827
|
+
onItemAdded(_key, metadata) {
|
|
828
|
+
const now = Date.now();
|
|
829
|
+
metadata.addedAt = now;
|
|
830
|
+
metadata.lastAccessedAt = now;
|
|
831
|
+
metadata.accessCount = 1;
|
|
832
|
+
}
|
|
833
|
+
onItemRemoved() {
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
// src/eviction/EvictionStrategyValidation.ts
|
|
838
|
+
function validateNumberRange(value, min, max, fieldName) {
|
|
839
|
+
if (typeof value !== "number" || isNaN(value) || !isFinite(value)) {
|
|
840
|
+
throw new Error(`${fieldName} must be a finite number`);
|
|
841
|
+
}
|
|
842
|
+
if (value < min || value > max) {
|
|
843
|
+
throw new Error(`${fieldName} must be between ${min} and ${max}, got ${value}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function validatePositiveInteger(value, fieldName) {
|
|
847
|
+
if (typeof value !== "number" || isNaN(value) || !isFinite(value)) {
|
|
848
|
+
throw new Error(`${fieldName} must be a finite number`);
|
|
849
|
+
}
|
|
850
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
851
|
+
throw new Error(`${fieldName} must be a positive integer, got ${value}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function sanitizeLFUConfig(config) {
|
|
855
|
+
const sanitized = { ...config };
|
|
856
|
+
if (typeof sanitized.decayFactor === "number") {
|
|
857
|
+
if (sanitized.decayFactor < 0) {
|
|
858
|
+
console.warn(`decayFactor must be between 0 and 1, got ${sanitized.decayFactor}. Correcting to 0.`);
|
|
859
|
+
sanitized.decayFactor = 0;
|
|
860
|
+
} else if (sanitized.decayFactor > 1) {
|
|
861
|
+
console.warn(`decayFactor must be between 0 and 1, got ${sanitized.decayFactor}. Correcting to 1.`);
|
|
862
|
+
sanitized.decayFactor = 1;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (typeof sanitized.decayInterval === "number" && sanitized.decayInterval <= 0) {
|
|
866
|
+
console.warn(`decayInterval must be positive, got ${sanitized.decayInterval}. Correcting to 300000.`);
|
|
867
|
+
sanitized.decayInterval = 3e5;
|
|
868
|
+
}
|
|
869
|
+
if (typeof sanitized.sketchWidth === "number") {
|
|
870
|
+
if (sanitized.sketchWidth <= 0) {
|
|
871
|
+
console.warn(`sketchWidth must be positive, got ${sanitized.sketchWidth}. Correcting to 1024.`);
|
|
872
|
+
sanitized.sketchWidth = 1024;
|
|
873
|
+
} else if (sanitized.sketchWidth < 16) {
|
|
874
|
+
console.warn(`sketchWidth should be at least 16 for optimal performance, got ${sanitized.sketchWidth}. Correcting to 16.`);
|
|
875
|
+
sanitized.sketchWidth = 16;
|
|
876
|
+
} else if (sanitized.sketchWidth > 65536) {
|
|
877
|
+
console.warn(`sketchWidth should not exceed 65536 for optimal performance, got ${sanitized.sketchWidth}. Correcting to 65536.`);
|
|
878
|
+
sanitized.sketchWidth = 65536;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (typeof sanitized.sketchDepth === "number") {
|
|
882
|
+
if (sanitized.sketchDepth <= 0) {
|
|
883
|
+
console.warn(`sketchDepth must be positive, got ${sanitized.sketchDepth}. Correcting to 4.`);
|
|
884
|
+
sanitized.sketchDepth = 4;
|
|
885
|
+
} else if (sanitized.sketchDepth < 1) {
|
|
886
|
+
console.warn(`sketchDepth should be at least 1 for optimal accuracy, got ${sanitized.sketchDepth}. Correcting to 1.`);
|
|
887
|
+
sanitized.sketchDepth = 1;
|
|
888
|
+
} else if (sanitized.sketchDepth > 16) {
|
|
889
|
+
console.warn(`sketchDepth should not exceed 16 for optimal accuracy, got ${sanitized.sketchDepth}. Correcting to 16.`);
|
|
890
|
+
sanitized.sketchDepth = 16;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (typeof sanitized.minFrequencyThreshold === "number" && sanitized.minFrequencyThreshold <= 0) {
|
|
894
|
+
console.warn(`minFrequencyThreshold must be positive, got ${sanitized.minFrequencyThreshold}. Correcting to 1.`);
|
|
895
|
+
sanitized.minFrequencyThreshold = 1;
|
|
896
|
+
}
|
|
897
|
+
return sanitized;
|
|
898
|
+
}
|
|
899
|
+
function validateLFUConfig(config) {
|
|
900
|
+
if (typeof config.decayFactor === "number") {
|
|
901
|
+
validateNumberRange(config.decayFactor, 0, 1, "decayFactor");
|
|
902
|
+
}
|
|
903
|
+
if (typeof config.decayInterval === "number") {
|
|
904
|
+
validatePositiveInteger(config.decayInterval, "decayInterval");
|
|
905
|
+
}
|
|
906
|
+
if (typeof config.sketchWidth === "number") {
|
|
907
|
+
validatePositiveInteger(config.sketchWidth, "sketchWidth");
|
|
908
|
+
if (config.sketchWidth < 16 || config.sketchWidth > 65536) {
|
|
909
|
+
throw new Error(`sketchWidth must be between 16 and 65536, got ${config.sketchWidth}`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (typeof config.sketchDepth === "number") {
|
|
913
|
+
validatePositiveInteger(config.sketchDepth, "sketchDepth");
|
|
914
|
+
if (config.sketchDepth < 1 || config.sketchDepth > 16) {
|
|
915
|
+
throw new Error(`sketchDepth must be between 1 and 16, got ${config.sketchDepth}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (typeof config.minFrequencyThreshold === "number") {
|
|
919
|
+
validatePositiveInteger(config.minFrequencyThreshold, "minFrequencyThreshold");
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function sanitizeARCConfig(config) {
|
|
923
|
+
const sanitized = { ...config };
|
|
924
|
+
if (typeof sanitized.maxCacheSize === "number" && sanitized.maxCacheSize <= 0) {
|
|
925
|
+
console.warn(`maxCacheSize must be positive, got ${sanitized.maxCacheSize}. Correcting to 1000.`);
|
|
926
|
+
sanitized.maxCacheSize = 1e3;
|
|
927
|
+
}
|
|
928
|
+
if (typeof sanitized.frequencyThreshold === "number" && sanitized.frequencyThreshold <= 0) {
|
|
929
|
+
console.warn(`frequencyThreshold must be positive, got ${sanitized.frequencyThreshold}. Correcting to 2.`);
|
|
930
|
+
sanitized.frequencyThreshold = 2;
|
|
931
|
+
}
|
|
932
|
+
if (typeof sanitized.frequencyDecayFactor === "number") {
|
|
933
|
+
if (sanitized.frequencyDecayFactor < 0) {
|
|
934
|
+
console.warn(`frequencyDecayFactor must be between 0 and 1, got ${sanitized.frequencyDecayFactor}. Correcting to 0.`);
|
|
935
|
+
sanitized.frequencyDecayFactor = 0;
|
|
936
|
+
} else if (sanitized.frequencyDecayFactor > 1) {
|
|
937
|
+
console.warn(`frequencyDecayFactor must be between 0 and 1, got ${sanitized.frequencyDecayFactor}. Correcting to 1.`);
|
|
938
|
+
sanitized.frequencyDecayFactor = 1;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (typeof sanitized.frequencyDecayInterval === "number" && sanitized.frequencyDecayInterval <= 0) {
|
|
942
|
+
console.warn(`frequencyDecayInterval must be positive, got ${sanitized.frequencyDecayInterval}. Correcting to 60000.`);
|
|
943
|
+
sanitized.frequencyDecayInterval = 6e4;
|
|
944
|
+
}
|
|
945
|
+
if (typeof sanitized.adaptiveLearningRate === "number") {
|
|
946
|
+
if (sanitized.adaptiveLearningRate < 0) {
|
|
947
|
+
console.warn(`adaptiveLearningRate must be between 0 and 10, got ${sanitized.adaptiveLearningRate}. Correcting to 0.`);
|
|
948
|
+
sanitized.adaptiveLearningRate = 0;
|
|
949
|
+
} else if (sanitized.adaptiveLearningRate > 10) {
|
|
950
|
+
console.warn(`adaptiveLearningRate must be between 0 and 10, got ${sanitized.adaptiveLearningRate}. Correcting to 10.`);
|
|
951
|
+
sanitized.adaptiveLearningRate = 10;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return sanitized;
|
|
955
|
+
}
|
|
956
|
+
function validateARCConfig(config) {
|
|
957
|
+
if (typeof config.maxCacheSize === "number") {
|
|
958
|
+
validatePositiveInteger(config.maxCacheSize, "maxCacheSize");
|
|
959
|
+
}
|
|
960
|
+
if (typeof config.frequencyThreshold === "number") {
|
|
961
|
+
validatePositiveInteger(config.frequencyThreshold, "frequencyThreshold");
|
|
962
|
+
}
|
|
963
|
+
if (typeof config.frequencyDecayFactor === "number") {
|
|
964
|
+
validateNumberRange(config.frequencyDecayFactor, 0, 1, "frequencyDecayFactor");
|
|
965
|
+
}
|
|
966
|
+
if (typeof config.frequencyDecayInterval === "number") {
|
|
967
|
+
validatePositiveInteger(config.frequencyDecayInterval, "frequencyDecayInterval");
|
|
968
|
+
}
|
|
969
|
+
if (typeof config.adaptiveLearningRate === "number") {
|
|
970
|
+
validateNumberRange(config.adaptiveLearningRate, 0, 10, "adaptiveLearningRate");
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function sanitizeTwoQueueConfig(config) {
|
|
974
|
+
const sanitized = { ...config };
|
|
975
|
+
if (typeof sanitized.maxCacheSize === "number" && sanitized.maxCacheSize <= 0) {
|
|
976
|
+
console.warn(`maxCacheSize must be positive, got ${sanitized.maxCacheSize}. Correcting to 1000.`);
|
|
977
|
+
sanitized.maxCacheSize = 1e3;
|
|
978
|
+
}
|
|
979
|
+
if (typeof sanitized.promotionThreshold === "number" && sanitized.promotionThreshold <= 0) {
|
|
980
|
+
console.warn(`promotionThreshold must be positive, got ${sanitized.promotionThreshold}. Correcting to 2.`);
|
|
981
|
+
sanitized.promotionThreshold = 2;
|
|
982
|
+
}
|
|
983
|
+
if (typeof sanitized.hotQueueDecayFactor === "number") {
|
|
984
|
+
if (sanitized.hotQueueDecayFactor < 0) {
|
|
985
|
+
console.warn(`hotQueueDecayFactor must be between 0 and 1, got ${sanitized.hotQueueDecayFactor}. Correcting to 0.`);
|
|
986
|
+
sanitized.hotQueueDecayFactor = 0;
|
|
987
|
+
} else if (sanitized.hotQueueDecayFactor > 1) {
|
|
988
|
+
console.warn(`hotQueueDecayFactor must be between 0 and 1, got ${sanitized.hotQueueDecayFactor}. Correcting to 1.`);
|
|
989
|
+
sanitized.hotQueueDecayFactor = 1;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (typeof sanitized.hotQueueDecayInterval === "number" && sanitized.hotQueueDecayInterval <= 0) {
|
|
993
|
+
console.warn(`hotQueueDecayInterval must be positive, got ${sanitized.hotQueueDecayInterval}. Correcting to 300000.`);
|
|
994
|
+
sanitized.hotQueueDecayInterval = 3e5;
|
|
995
|
+
}
|
|
996
|
+
return sanitized;
|
|
997
|
+
}
|
|
998
|
+
function validateTwoQueueConfig(config) {
|
|
999
|
+
if (typeof config.maxCacheSize === "number") {
|
|
1000
|
+
validatePositiveInteger(config.maxCacheSize, "maxCacheSize");
|
|
1001
|
+
}
|
|
1002
|
+
if (typeof config.promotionThreshold === "number") {
|
|
1003
|
+
validatePositiveInteger(config.promotionThreshold, "promotionThreshold");
|
|
1004
|
+
}
|
|
1005
|
+
if (typeof config.hotQueueDecayFactor === "number") {
|
|
1006
|
+
validateNumberRange(config.hotQueueDecayFactor, 0, 1, "hotQueueDecayFactor");
|
|
1007
|
+
}
|
|
1008
|
+
if (typeof config.hotQueueDecayInterval === "number") {
|
|
1009
|
+
validatePositiveInteger(config.hotQueueDecayInterval, "hotQueueDecayInterval");
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function validateEvictionStrategyConfig(config) {
|
|
1013
|
+
if (!config || typeof config !== "object") {
|
|
1014
|
+
throw new Error("Configuration must be a non-null object");
|
|
1015
|
+
}
|
|
1016
|
+
if (!config.type) {
|
|
1017
|
+
throw new Error("Configuration must specify a type");
|
|
1018
|
+
}
|
|
1019
|
+
const validTypes = ["lfu", "lru", "fifo", "mru", "random", "arc", "2q"];
|
|
1020
|
+
if (!validTypes.includes(config.type)) {
|
|
1021
|
+
throw new Error(`Invalid eviction strategy type: ${config.type}. Must be one of: ${validTypes.join(", ")}`);
|
|
1022
|
+
}
|
|
1023
|
+
switch (config.type) {
|
|
1024
|
+
case "lfu":
|
|
1025
|
+
validateLFUConfig(config);
|
|
1026
|
+
break;
|
|
1027
|
+
case "arc":
|
|
1028
|
+
validateARCConfig(config);
|
|
1029
|
+
break;
|
|
1030
|
+
case "2q":
|
|
1031
|
+
validateTwoQueueConfig(config);
|
|
1032
|
+
break;
|
|
1033
|
+
case "lru":
|
|
1034
|
+
case "fifo":
|
|
1035
|
+
case "mru":
|
|
1036
|
+
case "random":
|
|
1037
|
+
break;
|
|
1038
|
+
default:
|
|
1039
|
+
throw new Error(`Unsupported eviction strategy type: ${config.type}`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
function sanitizeConfigByType(config) {
|
|
1043
|
+
if (!config.type) {
|
|
1044
|
+
return config;
|
|
1045
|
+
}
|
|
1046
|
+
switch (config.type) {
|
|
1047
|
+
case "lfu":
|
|
1048
|
+
return sanitizeLFUConfig(config);
|
|
1049
|
+
case "arc":
|
|
1050
|
+
return sanitizeARCConfig(config);
|
|
1051
|
+
case "2q":
|
|
1052
|
+
return sanitizeTwoQueueConfig(config);
|
|
1053
|
+
case "lru":
|
|
1054
|
+
case "fifo":
|
|
1055
|
+
case "mru":
|
|
1056
|
+
case "random":
|
|
1057
|
+
return config;
|
|
1058
|
+
default:
|
|
1059
|
+
return config;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function createValidatedConfig(baseConfig, userConfig) {
|
|
1063
|
+
const sanitizedUserConfig = sanitizeConfigByType(userConfig);
|
|
1064
|
+
const mergedConfig = { ...baseConfig, ...sanitizedUserConfig };
|
|
1065
|
+
validateEvictionStrategyConfig(mergedConfig);
|
|
1066
|
+
return mergedConfig;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/eviction/strategies/LFUEvictionStrategy.ts
|
|
1070
|
+
function fnv1aHash(key, seed) {
|
|
1071
|
+
const FNV_OFFSET_BASIS = 2166136261;
|
|
1072
|
+
const FNV_PRIME = 16777619;
|
|
1073
|
+
let hash = (FNV_OFFSET_BASIS ^ seed) >>> 0;
|
|
1074
|
+
for (let i = 0; i < key.length; i++) {
|
|
1075
|
+
hash ^= key.charCodeAt(i);
|
|
1076
|
+
hash = hash * FNV_PRIME >>> 0;
|
|
1077
|
+
}
|
|
1078
|
+
hash ^= hash >>> 16;
|
|
1079
|
+
hash = hash * 2246822507 >>> 0;
|
|
1080
|
+
hash ^= hash >>> 13;
|
|
1081
|
+
hash = hash * 3266489909 >>> 0;
|
|
1082
|
+
hash ^= hash >>> 16;
|
|
1083
|
+
return hash >>> 0;
|
|
1084
|
+
}
|
|
1085
|
+
var CountMinSketch = class {
|
|
1086
|
+
sketches;
|
|
1087
|
+
width;
|
|
1088
|
+
depth;
|
|
1089
|
+
seeds;
|
|
1090
|
+
constructor(width = 1024, depth = 4) {
|
|
1091
|
+
this.width = width;
|
|
1092
|
+
this.depth = depth;
|
|
1093
|
+
this.sketches = Array(depth).fill(null).map(() => new Array(width).fill(0));
|
|
1094
|
+
this.seeds = Array(depth).fill(null).map(() => Math.floor(Math.random() * 1e6));
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Check if a number is a power of 2 for optimized bit masking
|
|
1098
|
+
*/
|
|
1099
|
+
isPowerOfTwo(n) {
|
|
1100
|
+
return n > 0 && (n & n - 1) === 0;
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Increment the frequency count for a key
|
|
1104
|
+
*/
|
|
1105
|
+
increment(key) {
|
|
1106
|
+
for (let i = 0; i < this.depth; i++) {
|
|
1107
|
+
const hash = fnv1aHash(key, this.seeds[i]);
|
|
1108
|
+
const index = this.isPowerOfTwo(this.width) ? hash & this.width - 1 : hash % this.width;
|
|
1109
|
+
this.sketches[i][index]++;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Estimate the frequency count for a key
|
|
1114
|
+
*/
|
|
1115
|
+
estimate(key) {
|
|
1116
|
+
let minCount = Infinity;
|
|
1117
|
+
for (let i = 0; i < this.depth; i++) {
|
|
1118
|
+
const hash = fnv1aHash(key, this.seeds[i]);
|
|
1119
|
+
const index = this.isPowerOfTwo(this.width) ? hash & this.width - 1 : hash % this.width;
|
|
1120
|
+
minCount = Math.min(minCount, this.sketches[i][index]);
|
|
1121
|
+
}
|
|
1122
|
+
return minCount === Infinity ? 0 : minCount;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Apply decay to all frequencies
|
|
1126
|
+
*/
|
|
1127
|
+
decay(factor) {
|
|
1128
|
+
for (let i = 0; i < this.depth; i++) {
|
|
1129
|
+
for (let j = 0; j < this.width; j++) {
|
|
1130
|
+
this.sketches[i][j] = Math.floor(this.sketches[i][j] * (1 - factor));
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Reset all frequencies to zero
|
|
1136
|
+
*/
|
|
1137
|
+
reset() {
|
|
1138
|
+
for (let i = 0; i < this.depth; i++) {
|
|
1139
|
+
for (let j = 0; j < this.width; j++) {
|
|
1140
|
+
this.sketches[i][j] = 0;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
var LFUEvictionStrategy = class extends EvictionStrategy {
|
|
1146
|
+
config;
|
|
1147
|
+
sketch;
|
|
1148
|
+
lastDecayTime;
|
|
1149
|
+
constructor(config = {}) {
|
|
1150
|
+
super();
|
|
1151
|
+
const defaultBackwardsCompatible = {
|
|
1152
|
+
useProbabilisticCounting: false,
|
|
1153
|
+
decayFactor: 0,
|
|
1154
|
+
decayInterval: Number.MAX_SAFE_INTEGER
|
|
695
1155
|
};
|
|
696
|
-
|
|
1156
|
+
const baseConfig = { ...DEFAULT_LFU_CONFIG, ...defaultBackwardsCompatible };
|
|
1157
|
+
this.config = createValidatedConfig(baseConfig, config);
|
|
1158
|
+
this.sketch = this.config.useProbabilisticCounting ? new CountMinSketch(this.config.sketchWidth, this.config.sketchDepth) : null;
|
|
1159
|
+
this.lastDecayTime = Date.now();
|
|
1160
|
+
}
|
|
1161
|
+
selectForEviction(items) {
|
|
1162
|
+
if (items.size === 0) return null;
|
|
1163
|
+
this.applyPeriodicDecay();
|
|
1164
|
+
let leastUsedKey = null;
|
|
1165
|
+
let lowestFrequency = Infinity;
|
|
1166
|
+
let oldestAccessTime = Infinity;
|
|
1167
|
+
for (const [key, metadata] of items) {
|
|
1168
|
+
const frequency = this.getEffectiveFrequency(key, metadata);
|
|
1169
|
+
if (frequency < lowestFrequency || frequency === lowestFrequency && metadata.lastAccessedAt < oldestAccessTime) {
|
|
1170
|
+
lowestFrequency = frequency;
|
|
1171
|
+
oldestAccessTime = metadata.lastAccessedAt;
|
|
1172
|
+
leastUsedKey = key;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return leastUsedKey;
|
|
1176
|
+
}
|
|
1177
|
+
onItemAccessed(key, metadata) {
|
|
1178
|
+
const now = Date.now();
|
|
1179
|
+
metadata.lastAccessedAt = now;
|
|
1180
|
+
metadata.accessCount++;
|
|
1181
|
+
if (this.sketch) {
|
|
1182
|
+
this.sketch.increment(key);
|
|
1183
|
+
metadata.rawFrequency = this.sketch.estimate(key);
|
|
1184
|
+
} else {
|
|
1185
|
+
metadata.rawFrequency = metadata.accessCount;
|
|
1186
|
+
}
|
|
1187
|
+
if ((this.config.decayFactor ?? 0) > 0) {
|
|
1188
|
+
metadata.frequencyScore = this.calculateFrequencyScore(metadata, now);
|
|
1189
|
+
metadata.lastFrequencyUpdate = now;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
onItemAdded(key, metadata) {
|
|
1193
|
+
const now = Date.now();
|
|
1194
|
+
metadata.addedAt = now;
|
|
1195
|
+
metadata.lastAccessedAt = now;
|
|
1196
|
+
metadata.accessCount = 1;
|
|
1197
|
+
metadata.rawFrequency = 1;
|
|
1198
|
+
if ((this.config.decayFactor ?? 0) > 0) {
|
|
1199
|
+
metadata.frequencyScore = 1;
|
|
1200
|
+
metadata.lastFrequencyUpdate = now;
|
|
1201
|
+
}
|
|
1202
|
+
if (this.sketch) {
|
|
1203
|
+
this.sketch.increment(key);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
onItemRemoved() {
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Get the effective frequency for an item, applying real-time decay if needed
|
|
1210
|
+
*/
|
|
1211
|
+
getEffectiveFrequency(_key, metadata) {
|
|
1212
|
+
if ((this.config.decayFactor ?? 0) === 0) {
|
|
1213
|
+
return metadata.rawFrequency || metadata.accessCount;
|
|
1214
|
+
}
|
|
1215
|
+
const now = Date.now();
|
|
1216
|
+
if (typeof metadata.frequencyScore === "number" && typeof metadata.lastFrequencyUpdate === "number") {
|
|
1217
|
+
const timeSinceUpdate = now - metadata.lastFrequencyUpdate;
|
|
1218
|
+
const decayAmount = timeSinceUpdate / (this.config.decayInterval ?? 6e4) * (this.config.decayFactor ?? 0.1);
|
|
1219
|
+
return Math.max(this.config.minFrequencyThreshold ?? 1, metadata.frequencyScore * (1 - decayAmount));
|
|
1220
|
+
}
|
|
1221
|
+
return metadata.rawFrequency || metadata.accessCount;
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Calculate frequency score with decay applied
|
|
1225
|
+
*/
|
|
1226
|
+
calculateFrequencyScore(metadata, currentTime) {
|
|
1227
|
+
const rawFreq = metadata.rawFrequency || metadata.accessCount;
|
|
1228
|
+
if (typeof metadata.lastFrequencyUpdate !== "number") {
|
|
1229
|
+
return rawFreq;
|
|
1230
|
+
}
|
|
1231
|
+
const timeSinceUpdate = currentTime - metadata.lastFrequencyUpdate;
|
|
1232
|
+
const decayAmount = timeSinceUpdate / (this.config.decayInterval ?? 6e4) * (this.config.decayFactor ?? 0.1);
|
|
1233
|
+
const previousScore = metadata.frequencyScore || rawFreq;
|
|
1234
|
+
const decayedScore = previousScore * (1 - decayAmount);
|
|
1235
|
+
return Math.max(this.config.minFrequencyThreshold ?? 1, decayedScore + 1);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Apply periodic decay to the frequency sketch and metadata
|
|
1239
|
+
*/
|
|
1240
|
+
applyPeriodicDecay() {
|
|
1241
|
+
if ((this.config.decayFactor ?? 0) === 0) return;
|
|
1242
|
+
const now = Date.now();
|
|
1243
|
+
const timeSinceDecay = now - this.lastDecayTime;
|
|
1244
|
+
if (timeSinceDecay >= (this.config.decayInterval ?? 6e4)) {
|
|
1245
|
+
if (this.sketch) {
|
|
1246
|
+
this.sketch.decay(this.config.decayFactor ?? 0.1);
|
|
1247
|
+
}
|
|
1248
|
+
this.lastDecayTime = now;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Get configuration for this strategy
|
|
1253
|
+
*/
|
|
1254
|
+
getConfig() {
|
|
1255
|
+
return { ...this.config };
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Reset frequency tracking (useful for testing or cache clearing)
|
|
1259
|
+
*/
|
|
1260
|
+
reset() {
|
|
1261
|
+
if (this.sketch) {
|
|
1262
|
+
this.sketch.reset();
|
|
1263
|
+
}
|
|
1264
|
+
this.lastDecayTime = Date.now();
|
|
1265
|
+
}
|
|
697
1266
|
};
|
|
698
1267
|
|
|
699
|
-
// src/
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
return
|
|
712
|
-
}
|
|
1268
|
+
// src/eviction/strategies/FIFOEvictionStrategy.ts
|
|
1269
|
+
var FIFOEvictionStrategy = class extends EvictionStrategy {
|
|
1270
|
+
selectForEviction(items) {
|
|
1271
|
+
if (items.size === 0) return null;
|
|
1272
|
+
let oldestKey = null;
|
|
1273
|
+
let oldestTime = Infinity;
|
|
1274
|
+
for (const [key, metadata] of items) {
|
|
1275
|
+
if (metadata.addedAt < oldestTime) {
|
|
1276
|
+
oldestTime = metadata.addedAt;
|
|
1277
|
+
oldestKey = key;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return oldestKey;
|
|
1281
|
+
}
|
|
1282
|
+
onItemAccessed(_key, metadata) {
|
|
1283
|
+
metadata.lastAccessedAt = Date.now();
|
|
1284
|
+
metadata.accessCount++;
|
|
1285
|
+
}
|
|
1286
|
+
onItemAdded(_key, metadata) {
|
|
1287
|
+
const now = Date.now();
|
|
1288
|
+
metadata.addedAt = now;
|
|
1289
|
+
metadata.lastAccessedAt = now;
|
|
1290
|
+
metadata.accessCount = 1;
|
|
1291
|
+
}
|
|
1292
|
+
onItemRemoved() {
|
|
1293
|
+
}
|
|
713
1294
|
};
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1295
|
+
|
|
1296
|
+
// src/eviction/strategies/MRUEvictionStrategy.ts
|
|
1297
|
+
var MRUEvictionStrategy = class extends EvictionStrategy {
|
|
1298
|
+
selectForEviction(items) {
|
|
1299
|
+
if (items.size === 0) return null;
|
|
1300
|
+
let newestKey = null;
|
|
1301
|
+
let newestTime = -1;
|
|
1302
|
+
for (const [key, metadata] of items) {
|
|
1303
|
+
if (metadata.lastAccessedAt > newestTime) {
|
|
1304
|
+
newestTime = metadata.lastAccessedAt;
|
|
1305
|
+
newestKey = key;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return newestKey;
|
|
1309
|
+
}
|
|
1310
|
+
onItemAccessed(_key, metadata) {
|
|
1311
|
+
metadata.lastAccessedAt = Date.now();
|
|
1312
|
+
metadata.accessCount++;
|
|
1313
|
+
}
|
|
1314
|
+
onItemAdded(_key, metadata) {
|
|
1315
|
+
const now = Date.now();
|
|
1316
|
+
metadata.addedAt = now;
|
|
1317
|
+
metadata.lastAccessedAt = now;
|
|
1318
|
+
metadata.accessCount = 1;
|
|
1319
|
+
}
|
|
1320
|
+
onItemRemoved() {
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
// src/eviction/strategies/RandomEvictionStrategy.ts
|
|
1325
|
+
var RandomEvictionStrategy = class extends EvictionStrategy {
|
|
1326
|
+
selectForEviction(items) {
|
|
1327
|
+
if (items.size === 0) return null;
|
|
1328
|
+
const keys = Array.from(items.keys());
|
|
1329
|
+
const randomIndex = Math.floor(Math.random() * keys.length);
|
|
1330
|
+
return keys[randomIndex];
|
|
1331
|
+
}
|
|
1332
|
+
onItemAccessed(_key, metadata) {
|
|
1333
|
+
metadata.lastAccessedAt = Date.now();
|
|
1334
|
+
metadata.accessCount++;
|
|
1335
|
+
}
|
|
1336
|
+
onItemAdded(_key, metadata) {
|
|
1337
|
+
const now = Date.now();
|
|
1338
|
+
metadata.addedAt = now;
|
|
1339
|
+
metadata.lastAccessedAt = now;
|
|
1340
|
+
metadata.accessCount = 1;
|
|
1341
|
+
}
|
|
1342
|
+
onItemRemoved() {
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
// src/eviction/strategies/ARCEvictionStrategy.ts
|
|
1347
|
+
var ARCEvictionStrategy = class extends EvictionStrategy {
|
|
1348
|
+
recentGhosts = /* @__PURE__ */ new Set();
|
|
1349
|
+
// T1 ghost entries
|
|
1350
|
+
frequentGhosts = /* @__PURE__ */ new Set();
|
|
1351
|
+
// T2 ghost entries
|
|
1352
|
+
targetRecentSize = 0;
|
|
1353
|
+
// Target size for T1 (recent entries)
|
|
1354
|
+
config;
|
|
1355
|
+
maxGhostSize;
|
|
1356
|
+
lastDecayTime;
|
|
1357
|
+
constructor(maxCacheSize = 1e3, config = {}) {
|
|
1358
|
+
super();
|
|
1359
|
+
const baseConfig = { ...DEFAULT_ARC_CONFIG, maxCacheSize };
|
|
1360
|
+
this.config = createValidatedConfig(baseConfig, config);
|
|
1361
|
+
this.maxGhostSize = this.config.maxCacheSize;
|
|
1362
|
+
this.lastDecayTime = Date.now();
|
|
1363
|
+
}
|
|
1364
|
+
selectForEviction(items) {
|
|
1365
|
+
if (items.size === 0) return null;
|
|
1366
|
+
this.applyPeriodicDecay(items);
|
|
1367
|
+
const recentItems = /* @__PURE__ */ new Map();
|
|
1368
|
+
const frequentItems = /* @__PURE__ */ new Map();
|
|
1369
|
+
for (const [key, metadata] of items) {
|
|
1370
|
+
if (this.isFrequentItem(metadata)) {
|
|
1371
|
+
frequentItems.set(key, metadata);
|
|
1372
|
+
} else {
|
|
1373
|
+
recentItems.set(key, metadata);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (recentItems.size > this.targetRecentSize && recentItems.size > 0) {
|
|
1377
|
+
return this.config.useFrequencyWeightedSelection ? this.selectFrequencyWeightedFromItems(recentItems, "recent") : this.selectLRUFromItems(recentItems);
|
|
1378
|
+
} else if (frequentItems.size > 0) {
|
|
1379
|
+
return this.config.useFrequencyWeightedSelection ? this.selectFrequencyWeightedFromItems(frequentItems, "frequent") : this.selectLRUFromItems(frequentItems);
|
|
1380
|
+
}
|
|
1381
|
+
return this.config.useFrequencyWeightedSelection ? this.selectFrequencyWeightedFromItems(items, "fallback") : this.selectLRUFromItems(items);
|
|
1382
|
+
}
|
|
1383
|
+
selectLRUFromItems(items) {
|
|
1384
|
+
let oldestKey = null;
|
|
1385
|
+
let oldestTime = Infinity;
|
|
1386
|
+
for (const [key, metadata] of items) {
|
|
1387
|
+
if (metadata.lastAccessedAt < oldestTime) {
|
|
1388
|
+
oldestTime = metadata.lastAccessedAt;
|
|
1389
|
+
oldestKey = key;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
return oldestKey;
|
|
1393
|
+
}
|
|
1394
|
+
onItemAccessed(key, metadata) {
|
|
1395
|
+
const now = Date.now();
|
|
1396
|
+
metadata.lastAccessedAt = now;
|
|
1397
|
+
metadata.accessCount++;
|
|
1398
|
+
metadata.rawFrequency = metadata.accessCount;
|
|
1399
|
+
if (this.config.useEnhancedFrequency && (this.config.frequencyDecayFactor ?? 0) > 0) {
|
|
1400
|
+
metadata.frequencyScore = this.calculateFrequencyScore(metadata, now);
|
|
1401
|
+
metadata.lastFrequencyUpdate = now;
|
|
1402
|
+
}
|
|
1403
|
+
const learningRate = this.config.adaptiveLearningRate ?? 1;
|
|
1404
|
+
if (this.recentGhosts.has(key)) {
|
|
1405
|
+
const adjustment = Math.ceil(learningRate);
|
|
1406
|
+
this.targetRecentSize = Math.min(this.targetRecentSize + adjustment, this.maxGhostSize);
|
|
1407
|
+
this.recentGhosts.delete(key);
|
|
1408
|
+
} else if (this.frequentGhosts.has(key)) {
|
|
1409
|
+
const adjustment = Math.ceil(learningRate);
|
|
1410
|
+
this.targetRecentSize = Math.max(this.targetRecentSize - adjustment, 0);
|
|
1411
|
+
this.frequentGhosts.delete(key);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
onItemAdded(key, metadata) {
|
|
1415
|
+
const now = Date.now();
|
|
1416
|
+
metadata.addedAt = now;
|
|
1417
|
+
metadata.lastAccessedAt = now;
|
|
1418
|
+
metadata.accessCount = 1;
|
|
1419
|
+
metadata.rawFrequency = 1;
|
|
1420
|
+
if (this.config.useEnhancedFrequency && (this.config.frequencyDecayFactor ?? 0) > 0) {
|
|
1421
|
+
metadata.frequencyScore = 1;
|
|
1422
|
+
metadata.lastFrequencyUpdate = now;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
onItemRemoved(key) {
|
|
1426
|
+
this.addToRecentGhosts(key);
|
|
1427
|
+
this.cleanupGhostLists();
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Add key to recent ghost list with proper size management
|
|
1431
|
+
*/
|
|
1432
|
+
addToRecentGhosts(key) {
|
|
1433
|
+
this.frequentGhosts.delete(key);
|
|
1434
|
+
this.recentGhosts.add(key);
|
|
1435
|
+
while (this.recentGhosts.size > this.maxGhostSize) {
|
|
1436
|
+
const firstKey = this.recentGhosts.values().next().value;
|
|
1437
|
+
if (firstKey) {
|
|
1438
|
+
this.recentGhosts.delete(firstKey);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Add key to frequent ghost list with proper size management
|
|
1444
|
+
*/
|
|
1445
|
+
addToFrequentGhosts(key) {
|
|
1446
|
+
this.recentGhosts.delete(key);
|
|
1447
|
+
this.frequentGhosts.add(key);
|
|
1448
|
+
while (this.frequentGhosts.size > this.maxGhostSize) {
|
|
1449
|
+
const firstKey = this.frequentGhosts.values().next().value;
|
|
1450
|
+
if (firstKey) {
|
|
1451
|
+
this.frequentGhosts.delete(firstKey);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Cleanup ghost lists to prevent memory leaks
|
|
1457
|
+
*/
|
|
1458
|
+
cleanupGhostLists() {
|
|
1459
|
+
while (this.recentGhosts.size > this.maxGhostSize) {
|
|
1460
|
+
const firstKey = this.recentGhosts.values().next().value;
|
|
1461
|
+
if (firstKey) {
|
|
1462
|
+
this.recentGhosts.delete(firstKey);
|
|
1463
|
+
} else {
|
|
1464
|
+
break;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
while (this.frequentGhosts.size > this.maxGhostSize) {
|
|
1468
|
+
const firstKey = this.frequentGhosts.values().next().value;
|
|
1469
|
+
if (firstKey) {
|
|
1470
|
+
this.frequentGhosts.delete(firstKey);
|
|
1471
|
+
} else {
|
|
1472
|
+
break;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Determine if an item should be classified as frequent vs recent
|
|
1478
|
+
*/
|
|
1479
|
+
isFrequentItem(metadata) {
|
|
1480
|
+
if (!this.config.useEnhancedFrequency) {
|
|
1481
|
+
return metadata.accessCount > 1;
|
|
1482
|
+
}
|
|
1483
|
+
const frequency = this.getEffectiveFrequency(metadata);
|
|
1484
|
+
return frequency >= (this.config.frequencyThreshold ?? 2);
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Get effective frequency for an item, applying decay if enabled
|
|
1488
|
+
*/
|
|
1489
|
+
getEffectiveFrequency(metadata) {
|
|
1490
|
+
if (!this.config.useEnhancedFrequency || (this.config.frequencyDecayFactor ?? 0) === 0) {
|
|
1491
|
+
return metadata.rawFrequency || metadata.accessCount;
|
|
1492
|
+
}
|
|
1493
|
+
const now = Date.now();
|
|
1494
|
+
if (typeof metadata.frequencyScore === "number" && typeof metadata.lastFrequencyUpdate === "number") {
|
|
1495
|
+
const timeSinceUpdate = now - metadata.lastFrequencyUpdate;
|
|
1496
|
+
const decayAmount = timeSinceUpdate / (this.config.frequencyDecayInterval ?? 6e5) * (this.config.frequencyDecayFactor ?? 0.05);
|
|
1497
|
+
return Math.max(1, metadata.frequencyScore * (1 - decayAmount));
|
|
1498
|
+
}
|
|
1499
|
+
return metadata.rawFrequency || metadata.accessCount;
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Calculate frequency score with decay applied
|
|
1503
|
+
*/
|
|
1504
|
+
calculateFrequencyScore(metadata, currentTime) {
|
|
1505
|
+
const rawFreq = metadata.rawFrequency || metadata.accessCount;
|
|
1506
|
+
if (typeof metadata.lastFrequencyUpdate !== "number") {
|
|
1507
|
+
return rawFreq;
|
|
1508
|
+
}
|
|
1509
|
+
const timeSinceUpdate = currentTime - metadata.lastFrequencyUpdate;
|
|
1510
|
+
const decayAmount = timeSinceUpdate / (this.config.frequencyDecayInterval ?? 6e5) * (this.config.frequencyDecayFactor ?? 0.05);
|
|
1511
|
+
const previousScore = metadata.frequencyScore || rawFreq;
|
|
1512
|
+
const decayedScore = previousScore * (1 - decayAmount);
|
|
1513
|
+
return Math.max(1, decayedScore + 1);
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Select eviction candidate using frequency-weighted approach
|
|
1517
|
+
*/
|
|
1518
|
+
selectFrequencyWeightedFromItems(items, context) {
|
|
1519
|
+
let bestKey = null;
|
|
1520
|
+
let bestScore = Infinity;
|
|
1521
|
+
for (const [key, metadata] of items) {
|
|
1522
|
+
const frequency = this.getEffectiveFrequency(metadata);
|
|
1523
|
+
const timeFactor = Date.now() - metadata.lastAccessedAt;
|
|
1524
|
+
let score;
|
|
1525
|
+
if (context === "recent") {
|
|
1526
|
+
score = timeFactor + 1e3 / Math.max(1, frequency);
|
|
1527
|
+
} else if (context === "frequent") {
|
|
1528
|
+
score = timeFactor / 1e3 + 10 / Math.max(1, frequency);
|
|
1529
|
+
} else {
|
|
1530
|
+
score = timeFactor / 1e3 / Math.max(1, frequency);
|
|
1531
|
+
}
|
|
1532
|
+
if (score < bestScore) {
|
|
1533
|
+
bestScore = score;
|
|
1534
|
+
bestKey = key;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return bestKey || (items.size > 0 ? items.keys().next().value ?? null : null);
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Apply periodic decay to frequency scores
|
|
1541
|
+
*/
|
|
1542
|
+
applyPeriodicDecay(items) {
|
|
1543
|
+
if (!this.config.useEnhancedFrequency || (this.config.frequencyDecayFactor ?? 0) === 0) return;
|
|
1544
|
+
const now = Date.now();
|
|
1545
|
+
const timeSinceDecay = now - this.lastDecayTime;
|
|
1546
|
+
if (timeSinceDecay >= (this.config.frequencyDecayInterval ?? 6e5)) {
|
|
1547
|
+
if (items.size > 0) {
|
|
1548
|
+
for (const metadata of items.values()) {
|
|
1549
|
+
if (typeof metadata.frequencyScore === "number") {
|
|
1550
|
+
const decayAmount = this.config.frequencyDecayFactor ?? 0.05;
|
|
1551
|
+
metadata.frequencyScore = Math.max(1, metadata.frequencyScore * (1 - decayAmount));
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
this.lastDecayTime = now;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Get configuration for this strategy
|
|
1560
|
+
*/
|
|
1561
|
+
getConfig() {
|
|
1562
|
+
return { ...this.config };
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Reset internal state (useful for testing)
|
|
1566
|
+
*/
|
|
1567
|
+
reset() {
|
|
1568
|
+
this.recentGhosts.clear();
|
|
1569
|
+
this.frequentGhosts.clear();
|
|
1570
|
+
this.targetRecentSize = 0;
|
|
1571
|
+
this.lastDecayTime = Date.now();
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Get current adaptive state for monitoring/debugging
|
|
1575
|
+
*/
|
|
1576
|
+
getAdaptiveState() {
|
|
1577
|
+
return {
|
|
1578
|
+
targetRecentSize: this.targetRecentSize,
|
|
1579
|
+
recentGhostSize: this.recentGhosts.size,
|
|
1580
|
+
frequentGhostSize: this.frequentGhosts.size
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
// src/eviction/strategies/TwoQueueEvictionStrategy.ts
|
|
1586
|
+
var TwoQueueEvictionStrategy = class extends EvictionStrategy {
|
|
1587
|
+
recentQueue = [];
|
|
1588
|
+
// A1 queue for recent items
|
|
1589
|
+
hotQueue = [];
|
|
1590
|
+
// Am queue for hot items
|
|
1591
|
+
ghostQueue = /* @__PURE__ */ new Set();
|
|
1592
|
+
// A1out ghost queue
|
|
1593
|
+
config;
|
|
1594
|
+
maxRecentSize;
|
|
1595
|
+
maxGhostSize;
|
|
1596
|
+
lastDecayTime;
|
|
1597
|
+
constructor(maxCacheSize = 1e3, config = {}) {
|
|
1598
|
+
super();
|
|
1599
|
+
const baseConfig = { ...DEFAULT_TWO_QUEUE_CONFIG, maxCacheSize };
|
|
1600
|
+
this.config = createValidatedConfig(baseConfig, config);
|
|
1601
|
+
this.maxRecentSize = Math.max(1, Math.floor(this.config.maxCacheSize * 0.25));
|
|
1602
|
+
this.maxGhostSize = this.config.maxCacheSize;
|
|
1603
|
+
this.lastDecayTime = Date.now();
|
|
1604
|
+
}
|
|
1605
|
+
selectForEviction(items) {
|
|
1606
|
+
if (items.size === 0) return null;
|
|
1607
|
+
this.applyPeriodicDecay(items);
|
|
1608
|
+
for (let i = this.recentQueue.length - 1; i >= 0; i--) {
|
|
1609
|
+
const key = this.recentQueue[i];
|
|
1610
|
+
if (items.has(key)) {
|
|
1611
|
+
return key;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (this.config.useFrequencyWeightedLRU) {
|
|
1615
|
+
return this.selectFromHotQueueFrequencyWeighted(items);
|
|
1616
|
+
} else {
|
|
1617
|
+
return this.selectFromHotQueueLRU(items);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Select eviction candidate from hot queue using traditional LRU
|
|
1622
|
+
*/
|
|
1623
|
+
selectFromHotQueueLRU(items) {
|
|
1624
|
+
let oldestKey = null;
|
|
1625
|
+
let oldestTime = Infinity;
|
|
1626
|
+
for (const key of this.hotQueue) {
|
|
1627
|
+
const metadata = items.get(key);
|
|
1628
|
+
if (metadata && metadata.lastAccessedAt < oldestTime) {
|
|
1629
|
+
oldestTime = metadata.lastAccessedAt;
|
|
1630
|
+
oldestKey = key;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
return oldestKey || (items.size > 0 ? items.keys().next().value ?? null : null);
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Select eviction candidate from hot queue using frequency-weighted LRU
|
|
1637
|
+
*/
|
|
1638
|
+
selectFromHotQueueFrequencyWeighted(items) {
|
|
1639
|
+
let bestKey = null;
|
|
1640
|
+
let lowestScore = Infinity;
|
|
1641
|
+
for (const key of this.hotQueue) {
|
|
1642
|
+
const metadata = items.get(key);
|
|
1643
|
+
if (!metadata) continue;
|
|
1644
|
+
const frequency = this.getEffectiveFrequency(metadata);
|
|
1645
|
+
const timeFactor = Date.now() - metadata.lastAccessedAt;
|
|
1646
|
+
const normalizedTimeFactor = timeFactor / (1e3 * 60);
|
|
1647
|
+
const score = normalizedTimeFactor / Math.max(1, frequency);
|
|
1648
|
+
if (score < lowestScore) {
|
|
1649
|
+
lowestScore = score;
|
|
1650
|
+
bestKey = key;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return bestKey || (items.size > 0 ? items.keys().next().value ?? null : null);
|
|
1654
|
+
}
|
|
1655
|
+
onItemAccessed(key, metadata) {
|
|
1656
|
+
const now = Date.now();
|
|
1657
|
+
metadata.lastAccessedAt = now;
|
|
1658
|
+
metadata.accessCount++;
|
|
1659
|
+
metadata.rawFrequency = metadata.accessCount;
|
|
1660
|
+
if ((this.config.hotQueueDecayFactor ?? 0) > 0) {
|
|
1661
|
+
metadata.frequencyScore = this.calculateFrequencyScore(metadata, now);
|
|
1662
|
+
metadata.lastFrequencyUpdate = now;
|
|
1663
|
+
}
|
|
1664
|
+
const recentIndex = this.recentQueue.indexOf(key);
|
|
1665
|
+
if (recentIndex !== -1) {
|
|
1666
|
+
if (this.shouldPromoteToHotQueue(metadata)) {
|
|
1667
|
+
this.recentQueue.splice(recentIndex, 1);
|
|
1668
|
+
this.hotQueue.unshift(key);
|
|
1669
|
+
}
|
|
1670
|
+
} else {
|
|
1671
|
+
const hotIndex = this.hotQueue.indexOf(key);
|
|
1672
|
+
if (hotIndex !== -1) {
|
|
1673
|
+
this.hotQueue.splice(hotIndex, 1);
|
|
1674
|
+
this.hotQueue.unshift(key);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
onItemAdded(key, metadata) {
|
|
1679
|
+
const now = Date.now();
|
|
1680
|
+
metadata.addedAt = now;
|
|
1681
|
+
metadata.lastAccessedAt = now;
|
|
1682
|
+
metadata.accessCount = 1;
|
|
1683
|
+
metadata.rawFrequency = 1;
|
|
1684
|
+
if ((this.config.hotQueueDecayFactor ?? 0) > 0) {
|
|
1685
|
+
metadata.frequencyScore = 1;
|
|
1686
|
+
metadata.lastFrequencyUpdate = now;
|
|
1687
|
+
}
|
|
1688
|
+
if (this.ghostQueue.has(key)) {
|
|
1689
|
+
this.ghostQueue.delete(key);
|
|
1690
|
+
this.hotQueue.unshift(key);
|
|
1691
|
+
} else {
|
|
1692
|
+
this.recentQueue.unshift(key);
|
|
1693
|
+
if (this.recentQueue.length > this.maxRecentSize) {
|
|
1694
|
+
const evicted = this.recentQueue.pop();
|
|
1695
|
+
if (evicted) {
|
|
1696
|
+
this.ghostQueue.add(evicted);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
if (this.ghostQueue.size > this.maxGhostSize) {
|
|
1701
|
+
const firstKey = this.ghostQueue.values().next().value;
|
|
1702
|
+
if (firstKey) {
|
|
1703
|
+
this.ghostQueue.delete(firstKey);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
onItemRemoved(key) {
|
|
1708
|
+
const recentIndex = this.recentQueue.indexOf(key);
|
|
1709
|
+
if (recentIndex !== -1) {
|
|
1710
|
+
this.recentQueue.splice(recentIndex, 1);
|
|
1711
|
+
}
|
|
1712
|
+
const hotIndex = this.hotQueue.indexOf(key);
|
|
1713
|
+
if (hotIndex !== -1) {
|
|
1714
|
+
this.hotQueue.splice(hotIndex, 1);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Determine if an item should be promoted from recent to hot queue
|
|
1719
|
+
*/
|
|
1720
|
+
shouldPromoteToHotQueue(metadata) {
|
|
1721
|
+
if (!this.config.useFrequencyPromotion) {
|
|
1722
|
+
return metadata.accessCount >= 2;
|
|
1723
|
+
}
|
|
1724
|
+
const threshold = this.config.promotionThreshold ?? 2;
|
|
1725
|
+
const frequency = this.getEffectiveFrequency(metadata);
|
|
1726
|
+
return frequency >= threshold;
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Get effective frequency for an item, applying decay if enabled
|
|
1730
|
+
*/
|
|
1731
|
+
getEffectiveFrequency(metadata) {
|
|
1732
|
+
if ((this.config.hotQueueDecayFactor ?? 0) === 0) {
|
|
1733
|
+
return metadata.rawFrequency || metadata.accessCount;
|
|
1734
|
+
}
|
|
1735
|
+
const now = Date.now();
|
|
1736
|
+
if (typeof metadata.frequencyScore === "number" && typeof metadata.lastFrequencyUpdate === "number") {
|
|
1737
|
+
const timeSinceUpdate = now - metadata.lastFrequencyUpdate;
|
|
1738
|
+
const decayAmount = timeSinceUpdate / (this.config.hotQueueDecayInterval ?? 3e5) * (this.config.hotQueueDecayFactor ?? 0.05);
|
|
1739
|
+
return Math.max(1, metadata.frequencyScore * (1 - decayAmount));
|
|
1740
|
+
}
|
|
1741
|
+
return metadata.rawFrequency || metadata.accessCount;
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Calculate frequency score with decay applied
|
|
1745
|
+
*/
|
|
1746
|
+
calculateFrequencyScore(metadata, currentTime) {
|
|
1747
|
+
const rawFreq = metadata.rawFrequency || metadata.accessCount;
|
|
1748
|
+
if (typeof metadata.lastFrequencyUpdate !== "number") {
|
|
1749
|
+
return rawFreq;
|
|
1750
|
+
}
|
|
1751
|
+
const timeSinceUpdate = currentTime - metadata.lastFrequencyUpdate;
|
|
1752
|
+
const decayAmount = timeSinceUpdate / (this.config.hotQueueDecayInterval ?? 3e5) * (this.config.hotQueueDecayFactor ?? 0.05);
|
|
1753
|
+
const previousScore = metadata.frequencyScore || rawFreq;
|
|
1754
|
+
const decayedScore = previousScore * (1 - decayAmount);
|
|
1755
|
+
return Math.max(1, decayedScore + 1);
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Apply periodic decay to hot queue items
|
|
1759
|
+
*/
|
|
1760
|
+
applyPeriodicDecay(items) {
|
|
1761
|
+
if ((this.config.hotQueueDecayFactor ?? 0) === 0) return;
|
|
1762
|
+
const now = Date.now();
|
|
1763
|
+
const timeSinceDecay = now - this.lastDecayTime;
|
|
1764
|
+
if (timeSinceDecay >= (this.config.hotQueueDecayInterval ?? 3e5)) {
|
|
1765
|
+
if (this.hotQueue.length > 0) {
|
|
1766
|
+
for (const key of this.hotQueue) {
|
|
1767
|
+
const metadata = items.get(key);
|
|
1768
|
+
if (metadata && typeof metadata.frequencyScore === "number") {
|
|
1769
|
+
const decayAmount = this.config.hotQueueDecayFactor ?? 0.05;
|
|
1770
|
+
metadata.frequencyScore = Math.max(1, metadata.frequencyScore * (1 - decayAmount));
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
this.lastDecayTime = now;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Get configuration for this strategy
|
|
1779
|
+
*/
|
|
1780
|
+
getConfig() {
|
|
1781
|
+
return { ...this.config };
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Reset internal state (useful for testing)
|
|
1785
|
+
*/
|
|
1786
|
+
reset() {
|
|
1787
|
+
this.recentQueue = [];
|
|
1788
|
+
this.hotQueue = [];
|
|
1789
|
+
this.ghostQueue.clear();
|
|
1790
|
+
this.lastDecayTime = Date.now();
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
|
|
1794
|
+
// src/eviction/EvictionStrategyFactory.ts
|
|
1795
|
+
function createEvictionStrategy(policy, maxCacheSize, config) {
|
|
1796
|
+
const safeMaxCacheSize = typeof maxCacheSize === "number" && maxCacheSize > 0 ? maxCacheSize : 1e3;
|
|
1797
|
+
switch (policy) {
|
|
1798
|
+
case "lru":
|
|
1799
|
+
return new LRUEvictionStrategy();
|
|
1800
|
+
case "lfu": {
|
|
1801
|
+
try {
|
|
1802
|
+
const lfuConfig = config?.type === "lfu" ? config : { type: "lfu" };
|
|
1803
|
+
return new LFUEvictionStrategy(lfuConfig);
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1806
|
+
console.warn(`Failed to create lfu strategy with provided configuration, falling back to LRU:`, errorMessage);
|
|
1807
|
+
return new LRUEvictionStrategy();
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
case "fifo":
|
|
1811
|
+
return new FIFOEvictionStrategy();
|
|
1812
|
+
case "mru":
|
|
1813
|
+
return new MRUEvictionStrategy();
|
|
1814
|
+
case "random":
|
|
1815
|
+
return new RandomEvictionStrategy();
|
|
1816
|
+
case "arc": {
|
|
1817
|
+
try {
|
|
1818
|
+
const arcConfig = config?.type === "arc" ? config : { ...DEFAULT_ARC_CONFIG, maxCacheSize: safeMaxCacheSize };
|
|
1819
|
+
const finalMaxSize = arcConfig.maxCacheSize && arcConfig.maxCacheSize > 0 ? arcConfig.maxCacheSize : safeMaxCacheSize;
|
|
1820
|
+
return new ARCEvictionStrategy(finalMaxSize, { ...arcConfig, maxCacheSize: finalMaxSize });
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1823
|
+
console.warn(`Failed to create arc strategy with provided configuration, falling back to LRU:`, errorMessage);
|
|
1824
|
+
return new LRUEvictionStrategy();
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
case "2q": {
|
|
1828
|
+
try {
|
|
1829
|
+
const twoQConfig = config?.type === "2q" ? config : { ...DEFAULT_TWO_QUEUE_CONFIG, maxCacheSize: safeMaxCacheSize };
|
|
1830
|
+
const finalMaxSize = twoQConfig.maxCacheSize && twoQConfig.maxCacheSize > 0 ? twoQConfig.maxCacheSize : safeMaxCacheSize;
|
|
1831
|
+
return new TwoQueueEvictionStrategy(finalMaxSize, { ...twoQConfig, maxCacheSize: finalMaxSize });
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1834
|
+
console.warn(`Failed to create 2q strategy with provided configuration, falling back to LRU:`, errorMessage);
|
|
1835
|
+
return new LRUEvictionStrategy();
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
default:
|
|
1839
|
+
throw new Error(`Unsupported eviction policy: ${policy}`);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// src/utils/CacheSize.ts
|
|
1844
|
+
var SIZE_UNITS = {
|
|
1845
|
+
// Decimal units (powers of 1000)
|
|
1846
|
+
"b": 1,
|
|
1847
|
+
"byte": 1,
|
|
1848
|
+
"bytes": 1,
|
|
1849
|
+
"kb": 1e3,
|
|
1850
|
+
"kilobyte": 1e3,
|
|
1851
|
+
"kilobytes": 1e3,
|
|
1852
|
+
"mb": 1e3 * 1e3,
|
|
1853
|
+
"megabyte": 1e3 * 1e3,
|
|
1854
|
+
"megabytes": 1e3 * 1e3,
|
|
1855
|
+
"gb": 1e3 * 1e3 * 1e3,
|
|
1856
|
+
"gigabyte": 1e3 * 1e3 * 1e3,
|
|
1857
|
+
"gigabytes": 1e3 * 1e3 * 1e3,
|
|
1858
|
+
"tb": 1e3 * 1e3 * 1e3 * 1e3,
|
|
1859
|
+
"terabyte": 1e3 * 1e3 * 1e3 * 1e3,
|
|
1860
|
+
"terabytes": 1e3 * 1e3 * 1e3 * 1e3,
|
|
1861
|
+
// Binary units (powers of 1024)
|
|
1862
|
+
"kib": 1024,
|
|
1863
|
+
"kibibyte": 1024,
|
|
1864
|
+
"kibibytes": 1024,
|
|
1865
|
+
"mib": 1024 * 1024,
|
|
1866
|
+
"mebibyte": 1024 * 1024,
|
|
1867
|
+
"mebibytes": 1024 * 1024,
|
|
1868
|
+
"gib": 1024 * 1024 * 1024,
|
|
1869
|
+
"gibibyte": 1024 * 1024 * 1024,
|
|
1870
|
+
"gibibytes": 1024 * 1024 * 1024,
|
|
1871
|
+
"tib": 1024 * 1024 * 1024 * 1024,
|
|
1872
|
+
"tebibyte": 1024 * 1024 * 1024 * 1024,
|
|
1873
|
+
"tebibytes": 1024 * 1024 * 1024 * 1024
|
|
1874
|
+
};
|
|
1875
|
+
function parseSizeString(sizeStr) {
|
|
1876
|
+
if (!sizeStr || typeof sizeStr !== "string") {
|
|
1877
|
+
throw new Error("Size string must be a non-empty string");
|
|
1878
|
+
}
|
|
1879
|
+
const trimmed = sizeStr.trim();
|
|
1880
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
1881
|
+
const bytes = parseFloat(trimmed);
|
|
1882
|
+
if (isNaN(bytes) || bytes < 0) {
|
|
1883
|
+
throw new Error(`Invalid size value: ${sizeStr}`);
|
|
1884
|
+
}
|
|
1885
|
+
return Math.floor(bytes);
|
|
1886
|
+
}
|
|
1887
|
+
const match = trimmed.match(/^(\d+(?:\.\d+)?)\s*([a-zA-Z]+)$/);
|
|
1888
|
+
if (!match) {
|
|
1889
|
+
throw new Error(`Invalid size format: ${sizeStr}. Expected format: '100', '5KB', '10MB', etc.`);
|
|
1890
|
+
}
|
|
1891
|
+
const [, valueStr, unitStr] = match;
|
|
1892
|
+
const value = parseFloat(valueStr);
|
|
1893
|
+
const unit = unitStr.toLowerCase();
|
|
1894
|
+
if (isNaN(value) || value < 0) {
|
|
1895
|
+
throw new Error(`Invalid size value: ${valueStr}`);
|
|
1896
|
+
}
|
|
1897
|
+
const multiplier = SIZE_UNITS[unit];
|
|
1898
|
+
if (!(unit in SIZE_UNITS)) {
|
|
1899
|
+
const supportedUnits = Object.keys(SIZE_UNITS).filter((u) => u.length <= 3).join(", ");
|
|
1900
|
+
throw new Error(`Unsupported size unit: ${unitStr}. Supported units: ${supportedUnits}`);
|
|
1901
|
+
}
|
|
1902
|
+
return Math.floor(value * multiplier);
|
|
1903
|
+
}
|
|
1904
|
+
function formatBytes(bytes, binary = false) {
|
|
1905
|
+
if (bytes === 0) return "0 B";
|
|
1906
|
+
if (bytes < 0) return `${bytes} B`;
|
|
1907
|
+
const k = binary ? 1024 : 1e3;
|
|
1908
|
+
const sizes = binary ? ["B", "KiB", "MiB", "GiB", "TiB", "PiB"] : ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
1909
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1910
|
+
const size = bytes / Math.pow(k, i);
|
|
1911
|
+
const formatted = size % 1 === 0 ? size.toString() : size.toFixed(1);
|
|
1912
|
+
return `${formatted} ${sizes[i]}`;
|
|
1913
|
+
}
|
|
1914
|
+
function estimateValueSize(value) {
|
|
1915
|
+
if (value === null || typeof value === "undefined") {
|
|
1916
|
+
return 8;
|
|
1917
|
+
}
|
|
1918
|
+
switch (typeof value) {
|
|
1919
|
+
case "boolean":
|
|
1920
|
+
return 4;
|
|
1921
|
+
case "number":
|
|
1922
|
+
return 8;
|
|
1923
|
+
case "string":
|
|
1924
|
+
return value.length * 2;
|
|
1925
|
+
case "object":
|
|
1926
|
+
if (Array.isArray(value)) {
|
|
1927
|
+
return value.reduce((total, item) => total + estimateValueSize(item), 24);
|
|
1928
|
+
}
|
|
1929
|
+
try {
|
|
1930
|
+
const jsonString = JSON.stringify(value);
|
|
1931
|
+
return jsonString.length * 2 + 16;
|
|
1932
|
+
} catch {
|
|
1933
|
+
return 64;
|
|
1934
|
+
}
|
|
1935
|
+
default:
|
|
1936
|
+
return 32;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
function validateSizeConfig(config) {
|
|
1940
|
+
if (typeof config.maxSizeBytes !== "undefined") {
|
|
1941
|
+
try {
|
|
1942
|
+
const bytes = parseSizeString(config.maxSizeBytes);
|
|
1943
|
+
if (bytes <= 0) {
|
|
1944
|
+
throw new Error("maxSizeBytes must be positive");
|
|
1945
|
+
}
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
throw new Error(`Invalid maxSizeBytes: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if (typeof config.maxItems !== "undefined") {
|
|
1951
|
+
if (!Number.isInteger(config.maxItems) || config.maxItems <= 0) {
|
|
1952
|
+
throw new Error("maxItems must be a positive integer");
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
// src/memory/EnhancedMemoryCacheMap.ts
|
|
1958
|
+
var logger16 = logger_default.get("EnhancedMemoryCacheMap");
|
|
1959
|
+
var EnhancedMemoryCacheMap = class _EnhancedMemoryCacheMap extends CacheMap {
|
|
1960
|
+
map = {};
|
|
1961
|
+
normalizedHashFunction;
|
|
1962
|
+
// Query result cache: maps query hash to cache entry with expiration
|
|
1963
|
+
queryResultCache = {};
|
|
1964
|
+
// Mutex-like tracking for TTL operations to prevent race conditions
|
|
1965
|
+
ttlOperationsInProgress = /* @__PURE__ */ new Set();
|
|
1966
|
+
// Size tracking
|
|
1967
|
+
currentSizeBytes = 0;
|
|
1968
|
+
currentItemCount = 0;
|
|
1969
|
+
queryResultsCacheSize = 0;
|
|
1970
|
+
// Size limits
|
|
1971
|
+
maxSizeBytes;
|
|
1972
|
+
maxItems;
|
|
1973
|
+
// Eviction strategy
|
|
1974
|
+
evictionStrategy;
|
|
1975
|
+
constructor(types, sizeConfig, initialData) {
|
|
1976
|
+
super(types);
|
|
1977
|
+
this.normalizedHashFunction = createNormalizedHashFunction();
|
|
1978
|
+
if (sizeConfig?.maxSizeBytes) {
|
|
1979
|
+
this.maxSizeBytes = parseSizeString(sizeConfig.maxSizeBytes);
|
|
1980
|
+
logger16.debug("Cache size limit set", { maxSizeBytes: this.maxSizeBytes });
|
|
1981
|
+
}
|
|
1982
|
+
if (sizeConfig?.maxItems) {
|
|
1983
|
+
this.maxItems = sizeConfig.maxItems;
|
|
1984
|
+
logger16.debug("Cache item limit set", { maxItems: this.maxItems });
|
|
1985
|
+
}
|
|
1986
|
+
const policy = sizeConfig?.evictionPolicy || "lru";
|
|
1987
|
+
const maxCacheSize = this.maxItems || 1e3;
|
|
1988
|
+
this.evictionStrategy = createEvictionStrategy(policy, maxCacheSize);
|
|
1989
|
+
logger16.debug("Eviction strategy initialized", { policy });
|
|
1990
|
+
if (initialData) {
|
|
1991
|
+
for (const [keyStr, value] of Object.entries(initialData)) {
|
|
1992
|
+
try {
|
|
1993
|
+
const key = JSON.parse(keyStr);
|
|
1994
|
+
this.set(key, value);
|
|
1995
|
+
} catch (error) {
|
|
1996
|
+
logger16.error("Failed to parse initial data key", { keyStr, error });
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
get(key) {
|
|
2002
|
+
logger16.trace("get", { key });
|
|
2003
|
+
const hashedKey = this.normalizedHashFunction(key);
|
|
2004
|
+
const entry = this.map[hashedKey];
|
|
2005
|
+
if (entry && this.normalizedHashFunction(entry.originalKey) === hashedKey) {
|
|
2006
|
+
this.evictionStrategy.onItemAccessed(hashedKey, entry.metadata);
|
|
2007
|
+
return entry.value;
|
|
2008
|
+
}
|
|
2009
|
+
return null;
|
|
2010
|
+
}
|
|
2011
|
+
getWithTTL(key, ttl) {
|
|
2012
|
+
logger16.trace("getWithTTL", { key, ttl });
|
|
2013
|
+
if (ttl === 0) {
|
|
2014
|
+
return null;
|
|
2015
|
+
}
|
|
2016
|
+
const hashedKey = this.normalizedHashFunction(key);
|
|
2017
|
+
const entry = this.map[hashedKey];
|
|
2018
|
+
if (entry && this.normalizedHashFunction(entry.originalKey) === hashedKey) {
|
|
2019
|
+
const now = Date.now();
|
|
2020
|
+
const age = now - entry.metadata.addedAt;
|
|
2021
|
+
if (age >= ttl) {
|
|
2022
|
+
logger16.trace("Item expired, removing from enhanced cache", { key, age, ttl });
|
|
2023
|
+
this.delete(key);
|
|
2024
|
+
return null;
|
|
2025
|
+
}
|
|
2026
|
+
this.evictionStrategy.onItemAccessed(hashedKey, entry.metadata);
|
|
2027
|
+
return entry.value;
|
|
2028
|
+
}
|
|
2029
|
+
return null;
|
|
2030
|
+
}
|
|
2031
|
+
set(key, value) {
|
|
2032
|
+
logger16.trace("set", { key, value });
|
|
2033
|
+
const hashedKey = this.normalizedHashFunction(key);
|
|
2034
|
+
const estimatedSize = estimateValueSize(value);
|
|
2035
|
+
const existingEntry = this.map[hashedKey];
|
|
2036
|
+
const isUpdate = existingEntry && this.normalizedHashFunction(existingEntry.originalKey) === hashedKey;
|
|
2037
|
+
if (isUpdate) {
|
|
2038
|
+
const sizeDiff = estimatedSize - existingEntry.metadata.estimatedSize;
|
|
2039
|
+
this.currentSizeBytes += sizeDiff;
|
|
2040
|
+
const oldValue = existingEntry.value;
|
|
2041
|
+
existingEntry.value = value;
|
|
2042
|
+
existingEntry.metadata.estimatedSize = estimatedSize;
|
|
2043
|
+
this.evictionStrategy.onItemRemoved(hashedKey);
|
|
2044
|
+
this.evictionStrategy.onItemAdded(hashedKey, existingEntry.metadata);
|
|
2045
|
+
logger16.trace("Updated existing cache entry", {
|
|
2046
|
+
key: hashedKey,
|
|
2047
|
+
sizeDiff,
|
|
2048
|
+
currentSize: this.currentSizeBytes,
|
|
2049
|
+
oldValue: oldValue !== value
|
|
2050
|
+
});
|
|
2051
|
+
} else {
|
|
2052
|
+
this.ensureSpaceAvailable(estimatedSize);
|
|
2053
|
+
const metadata = {
|
|
2054
|
+
addedAt: Date.now(),
|
|
2055
|
+
lastAccessedAt: Date.now(),
|
|
2056
|
+
accessCount: 0,
|
|
2057
|
+
estimatedSize,
|
|
2058
|
+
key: hashedKey
|
|
2059
|
+
};
|
|
2060
|
+
this.map[hashedKey] = {
|
|
2061
|
+
originalKey: key,
|
|
2062
|
+
value,
|
|
2063
|
+
metadata
|
|
2064
|
+
};
|
|
2065
|
+
this.currentSizeBytes += estimatedSize;
|
|
2066
|
+
this.currentItemCount++;
|
|
2067
|
+
this.evictionStrategy.onItemAdded(hashedKey, metadata);
|
|
2068
|
+
logger16.trace("Added new cache entry", {
|
|
2069
|
+
key: hashedKey,
|
|
2070
|
+
size: estimatedSize,
|
|
2071
|
+
currentSize: this.currentSizeBytes,
|
|
2072
|
+
currentCount: this.currentItemCount
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
includesKey(key) {
|
|
2077
|
+
const hashedKey = this.normalizedHashFunction(key);
|
|
2078
|
+
const entry = this.map[hashedKey];
|
|
2079
|
+
return !!entry && this.normalizedHashFunction(entry.originalKey) === hashedKey;
|
|
2080
|
+
}
|
|
2081
|
+
delete(key) {
|
|
2082
|
+
logger16.trace("delete", { key });
|
|
2083
|
+
const hashedKey = this.normalizedHashFunction(key);
|
|
2084
|
+
const entry = this.map[hashedKey];
|
|
2085
|
+
if (entry && this.normalizedHashFunction(entry.originalKey) === hashedKey) {
|
|
2086
|
+
this.currentSizeBytes -= entry.metadata.estimatedSize;
|
|
2087
|
+
this.currentItemCount--;
|
|
2088
|
+
this.evictionStrategy.onItemRemoved(hashedKey);
|
|
2089
|
+
delete this.map[hashedKey];
|
|
2090
|
+
logger16.trace("Deleted cache entry", {
|
|
2091
|
+
key: hashedKey,
|
|
2092
|
+
freedSize: entry.metadata.estimatedSize,
|
|
2093
|
+
currentSize: this.currentSizeBytes,
|
|
2094
|
+
currentCount: this.currentItemCount
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
keys() {
|
|
2099
|
+
return Object.values(this.map).map((entry) => entry.originalKey);
|
|
2100
|
+
}
|
|
2101
|
+
values() {
|
|
2102
|
+
return Object.values(this.map).map((entry) => entry.value);
|
|
2103
|
+
}
|
|
2104
|
+
clear() {
|
|
2105
|
+
logger16.debug("Clearing cache", {
|
|
2106
|
+
itemsCleared: this.currentItemCount,
|
|
2107
|
+
bytesFreed: this.currentSizeBytes
|
|
2108
|
+
});
|
|
2109
|
+
for (const hashedKey of Object.keys(this.map)) {
|
|
2110
|
+
this.evictionStrategy.onItemRemoved(hashedKey);
|
|
2111
|
+
}
|
|
2112
|
+
this.map = {};
|
|
2113
|
+
this.currentSizeBytes = 0;
|
|
2114
|
+
this.currentItemCount = 0;
|
|
2115
|
+
}
|
|
2116
|
+
allIn(locations) {
|
|
2117
|
+
const allValues = this.values();
|
|
2118
|
+
if (locations.length === 0) {
|
|
2119
|
+
logger16.debug("Returning all items, LocKeys is empty");
|
|
2120
|
+
return allValues;
|
|
2121
|
+
} else {
|
|
2122
|
+
logger16.debug("allIn", { locations, count: allValues.length });
|
|
2123
|
+
return allValues.filter((item) => {
|
|
2124
|
+
const key = item.key;
|
|
2125
|
+
if (key && isComKey2(key)) {
|
|
2126
|
+
return isLocKeyArrayEqual(locations, key.loc);
|
|
2127
|
+
}
|
|
2128
|
+
return false;
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
contains(query, locations) {
|
|
2133
|
+
logger16.debug("contains", { query, locations });
|
|
2134
|
+
const items = this.allIn(locations);
|
|
2135
|
+
return items.some((item) => isQueryMatch2(item, query));
|
|
2136
|
+
}
|
|
2137
|
+
queryIn(query, locations = []) {
|
|
2138
|
+
logger16.debug("queryIn", { query, locations });
|
|
2139
|
+
const items = this.allIn(locations);
|
|
2140
|
+
return items.filter((item) => isQueryMatch2(item, query));
|
|
2141
|
+
}
|
|
2142
|
+
clone() {
|
|
2143
|
+
const sizeConfig = {};
|
|
2144
|
+
if (this.maxSizeBytes) {
|
|
2145
|
+
sizeConfig.maxSizeBytes = this.maxSizeBytes.toString();
|
|
2146
|
+
}
|
|
2147
|
+
if (this.maxItems) {
|
|
2148
|
+
sizeConfig.maxItems = this.maxItems;
|
|
2149
|
+
}
|
|
2150
|
+
const clone = new _EnhancedMemoryCacheMap(this.types, sizeConfig);
|
|
2151
|
+
for (const key of this.keys()) {
|
|
2152
|
+
const value = this.get(key);
|
|
2153
|
+
if (value) {
|
|
2154
|
+
clone.set(key, value);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
return clone;
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Get current cache statistics
|
|
2161
|
+
*/
|
|
2162
|
+
getStats() {
|
|
2163
|
+
const stats = {
|
|
2164
|
+
currentSizeBytes: this.currentSizeBytes,
|
|
2165
|
+
currentItemCount: this.currentItemCount,
|
|
2166
|
+
maxSizeBytes: this.maxSizeBytes,
|
|
2167
|
+
maxItems: this.maxItems,
|
|
2168
|
+
utilizationPercent: {}
|
|
2169
|
+
};
|
|
2170
|
+
if (this.maxSizeBytes) {
|
|
2171
|
+
stats.utilizationPercent.bytes = this.currentSizeBytes / this.maxSizeBytes * 100;
|
|
2172
|
+
}
|
|
2173
|
+
if (this.maxItems) {
|
|
2174
|
+
stats.utilizationPercent.items = this.currentItemCount / this.maxItems * 100;
|
|
2175
|
+
}
|
|
2176
|
+
return stats;
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Ensure there's space available for a new item of the given size
|
|
2180
|
+
* Evicts items if necessary based on the configured eviction policy
|
|
2181
|
+
*/
|
|
2182
|
+
ensureSpaceAvailable(newItemSize) {
|
|
2183
|
+
const itemMetadata = /* @__PURE__ */ new Map();
|
|
2184
|
+
for (const [hashedKey, entry] of Object.entries(this.map)) {
|
|
2185
|
+
itemMetadata.set(hashedKey, entry.metadata);
|
|
2186
|
+
}
|
|
2187
|
+
while (this.maxItems && this.currentItemCount >= this.maxItems) {
|
|
2188
|
+
const keyToEvict = this.evictionStrategy.selectForEviction(itemMetadata);
|
|
2189
|
+
if (!keyToEvict) {
|
|
2190
|
+
logger16.debug("No item selected for eviction despite being over item limit");
|
|
2191
|
+
break;
|
|
2192
|
+
}
|
|
2193
|
+
this.evictItem(keyToEvict, itemMetadata);
|
|
2194
|
+
logger16.debug("Evicted item due to count limit", {
|
|
2195
|
+
evictedKey: keyToEvict,
|
|
2196
|
+
currentCount: this.currentItemCount,
|
|
2197
|
+
maxItems: this.maxItems
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
while (this.maxSizeBytes && this.getTotalSizeBytes() + newItemSize > this.maxSizeBytes) {
|
|
2201
|
+
const keyToEvict = this.evictionStrategy.selectForEviction(itemMetadata);
|
|
2202
|
+
if (!keyToEvict) {
|
|
2203
|
+
logger16.debug("No item selected for eviction despite being over size limit");
|
|
2204
|
+
break;
|
|
2205
|
+
}
|
|
2206
|
+
this.evictItem(keyToEvict, itemMetadata);
|
|
2207
|
+
logger16.debug("Evicted item due to size limit", {
|
|
2208
|
+
evictedKey: keyToEvict,
|
|
2209
|
+
currentItemsSize: this.currentSizeBytes,
|
|
2210
|
+
queryResultsSize: this.queryResultsCacheSize,
|
|
2211
|
+
totalSize: this.getTotalSizeBytes(),
|
|
2212
|
+
newItemSize,
|
|
2213
|
+
maxSizeBytes: this.maxSizeBytes
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Evict a specific item and update metadata tracking
|
|
2219
|
+
*/
|
|
2220
|
+
evictItem(hashedKey, itemMetadata) {
|
|
2221
|
+
const entry = this.map[hashedKey];
|
|
2222
|
+
if (entry) {
|
|
2223
|
+
const originalKey = entry.originalKey;
|
|
2224
|
+
this.delete(originalKey);
|
|
2225
|
+
itemMetadata.delete(hashedKey);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
// Query result caching methods
|
|
2229
|
+
setQueryResult(queryHash, itemKeys, ttl) {
|
|
2230
|
+
logger16.trace("setQueryResult", { queryHash, itemKeys, ttl });
|
|
2231
|
+
if (queryHash in this.queryResultCache) {
|
|
2232
|
+
this.removeQueryResultFromSizeTracking(queryHash);
|
|
2233
|
+
}
|
|
2234
|
+
const entry = {
|
|
2235
|
+
itemKeys: [...itemKeys]
|
|
2236
|
+
// Create a copy to avoid external mutations
|
|
2237
|
+
};
|
|
2238
|
+
if (ttl) {
|
|
2239
|
+
entry.expiresAt = Date.now() + ttl;
|
|
2240
|
+
}
|
|
2241
|
+
this.queryResultCache[queryHash] = entry;
|
|
2242
|
+
this.addQueryResultToSizeTracking(queryHash, entry);
|
|
2243
|
+
}
|
|
2244
|
+
getQueryResult(queryHash) {
|
|
2245
|
+
logger16.trace("getQueryResult", { queryHash });
|
|
2246
|
+
if (this.ttlOperationsInProgress.has(queryHash)) {
|
|
2247
|
+
const entry2 = this.queryResultCache[queryHash];
|
|
2248
|
+
return entry2 ? [...entry2.itemKeys] : null;
|
|
2249
|
+
}
|
|
2250
|
+
const entry = this.queryResultCache[queryHash];
|
|
2251
|
+
if (!entry) {
|
|
2252
|
+
return null;
|
|
2253
|
+
}
|
|
2254
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
2255
|
+
this.ttlOperationsInProgress.add(queryHash);
|
|
2256
|
+
try {
|
|
2257
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
2258
|
+
logger16.trace("Query result expired, removing", { queryHash, expiresAt: entry.expiresAt });
|
|
2259
|
+
this.removeQueryResultFromSizeTracking(queryHash);
|
|
2260
|
+
delete this.queryResultCache[queryHash];
|
|
2261
|
+
return null;
|
|
2262
|
+
}
|
|
2263
|
+
} finally {
|
|
2264
|
+
this.ttlOperationsInProgress.delete(queryHash);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
return [...entry.itemKeys];
|
|
2268
|
+
}
|
|
2269
|
+
hasQueryResult(queryHash) {
|
|
2270
|
+
if (this.ttlOperationsInProgress.has(queryHash)) {
|
|
2271
|
+
return queryHash in this.queryResultCache;
|
|
2272
|
+
}
|
|
2273
|
+
const entry = this.queryResultCache[queryHash];
|
|
2274
|
+
if (!entry) {
|
|
2275
|
+
return false;
|
|
2276
|
+
}
|
|
2277
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
2278
|
+
this.ttlOperationsInProgress.add(queryHash);
|
|
2279
|
+
try {
|
|
2280
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
2281
|
+
this.removeQueryResultFromSizeTracking(queryHash);
|
|
2282
|
+
delete this.queryResultCache[queryHash];
|
|
2283
|
+
return false;
|
|
2284
|
+
}
|
|
2285
|
+
} finally {
|
|
2286
|
+
this.ttlOperationsInProgress.delete(queryHash);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
return true;
|
|
2290
|
+
}
|
|
2291
|
+
deleteQueryResult(queryHash) {
|
|
2292
|
+
if (queryHash in this.queryResultCache) {
|
|
2293
|
+
this.removeQueryResultFromSizeTracking(queryHash);
|
|
2294
|
+
delete this.queryResultCache[queryHash];
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
clearQueryResults() {
|
|
2298
|
+
this.queryResultCache = {};
|
|
2299
|
+
this.queryResultsCacheSize = 0;
|
|
2300
|
+
}
|
|
2301
|
+
invalidateItemKeys(keys) {
|
|
2302
|
+
logger16.debug("invalidateItemKeys", { keys });
|
|
2303
|
+
keys.forEach((key) => {
|
|
2304
|
+
this.delete(key);
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
invalidateLocation(locations) {
|
|
2308
|
+
logger16.debug("invalidateLocation", { locations });
|
|
2309
|
+
if (locations.length === 0) {
|
|
2310
|
+
const allKeys = this.keys();
|
|
2311
|
+
const primaryKeys = allKeys.filter((key) => !isComKey2(key));
|
|
2312
|
+
this.invalidateItemKeys(primaryKeys);
|
|
2313
|
+
} else {
|
|
2314
|
+
const itemsInLocation = this.allIn(locations);
|
|
2315
|
+
const keysToInvalidate = itemsInLocation.map((item) => item.key);
|
|
2316
|
+
this.invalidateItemKeys(keysToInvalidate);
|
|
2317
|
+
}
|
|
2318
|
+
this.clearQueryResults();
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Add query result to size tracking
|
|
2322
|
+
*/
|
|
2323
|
+
addQueryResultToSizeTracking(queryHash, entry) {
|
|
2324
|
+
const hashSize = estimateValueSize(queryHash);
|
|
2325
|
+
const itemKeysSize = estimateValueSize(entry.itemKeys);
|
|
2326
|
+
const metadataSize = entry.expiresAt ? 8 : 0;
|
|
2327
|
+
const totalSize = hashSize + itemKeysSize + metadataSize;
|
|
2328
|
+
this.queryResultsCacheSize += totalSize;
|
|
2329
|
+
logger16.trace("Added query result to size tracking", {
|
|
2330
|
+
queryHash,
|
|
2331
|
+
estimatedSize: totalSize,
|
|
2332
|
+
totalQueryCacheSize: this.queryResultsCacheSize
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* Remove query result from size tracking
|
|
2337
|
+
*/
|
|
2338
|
+
removeQueryResultFromSizeTracking(queryHash) {
|
|
2339
|
+
const entry = this.queryResultCache[queryHash];
|
|
2340
|
+
if (entry) {
|
|
2341
|
+
const hashSize = estimateValueSize(queryHash);
|
|
2342
|
+
const itemKeysSize = estimateValueSize(entry.itemKeys);
|
|
2343
|
+
const metadataSize = entry.expiresAt ? 8 : 0;
|
|
2344
|
+
const totalSize = hashSize + itemKeysSize + metadataSize;
|
|
2345
|
+
this.queryResultsCacheSize = Math.max(0, this.queryResultsCacheSize - totalSize);
|
|
2346
|
+
logger16.trace("Removed query result from size tracking", {
|
|
2347
|
+
queryHash,
|
|
2348
|
+
estimatedSize: totalSize,
|
|
2349
|
+
totalQueryCacheSize: this.queryResultsCacheSize
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Get total cache size including query results
|
|
2355
|
+
*/
|
|
2356
|
+
getTotalSizeBytes() {
|
|
2357
|
+
return this.currentSizeBytes + this.queryResultsCacheSize;
|
|
2358
|
+
}
|
|
2359
|
+
};
|
|
2360
|
+
|
|
2361
|
+
// src/browser/LocalStorageCacheMap.ts
|
|
2362
|
+
import {
|
|
2363
|
+
isComKey as isComKey3,
|
|
2364
|
+
isQueryMatch as isQueryMatch3
|
|
2365
|
+
} from "@fjell/core";
|
|
2366
|
+
var logger17 = logger_default.get("LocalStorageCacheMap");
|
|
2367
|
+
var LocalStorageCacheMap = class _LocalStorageCacheMap extends CacheMap {
|
|
2368
|
+
keyPrefix;
|
|
2369
|
+
normalizedHashFunction;
|
|
2370
|
+
fallbackCache = null;
|
|
2371
|
+
quotaExceeded = false;
|
|
2372
|
+
constructor(types, keyPrefix = "fjell-cache") {
|
|
2373
|
+
super(types);
|
|
2374
|
+
this.keyPrefix = keyPrefix;
|
|
2375
|
+
this.normalizedHashFunction = createNormalizedHashFunction();
|
|
2376
|
+
}
|
|
2377
|
+
getStorageKey(key) {
|
|
2378
|
+
const hashedKey = this.normalizedHashFunction(key);
|
|
2379
|
+
return `${this.keyPrefix}:${hashedKey}`;
|
|
2380
|
+
}
|
|
2381
|
+
initializeFallbackCache() {
|
|
2382
|
+
if (!this.fallbackCache) {
|
|
2383
|
+
this.fallbackCache = new MemoryCacheMap(this.types);
|
|
2384
|
+
logger17.warning("LocalStorage quota exceeded, falling back to in-memory cache");
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
isQuotaExceededError(error) {
|
|
2388
|
+
return error && (error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED" || error.code === 22 || error.code === 1014);
|
|
2389
|
+
}
|
|
2390
|
+
tryCleanupOldEntries() {
|
|
2391
|
+
try {
|
|
2392
|
+
const allEntries = this.collectCacheEntries();
|
|
2393
|
+
return this.removeOldestEntries(allEntries);
|
|
2394
|
+
} catch (error) {
|
|
2395
|
+
logger17.error("Failed to cleanup old localStorage entries", { error });
|
|
2396
|
+
return false;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
collectCacheEntries() {
|
|
2400
|
+
const allEntries = [];
|
|
2401
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
2402
|
+
const key = localStorage.key(i);
|
|
2403
|
+
if (!key || !key.startsWith(this.keyPrefix + ":")) {
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
try {
|
|
2407
|
+
const stored = localStorage.getItem(key);
|
|
2408
|
+
if (stored) {
|
|
2409
|
+
const parsed = JSON.parse(stored);
|
|
2410
|
+
allEntries.push({ key, timestamp: parsed.timestamp || 0 });
|
|
2411
|
+
}
|
|
2412
|
+
} catch {
|
|
2413
|
+
allEntries.push({ key, timestamp: 0 });
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
return allEntries;
|
|
2417
|
+
}
|
|
2418
|
+
removeOldestEntries(allEntries) {
|
|
2419
|
+
allEntries.sort((a, b) => a.timestamp - b.timestamp);
|
|
2420
|
+
const toRemove = Math.ceil(allEntries.length * 0.25);
|
|
2421
|
+
for (let i = 0; i < toRemove; i++) {
|
|
2422
|
+
localStorage.removeItem(allEntries[i].key);
|
|
2423
|
+
}
|
|
2424
|
+
logger17.info(`Cleaned up ${toRemove} old localStorage entries to free space`);
|
|
2425
|
+
return toRemove > 0;
|
|
2426
|
+
}
|
|
2427
|
+
getAllStorageKeys() {
|
|
2428
|
+
const keys = [];
|
|
2429
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
2430
|
+
const key = localStorage.key(i);
|
|
2431
|
+
if (key && key.startsWith(`${this.keyPrefix}:`)) {
|
|
2432
|
+
keys.push(key);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
return keys;
|
|
2436
|
+
}
|
|
2437
|
+
get(key) {
|
|
2438
|
+
logger17.trace("get", { key });
|
|
2439
|
+
if (this.quotaExceeded && this.fallbackCache) {
|
|
2440
|
+
const memoryResult = this.fallbackCache.get(key);
|
|
2441
|
+
if (memoryResult) {
|
|
2442
|
+
return memoryResult;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
try {
|
|
2446
|
+
const storageKey = this.getStorageKey(key);
|
|
2447
|
+
const stored = localStorage.getItem(storageKey);
|
|
2448
|
+
if (stored) {
|
|
2449
|
+
const parsed = JSON.parse(stored);
|
|
2450
|
+
if (this.normalizedHashFunction(parsed.originalKey) === this.normalizedHashFunction(key)) {
|
|
2451
|
+
return parsed.value;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
if (!this.quotaExceeded && this.fallbackCache) {
|
|
2455
|
+
return this.fallbackCache.get(key);
|
|
2456
|
+
}
|
|
2457
|
+
return null;
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
logger17.error("Error retrieving from localStorage", { key, error });
|
|
2460
|
+
if (this.fallbackCache) {
|
|
2461
|
+
return this.fallbackCache.get(key);
|
|
2462
|
+
}
|
|
2463
|
+
return null;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
getWithTTL(key, ttl) {
|
|
2467
|
+
logger17.trace("getWithTTL", { key, ttl });
|
|
2468
|
+
if (ttl === 0) {
|
|
2469
|
+
return null;
|
|
2470
|
+
}
|
|
2471
|
+
try {
|
|
2472
|
+
const storageKey = this.getStorageKey(key);
|
|
2473
|
+
const stored = localStorage.getItem(storageKey);
|
|
2474
|
+
if (stored) {
|
|
2475
|
+
const parsed = JSON.parse(stored);
|
|
2476
|
+
if (this.normalizedHashFunction(parsed.originalKey) !== this.normalizedHashFunction(key)) {
|
|
2477
|
+
return null;
|
|
2478
|
+
}
|
|
2479
|
+
if (typeof parsed.timestamp === "number" && !isNaN(parsed.timestamp)) {
|
|
2480
|
+
const now = Date.now();
|
|
2481
|
+
const age = now - parsed.timestamp;
|
|
2482
|
+
if (age >= ttl) {
|
|
2483
|
+
logger17.trace("Item expired, removing from localStorage", { key, age, ttl });
|
|
2484
|
+
localStorage.removeItem(storageKey);
|
|
2485
|
+
return null;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
return parsed.value;
|
|
2489
|
+
}
|
|
2490
|
+
return null;
|
|
2491
|
+
} catch (error) {
|
|
2492
|
+
logger17.error("Error retrieving with TTL from localStorage", { key, ttl, error });
|
|
2493
|
+
return null;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
set(key, value) {
|
|
2497
|
+
logger17.trace("set", { key, value });
|
|
2498
|
+
if (this.quotaExceeded && this.fallbackCache) {
|
|
2499
|
+
this.fallbackCache.set(key, value);
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
try {
|
|
2503
|
+
const storageKey = this.getStorageKey(key);
|
|
2504
|
+
const toStore = {
|
|
2505
|
+
originalKey: key,
|
|
2506
|
+
value,
|
|
2507
|
+
timestamp: Date.now()
|
|
2508
|
+
};
|
|
2509
|
+
localStorage.setItem(storageKey, JSON.stringify(toStore));
|
|
2510
|
+
if (this.quotaExceeded) {
|
|
2511
|
+
logger17.info("LocalStorage is working again, switching back from fallback cache");
|
|
2512
|
+
this.quotaExceeded = false;
|
|
2513
|
+
}
|
|
2514
|
+
} catch (error) {
|
|
2515
|
+
logger17.error("Error storing to localStorage", { key, value, error });
|
|
2516
|
+
if (this.isQuotaExceededError(error)) {
|
|
2517
|
+
logger17.warning("LocalStorage quota exceeded, attempting cleanup...");
|
|
2518
|
+
const cleanupSucceeded = this.tryCleanupOldEntries();
|
|
2519
|
+
if (cleanupSucceeded) {
|
|
2520
|
+
try {
|
|
2521
|
+
const storageKey = this.getStorageKey(key);
|
|
2522
|
+
const toStore = {
|
|
2523
|
+
originalKey: key,
|
|
2524
|
+
value,
|
|
2525
|
+
timestamp: Date.now()
|
|
2526
|
+
};
|
|
2527
|
+
localStorage.setItem(storageKey, JSON.stringify(toStore));
|
|
2528
|
+
logger17.info("Successfully stored item after cleanup");
|
|
2529
|
+
return;
|
|
2530
|
+
} catch {
|
|
2531
|
+
logger17.warning("Storage failed even after cleanup, falling back to memory cache");
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
this.quotaExceeded = true;
|
|
2535
|
+
this.initializeFallbackCache();
|
|
2536
|
+
if (this.fallbackCache) {
|
|
2537
|
+
this.fallbackCache.set(key, value);
|
|
2538
|
+
logger17.info("Item stored in fallback memory cache");
|
|
2539
|
+
}
|
|
2540
|
+
} else {
|
|
2541
|
+
throw new Error(`Failed to store item in localStorage: ${error}`);
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
includesKey(key) {
|
|
2546
|
+
if (this.quotaExceeded && this.fallbackCache) {
|
|
2547
|
+
if (this.fallbackCache.includesKey(key)) {
|
|
2548
|
+
return true;
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
try {
|
|
2552
|
+
const storageKey = this.getStorageKey(key);
|
|
2553
|
+
const stored = localStorage.getItem(storageKey);
|
|
2554
|
+
if (stored) {
|
|
2555
|
+
const parsed = JSON.parse(stored);
|
|
2556
|
+
return this.normalizedHashFunction(parsed.originalKey) === this.normalizedHashFunction(key);
|
|
2557
|
+
}
|
|
2558
|
+
if (!this.quotaExceeded && this.fallbackCache) {
|
|
2559
|
+
return this.fallbackCache.includesKey(key);
|
|
2560
|
+
}
|
|
2561
|
+
return false;
|
|
2562
|
+
} catch (error) {
|
|
2563
|
+
logger17.error("Error checking key in localStorage", { key, error });
|
|
2564
|
+
if (this.fallbackCache) {
|
|
2565
|
+
return this.fallbackCache.includesKey(key);
|
|
2566
|
+
}
|
|
2567
|
+
return false;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
delete(key) {
|
|
2571
|
+
logger17.trace("delete", { key });
|
|
2572
|
+
if (this.fallbackCache) {
|
|
2573
|
+
this.fallbackCache.delete(key);
|
|
2574
|
+
}
|
|
2575
|
+
try {
|
|
2576
|
+
const storageKey = this.getStorageKey(key);
|
|
2577
|
+
localStorage.removeItem(storageKey);
|
|
2578
|
+
} catch (error) {
|
|
2579
|
+
logger17.error("Error deleting from localStorage", { key, error });
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
allIn(locations) {
|
|
2583
|
+
const allKeys = this.keys();
|
|
2584
|
+
if (locations.length === 0) {
|
|
2585
|
+
logger17.debug("Returning all items, LocKeys is empty");
|
|
2586
|
+
return allKeys.map((key) => this.get(key)).filter((item) => item !== null);
|
|
2587
|
+
} else {
|
|
2588
|
+
const locKeys = locations;
|
|
2589
|
+
logger17.debug("allIn", { locKeys, keys: allKeys.length });
|
|
2590
|
+
return allKeys.filter((key) => key && isComKey3(key)).filter((key) => {
|
|
2591
|
+
const ComKey12 = key;
|
|
2592
|
+
logger17.debug("Comparing Location Keys", {
|
|
2593
|
+
locKeys,
|
|
2594
|
+
ComKey: ComKey12
|
|
2595
|
+
});
|
|
2596
|
+
return isLocKeyArrayEqual(locKeys, ComKey12.loc);
|
|
2597
|
+
}).map((key) => this.get(key));
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
contains(query, locations) {
|
|
2601
|
+
logger17.debug("contains", { query, locations });
|
|
2602
|
+
const items = this.allIn(locations);
|
|
2603
|
+
return items.some((item) => isQueryMatch3(item, query));
|
|
2604
|
+
}
|
|
2605
|
+
queryIn(query, locations = []) {
|
|
2606
|
+
logger17.debug("queryIn", { query, locations });
|
|
2607
|
+
const items = this.allIn(locations);
|
|
2608
|
+
return items.filter((item) => isQueryMatch3(item, query));
|
|
2609
|
+
}
|
|
2610
|
+
clone() {
|
|
2611
|
+
return new _LocalStorageCacheMap(this.types, this.keyPrefix);
|
|
2612
|
+
}
|
|
2613
|
+
parseStorageEntry(storageKey) {
|
|
2614
|
+
try {
|
|
2615
|
+
const stored = localStorage.getItem(storageKey);
|
|
2616
|
+
if (stored) {
|
|
2617
|
+
return JSON.parse(stored);
|
|
2618
|
+
}
|
|
2619
|
+
} catch (parseError) {
|
|
2620
|
+
logger17.debug("Skipping corrupted localStorage entry", { storageKey, error: parseError });
|
|
2621
|
+
}
|
|
2622
|
+
return null;
|
|
2623
|
+
}
|
|
2624
|
+
keys() {
|
|
2625
|
+
const keys = [];
|
|
2626
|
+
try {
|
|
2627
|
+
const storageKeys = this.getAllStorageKeys();
|
|
2628
|
+
for (const storageKey of storageKeys) {
|
|
2629
|
+
const parsed = this.parseStorageEntry(storageKey);
|
|
2630
|
+
if (parsed?.originalKey) {
|
|
2631
|
+
keys.push(parsed.originalKey);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
} catch (error) {
|
|
2635
|
+
logger17.error("Error getting keys from localStorage", { error });
|
|
2636
|
+
}
|
|
2637
|
+
return keys;
|
|
2638
|
+
}
|
|
2639
|
+
values() {
|
|
2640
|
+
const values = [];
|
|
2641
|
+
try {
|
|
2642
|
+
const storageKeys = this.getAllStorageKeys();
|
|
2643
|
+
for (const storageKey of storageKeys) {
|
|
2644
|
+
const parsed = this.parseStorageEntry(storageKey);
|
|
2645
|
+
if (parsed?.value) {
|
|
2646
|
+
values.push(parsed.value);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
} catch (error) {
|
|
2650
|
+
logger17.error("Error getting values from localStorage", { error });
|
|
2651
|
+
}
|
|
2652
|
+
return values;
|
|
2653
|
+
}
|
|
2654
|
+
clear() {
|
|
2655
|
+
logger17.debug("Clearing localStorage cache");
|
|
2656
|
+
try {
|
|
2657
|
+
const storageKeys = this.getAllStorageKeys();
|
|
2658
|
+
for (const storageKey of storageKeys) {
|
|
2659
|
+
if (!storageKey.includes(":query:")) {
|
|
2660
|
+
localStorage.removeItem(storageKey);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
} catch (error) {
|
|
2664
|
+
logger17.error("Error clearing localStorage cache", { error });
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
// Query result caching methods implementation
|
|
2668
|
+
setQueryResult(queryHash, itemKeys, ttl) {
|
|
2669
|
+
logger17.trace("setQueryResult", { queryHash, itemKeys, ttl });
|
|
2670
|
+
const queryKey = `${this.keyPrefix}:query:${queryHash}`;
|
|
2671
|
+
const entry = {
|
|
2672
|
+
itemKeys
|
|
2673
|
+
};
|
|
2674
|
+
if (ttl) {
|
|
2675
|
+
entry.expiresAt = Date.now() + ttl;
|
|
2676
|
+
}
|
|
2677
|
+
try {
|
|
2678
|
+
localStorage.setItem(queryKey, JSON.stringify(entry));
|
|
2679
|
+
} catch (error) {
|
|
2680
|
+
logger17.error("Failed to store query result in localStorage", { queryHash, error });
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
getQueryResult(queryHash) {
|
|
2684
|
+
logger17.trace("getQueryResult", { queryHash });
|
|
2685
|
+
const queryKey = `${this.keyPrefix}:query:${queryHash}`;
|
|
2686
|
+
try {
|
|
2687
|
+
const data = localStorage.getItem(queryKey);
|
|
2688
|
+
if (!data) {
|
|
2689
|
+
return null;
|
|
2690
|
+
}
|
|
2691
|
+
const entry = JSON.parse(data);
|
|
2692
|
+
if (Array.isArray(entry)) {
|
|
2693
|
+
return entry;
|
|
2694
|
+
}
|
|
2695
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
2696
|
+
logger17.trace("Query result expired, removing", { queryHash, expiresAt: entry.expiresAt });
|
|
2697
|
+
localStorage.removeItem(queryKey);
|
|
2698
|
+
return null;
|
|
2699
|
+
}
|
|
2700
|
+
return entry.itemKeys || null;
|
|
2701
|
+
} catch (error) {
|
|
2702
|
+
logger17.error("Failed to retrieve query result from localStorage", { queryHash, error });
|
|
2703
|
+
return null;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
hasQueryResult(queryHash) {
|
|
2707
|
+
return this.getQueryResult(queryHash) !== null;
|
|
2708
|
+
}
|
|
2709
|
+
deleteQueryResult(queryHash) {
|
|
2710
|
+
logger17.trace("deleteQueryResult", { queryHash });
|
|
2711
|
+
const queryKey = `${this.keyPrefix}:query:${queryHash}`;
|
|
2712
|
+
try {
|
|
2713
|
+
localStorage.removeItem(queryKey);
|
|
2714
|
+
} catch (error) {
|
|
2715
|
+
logger17.error("Failed to delete query result from localStorage", { queryHash, error });
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
invalidateItemKeys(keys) {
|
|
2719
|
+
logger17.debug("invalidateItemKeys", { keys });
|
|
2720
|
+
keys.forEach((key) => {
|
|
2721
|
+
this.delete(key);
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
invalidateLocation(locations) {
|
|
2725
|
+
logger17.debug("invalidateLocation", { locations });
|
|
2726
|
+
if (locations.length === 0) {
|
|
2727
|
+
const allKeys = this.keys();
|
|
2728
|
+
const primaryKeys = allKeys.filter((key) => !isComKey3(key));
|
|
2729
|
+
this.invalidateItemKeys(primaryKeys);
|
|
2730
|
+
} else {
|
|
2731
|
+
const itemsInLocation = this.allIn(locations);
|
|
2732
|
+
const keysToInvalidate = itemsInLocation.map((item) => item.key);
|
|
2733
|
+
this.invalidateItemKeys(keysToInvalidate);
|
|
2734
|
+
}
|
|
2735
|
+
this.clearQueryResults();
|
|
2736
|
+
}
|
|
2737
|
+
clearQueryResults() {
|
|
2738
|
+
logger17.trace("clearQueryResults");
|
|
2739
|
+
const queryPrefix = `${this.keyPrefix}:query:`;
|
|
2740
|
+
try {
|
|
2741
|
+
const keysToRemove = [];
|
|
2742
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
2743
|
+
const key = localStorage.key(i);
|
|
2744
|
+
if (key && key.startsWith(queryPrefix)) {
|
|
2745
|
+
keysToRemove.push(key);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
|
2749
|
+
} catch (error) {
|
|
2750
|
+
logger17.error("Failed to clear query results from localStorage", { error });
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
};
|
|
2754
|
+
|
|
2755
|
+
// src/browser/SessionStorageCacheMap.ts
|
|
2756
|
+
import {
|
|
2757
|
+
isComKey as isComKey4,
|
|
2758
|
+
isQueryMatch as isQueryMatch4
|
|
2759
|
+
} from "@fjell/core";
|
|
2760
|
+
var logger18 = logger_default.get("SessionStorageCacheMap");
|
|
2761
|
+
var SessionStorageCacheMap = class _SessionStorageCacheMap extends CacheMap {
|
|
2762
|
+
keyPrefix;
|
|
2763
|
+
normalizedHashFunction;
|
|
2764
|
+
constructor(types, keyPrefix = "fjell-session-cache") {
|
|
2765
|
+
super(types);
|
|
2766
|
+
this.keyPrefix = keyPrefix;
|
|
2767
|
+
this.normalizedHashFunction = createNormalizedHashFunction();
|
|
2768
|
+
}
|
|
2769
|
+
getStorageKey(key) {
|
|
2770
|
+
const hashedKey = this.normalizedHashFunction(key);
|
|
2771
|
+
return `${this.keyPrefix}:${hashedKey}`;
|
|
2772
|
+
}
|
|
2773
|
+
getAllStorageKeys() {
|
|
2774
|
+
const keys = [];
|
|
2775
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
2776
|
+
const key = sessionStorage.key(i);
|
|
2777
|
+
if (key && key.startsWith(`${this.keyPrefix}:`)) {
|
|
2778
|
+
keys.push(key);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
return keys;
|
|
2782
|
+
}
|
|
2783
|
+
get(key) {
|
|
2784
|
+
logger18.trace("get", { key });
|
|
2785
|
+
try {
|
|
2786
|
+
const storageKey = this.getStorageKey(key);
|
|
2787
|
+
const stored = sessionStorage.getItem(storageKey);
|
|
2788
|
+
if (stored) {
|
|
2789
|
+
const parsed = JSON.parse(stored);
|
|
2790
|
+
if (this.normalizedHashFunction(parsed.originalKey) === this.normalizedHashFunction(key)) {
|
|
2791
|
+
return parsed.value;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return null;
|
|
2795
|
+
} catch (error) {
|
|
2796
|
+
logger18.error("Error retrieving from sessionStorage", { key, error });
|
|
2797
|
+
return null;
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
getWithTTL(key, ttl) {
|
|
2801
|
+
logger18.trace("getWithTTL", { key, ttl });
|
|
2802
|
+
if (ttl === 0) {
|
|
2803
|
+
return null;
|
|
2804
|
+
}
|
|
2805
|
+
try {
|
|
2806
|
+
const storageKey = this.getStorageKey(key);
|
|
2807
|
+
const stored = sessionStorage.getItem(storageKey);
|
|
2808
|
+
if (stored) {
|
|
2809
|
+
const parsed = JSON.parse(stored);
|
|
2810
|
+
if (this.normalizedHashFunction(parsed.originalKey) !== this.normalizedHashFunction(key)) {
|
|
2811
|
+
return null;
|
|
2812
|
+
}
|
|
2813
|
+
if (typeof parsed.timestamp === "number" && !isNaN(parsed.timestamp)) {
|
|
2814
|
+
const now = Date.now();
|
|
2815
|
+
const age = now - parsed.timestamp;
|
|
2816
|
+
if (age >= ttl) {
|
|
2817
|
+
logger18.trace("Item expired, removing from sessionStorage", { key, age, ttl });
|
|
2818
|
+
sessionStorage.removeItem(storageKey);
|
|
2819
|
+
return null;
|
|
2820
|
+
}
|
|
2821
|
+
} else {
|
|
2822
|
+
logger18.trace("Item has no valid timestamp, treating as expired", { key });
|
|
2823
|
+
sessionStorage.removeItem(storageKey);
|
|
2824
|
+
return null;
|
|
2825
|
+
}
|
|
2826
|
+
return parsed.value;
|
|
2827
|
+
}
|
|
2828
|
+
return null;
|
|
2829
|
+
} catch (error) {
|
|
2830
|
+
logger18.error("Error retrieving with TTL from sessionStorage", { key, ttl, error });
|
|
2831
|
+
return null;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
set(key, value) {
|
|
2835
|
+
logger18.trace("set", { key, value });
|
|
2836
|
+
try {
|
|
2837
|
+
const storageKey = this.getStorageKey(key);
|
|
2838
|
+
const toStore = {
|
|
2839
|
+
originalKey: key,
|
|
2840
|
+
value,
|
|
2841
|
+
timestamp: Date.now()
|
|
2842
|
+
};
|
|
2843
|
+
sessionStorage.setItem(storageKey, JSON.stringify(toStore));
|
|
2844
|
+
} catch (error) {
|
|
2845
|
+
logger18.error("Error storing to sessionStorage", { key, value, error });
|
|
2846
|
+
throw new Error(`Failed to store item in sessionStorage: ${error}`);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
includesKey(key) {
|
|
2850
|
+
try {
|
|
2851
|
+
const storageKey = this.getStorageKey(key);
|
|
2852
|
+
const stored = sessionStorage.getItem(storageKey);
|
|
2853
|
+
if (stored) {
|
|
2854
|
+
const parsed = JSON.parse(stored);
|
|
2855
|
+
return this.normalizedHashFunction(parsed.originalKey) === this.normalizedHashFunction(key);
|
|
2856
|
+
}
|
|
2857
|
+
return false;
|
|
2858
|
+
} catch (error) {
|
|
2859
|
+
logger18.error("Error checking key in sessionStorage", { key, error });
|
|
2860
|
+
return false;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
delete(key) {
|
|
2864
|
+
logger18.trace("delete", { key });
|
|
2865
|
+
try {
|
|
2866
|
+
const storageKey = this.getStorageKey(key);
|
|
2867
|
+
sessionStorage.removeItem(storageKey);
|
|
2868
|
+
} catch (error) {
|
|
2869
|
+
logger18.error("Error deleting from sessionStorage", { key, error });
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
allIn(locations) {
|
|
2873
|
+
const allKeys = this.keys();
|
|
2874
|
+
if (locations.length === 0) {
|
|
2875
|
+
logger18.debug("Returning all items, LocKeys is empty");
|
|
2876
|
+
return allKeys.map((key) => this.get(key)).filter((item) => item !== null);
|
|
2877
|
+
} else {
|
|
2878
|
+
const locKeys = locations;
|
|
2879
|
+
logger18.debug("allIn", { locKeys, keys: allKeys.length });
|
|
2880
|
+
return allKeys.filter((key) => key && isComKey4(key)).filter((key) => {
|
|
2881
|
+
const ComKey12 = key;
|
|
2882
|
+
logger18.debug("Comparing Location Keys", {
|
|
2883
|
+
locKeys,
|
|
2884
|
+
ComKey: ComKey12
|
|
2885
|
+
});
|
|
2886
|
+
return isLocKeyArrayEqual(locKeys, ComKey12.loc);
|
|
2887
|
+
}).map((key) => this.get(key));
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
contains(query, locations) {
|
|
2891
|
+
logger18.debug("contains", { query, locations });
|
|
2892
|
+
const items = this.allIn(locations);
|
|
2893
|
+
return items.some((item) => isQueryMatch4(item, query));
|
|
2894
|
+
}
|
|
2895
|
+
queryIn(query, locations = []) {
|
|
2896
|
+
logger18.debug("queryIn", { query, locations });
|
|
2897
|
+
const items = this.allIn(locations);
|
|
2898
|
+
return items.filter((item) => isQueryMatch4(item, query));
|
|
2899
|
+
}
|
|
2900
|
+
clone() {
|
|
2901
|
+
return new _SessionStorageCacheMap(this.types, this.keyPrefix);
|
|
2902
|
+
}
|
|
2903
|
+
keys() {
|
|
2904
|
+
const keys = [];
|
|
2905
|
+
try {
|
|
2906
|
+
const storageKeys = this.getAllStorageKeys();
|
|
2907
|
+
for (const storageKey of storageKeys) {
|
|
2908
|
+
const stored = sessionStorage.getItem(storageKey);
|
|
2909
|
+
if (!stored) continue;
|
|
2910
|
+
try {
|
|
2911
|
+
const parsed = JSON.parse(stored);
|
|
2912
|
+
if (parsed.originalKey) {
|
|
2913
|
+
keys.push(parsed.originalKey);
|
|
2914
|
+
}
|
|
2915
|
+
} catch (itemError) {
|
|
2916
|
+
logger18.trace("Skipping invalid storage item", { storageKey, error: itemError });
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
} catch (error) {
|
|
2920
|
+
logger18.error("Error getting keys from sessionStorage", { error });
|
|
2921
|
+
}
|
|
2922
|
+
return keys;
|
|
2923
|
+
}
|
|
2924
|
+
values() {
|
|
2925
|
+
const values = [];
|
|
2926
|
+
try {
|
|
2927
|
+
const storageKeys = this.getAllStorageKeys();
|
|
2928
|
+
for (const storageKey of storageKeys) {
|
|
2929
|
+
const stored = sessionStorage.getItem(storageKey);
|
|
2930
|
+
if (!stored) continue;
|
|
2931
|
+
try {
|
|
2932
|
+
const parsed = JSON.parse(stored);
|
|
2933
|
+
if (parsed.value != null) {
|
|
2934
|
+
values.push(parsed.value);
|
|
2935
|
+
}
|
|
2936
|
+
} catch (itemError) {
|
|
2937
|
+
logger18.trace("Skipping invalid storage item for values", { storageKey, error: itemError });
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
} catch (error) {
|
|
2941
|
+
logger18.error("Error getting values from sessionStorage", { error });
|
|
2942
|
+
}
|
|
2943
|
+
return values;
|
|
2944
|
+
}
|
|
2945
|
+
clear() {
|
|
2946
|
+
logger18.debug("Clearing sessionStorage cache");
|
|
2947
|
+
try {
|
|
2948
|
+
const storageKeys = this.getAllStorageKeys();
|
|
2949
|
+
for (const storageKey of storageKeys) {
|
|
2950
|
+
sessionStorage.removeItem(storageKey);
|
|
2951
|
+
}
|
|
2952
|
+
} catch (error) {
|
|
2953
|
+
logger18.error("Error clearing sessionStorage cache", { error });
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
// Query result caching methods implementation
|
|
2957
|
+
setQueryResult(queryHash, itemKeys, ttl) {
|
|
2958
|
+
logger18.trace("setQueryResult", { queryHash, itemKeys, ttl });
|
|
2959
|
+
const queryKey = `${this.keyPrefix}:query:${queryHash}`;
|
|
2960
|
+
const entry = {
|
|
2961
|
+
itemKeys
|
|
2962
|
+
};
|
|
2963
|
+
if (ttl) {
|
|
2964
|
+
entry.expiresAt = Date.now() + ttl;
|
|
2965
|
+
}
|
|
2966
|
+
try {
|
|
2967
|
+
sessionStorage.setItem(queryKey, JSON.stringify(entry));
|
|
2968
|
+
} catch (error) {
|
|
2969
|
+
logger18.error("Failed to store query result in sessionStorage", { queryHash, error });
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
getQueryResult(queryHash) {
|
|
2973
|
+
logger18.trace("getQueryResult", { queryHash });
|
|
2974
|
+
const queryKey = `${this.keyPrefix}:query:${queryHash}`;
|
|
2975
|
+
try {
|
|
2976
|
+
const data = sessionStorage.getItem(queryKey);
|
|
2977
|
+
if (!data) {
|
|
2978
|
+
return null;
|
|
2979
|
+
}
|
|
2980
|
+
const entry = JSON.parse(data);
|
|
2981
|
+
if (Array.isArray(entry)) {
|
|
2982
|
+
return entry;
|
|
2983
|
+
}
|
|
2984
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
2985
|
+
logger18.trace("Query result expired, removing", { queryHash, expiresAt: entry.expiresAt });
|
|
2986
|
+
sessionStorage.removeItem(queryKey);
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
return entry.itemKeys || null;
|
|
2990
|
+
} catch (error) {
|
|
2991
|
+
logger18.error("Failed to retrieve query result from sessionStorage", { queryHash, error });
|
|
2992
|
+
return null;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
hasQueryResult(queryHash) {
|
|
2996
|
+
return this.getQueryResult(queryHash) !== null;
|
|
2997
|
+
}
|
|
2998
|
+
deleteQueryResult(queryHash) {
|
|
2999
|
+
logger18.trace("deleteQueryResult", { queryHash });
|
|
3000
|
+
const queryKey = `${this.keyPrefix}:query:${queryHash}`;
|
|
3001
|
+
try {
|
|
3002
|
+
sessionStorage.removeItem(queryKey);
|
|
3003
|
+
} catch (error) {
|
|
3004
|
+
logger18.error("Failed to delete query result from sessionStorage", { queryHash, error });
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
invalidateItemKeys(keys) {
|
|
3008
|
+
logger18.debug("invalidateItemKeys", { keys });
|
|
3009
|
+
keys.forEach((key) => {
|
|
3010
|
+
this.delete(key);
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
invalidateLocation(locations) {
|
|
3014
|
+
logger18.debug("invalidateLocation", { locations });
|
|
3015
|
+
if (locations.length === 0) {
|
|
3016
|
+
const allKeys = this.keys();
|
|
3017
|
+
const primaryKeys = allKeys.filter((key) => !isComKey4(key));
|
|
3018
|
+
this.invalidateItemKeys(primaryKeys);
|
|
3019
|
+
} else {
|
|
3020
|
+
const itemsInLocation = this.allIn(locations);
|
|
3021
|
+
const keysToInvalidate = itemsInLocation.map((item) => item.key);
|
|
3022
|
+
this.invalidateItemKeys(keysToInvalidate);
|
|
3023
|
+
}
|
|
3024
|
+
this.clearQueryResults();
|
|
3025
|
+
}
|
|
3026
|
+
clearQueryResults() {
|
|
3027
|
+
logger18.trace("clearQueryResults");
|
|
3028
|
+
const queryPrefix = `${this.keyPrefix}:query:`;
|
|
3029
|
+
try {
|
|
3030
|
+
const keysToRemove = [];
|
|
3031
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
3032
|
+
const key = sessionStorage.key(i);
|
|
3033
|
+
if (key && key.startsWith(queryPrefix)) {
|
|
3034
|
+
keysToRemove.push(key);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
keysToRemove.forEach((key) => sessionStorage.removeItem(key));
|
|
3038
|
+
} catch (error) {
|
|
3039
|
+
logger18.error("Failed to clear query results from sessionStorage", { error });
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
};
|
|
3043
|
+
|
|
3044
|
+
// src/browser/AsyncIndexDBCacheMap.ts
|
|
3045
|
+
import {
|
|
3046
|
+
isComKey as isComKey5,
|
|
3047
|
+
isQueryMatch as isQueryMatch5
|
|
3048
|
+
} from "@fjell/core";
|
|
3049
|
+
var logger19 = logger_default.get("AsyncIndexDBCacheMap");
|
|
3050
|
+
var AsyncIndexDBCacheMap = class _AsyncIndexDBCacheMap {
|
|
3051
|
+
types;
|
|
3052
|
+
dbName;
|
|
3053
|
+
storeName;
|
|
3054
|
+
version;
|
|
3055
|
+
normalizedHashFunction;
|
|
3056
|
+
dbPromise = null;
|
|
3057
|
+
constructor(types, dbName = "fjell-indexdb-cache", storeName = "cache", version = 1) {
|
|
3058
|
+
this.types = types;
|
|
3059
|
+
this.dbName = dbName;
|
|
3060
|
+
this.storeName = storeName;
|
|
3061
|
+
this.version = version;
|
|
3062
|
+
this.normalizedHashFunction = createNormalizedHashFunction();
|
|
3063
|
+
}
|
|
3064
|
+
async getDB() {
|
|
3065
|
+
if (!this.dbPromise) {
|
|
3066
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
3067
|
+
const request = indexedDB.open(this.dbName, this.version);
|
|
3068
|
+
request.onerror = () => {
|
|
3069
|
+
logger19.error("Error opening IndexedDB", { error: request.error });
|
|
3070
|
+
reject(request.error);
|
|
3071
|
+
};
|
|
3072
|
+
request.onsuccess = () => {
|
|
3073
|
+
logger19.debug("IndexedDB opened successfully");
|
|
3074
|
+
resolve(request.result);
|
|
3075
|
+
};
|
|
3076
|
+
request.onupgradeneeded = (event) => {
|
|
3077
|
+
logger19.debug("IndexedDB upgrade needed");
|
|
3078
|
+
const db = event.target.result;
|
|
3079
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
3080
|
+
db.createObjectStore(this.storeName);
|
|
3081
|
+
logger19.debug("Created object store", { storeName: this.storeName });
|
|
3082
|
+
}
|
|
3083
|
+
};
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
return this.dbPromise;
|
|
3087
|
+
}
|
|
3088
|
+
getStorageKey(key) {
|
|
3089
|
+
return this.normalizedHashFunction(key);
|
|
3090
|
+
}
|
|
3091
|
+
async get(key) {
|
|
3092
|
+
logger19.trace("get", { key });
|
|
3093
|
+
try {
|
|
3094
|
+
const db = await this.getDB();
|
|
3095
|
+
const transaction = db.transaction([this.storeName], "readonly");
|
|
3096
|
+
const store = transaction.objectStore(this.storeName);
|
|
3097
|
+
const storageKey = this.getStorageKey(key);
|
|
3098
|
+
return new Promise((resolve, reject) => {
|
|
3099
|
+
const request = store.get(storageKey);
|
|
3100
|
+
request.onerror = () => {
|
|
3101
|
+
logger19.error("Error getting from IndexedDB", { key, error: request.error });
|
|
3102
|
+
reject(request.error);
|
|
3103
|
+
};
|
|
3104
|
+
request.onsuccess = () => {
|
|
3105
|
+
const stored = request.result;
|
|
3106
|
+
if (stored && this.normalizedHashFunction(stored.originalKey) === this.normalizedHashFunction(key)) {
|
|
3107
|
+
resolve(stored.value);
|
|
3108
|
+
} else {
|
|
3109
|
+
resolve(null);
|
|
3110
|
+
}
|
|
3111
|
+
};
|
|
3112
|
+
});
|
|
3113
|
+
} catch (error) {
|
|
3114
|
+
logger19.error("Error in IndexedDB get operation", { key, error });
|
|
3115
|
+
return null;
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
async set(key, value) {
|
|
3119
|
+
logger19.trace("set", { key, value });
|
|
3120
|
+
try {
|
|
3121
|
+
const db = await this.getDB();
|
|
3122
|
+
const transaction = db.transaction([this.storeName], "readwrite");
|
|
3123
|
+
const store = transaction.objectStore(this.storeName);
|
|
3124
|
+
const storageKey = this.getStorageKey(key);
|
|
3125
|
+
const storedItem = {
|
|
3126
|
+
originalKey: key,
|
|
3127
|
+
value
|
|
3128
|
+
};
|
|
3129
|
+
return new Promise((resolve, reject) => {
|
|
3130
|
+
const request = store.put(storedItem, storageKey);
|
|
3131
|
+
request.onerror = () => {
|
|
3132
|
+
logger19.error("Error setting in IndexedDB", { key, value, error: request.error });
|
|
3133
|
+
reject(request.error);
|
|
3134
|
+
};
|
|
3135
|
+
request.onsuccess = () => {
|
|
3136
|
+
resolve();
|
|
3137
|
+
};
|
|
3138
|
+
});
|
|
3139
|
+
} catch (error) {
|
|
3140
|
+
logger19.error("Error in IndexedDB set operation", { key, value, error });
|
|
3141
|
+
throw new Error(`Failed to store item in IndexedDB: ${error}`);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
async includesKey(key) {
|
|
3145
|
+
try {
|
|
3146
|
+
const db = await this.getDB();
|
|
3147
|
+
const transaction = db.transaction([this.storeName], "readonly");
|
|
3148
|
+
const store = transaction.objectStore(this.storeName);
|
|
3149
|
+
const storageKey = this.getStorageKey(key);
|
|
3150
|
+
return new Promise((resolve, reject) => {
|
|
3151
|
+
const request = store.get(storageKey);
|
|
3152
|
+
request.onerror = () => {
|
|
3153
|
+
logger19.error("Error checking key in IndexedDB", { key, error: request.error });
|
|
3154
|
+
reject(request.error);
|
|
3155
|
+
};
|
|
3156
|
+
request.onsuccess = () => {
|
|
3157
|
+
const stored = request.result;
|
|
3158
|
+
if (stored) {
|
|
3159
|
+
const matches = this.normalizedHashFunction(stored.originalKey) === this.normalizedHashFunction(key);
|
|
3160
|
+
resolve(matches);
|
|
3161
|
+
} else {
|
|
3162
|
+
resolve(false);
|
|
3163
|
+
}
|
|
3164
|
+
};
|
|
3165
|
+
});
|
|
3166
|
+
} catch (error) {
|
|
3167
|
+
logger19.error("Error in IndexedDB includesKey operation", { key, error });
|
|
3168
|
+
return false;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
async delete(key) {
|
|
3172
|
+
logger19.trace("delete", { key });
|
|
3173
|
+
try {
|
|
3174
|
+
const db = await this.getDB();
|
|
3175
|
+
const transaction = db.transaction([this.storeName], "readwrite");
|
|
3176
|
+
const store = transaction.objectStore(this.storeName);
|
|
3177
|
+
const storageKey = this.getStorageKey(key);
|
|
3178
|
+
return new Promise((resolve, reject) => {
|
|
3179
|
+
const request = store.delete(storageKey);
|
|
3180
|
+
request.onerror = () => {
|
|
3181
|
+
logger19.error("Error deleting from IndexedDB", { key, error: request.error });
|
|
3182
|
+
reject(request.error);
|
|
3183
|
+
};
|
|
3184
|
+
request.onsuccess = () => {
|
|
3185
|
+
resolve();
|
|
3186
|
+
};
|
|
3187
|
+
});
|
|
3188
|
+
} catch (error) {
|
|
3189
|
+
logger19.error("Error in IndexedDB delete operation", { key, error });
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
async allIn(locations) {
|
|
3193
|
+
const allKeys = await this.keys();
|
|
3194
|
+
if (locations.length === 0) {
|
|
3195
|
+
logger19.debug("Returning all items, LocKeys is empty");
|
|
3196
|
+
const promises = allKeys.map((key) => this.get(key));
|
|
3197
|
+
const results = await Promise.all(promises);
|
|
3198
|
+
return results.filter((item) => item !== null);
|
|
3199
|
+
} else {
|
|
3200
|
+
const locKeys = locations;
|
|
3201
|
+
logger19.debug("allIn", { locKeys, keys: allKeys.length });
|
|
3202
|
+
const filteredKeys = allKeys.filter((key) => key && isComKey5(key)).filter((key) => {
|
|
3203
|
+
const ComKey12 = key;
|
|
3204
|
+
logger19.debug("Comparing Location Keys", {
|
|
3205
|
+
locKeys,
|
|
3206
|
+
ComKey: ComKey12
|
|
3207
|
+
});
|
|
3208
|
+
return isLocKeyArrayEqual(locKeys, ComKey12.loc);
|
|
3209
|
+
});
|
|
3210
|
+
const promises = filteredKeys.map((key) => this.get(key));
|
|
3211
|
+
const results = await Promise.all(promises);
|
|
3212
|
+
return results.filter((item) => item !== null);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
async contains(query, locations) {
|
|
3216
|
+
logger19.debug("contains", { query, locations });
|
|
3217
|
+
const items = await this.allIn(locations);
|
|
3218
|
+
return items.some((item) => isQueryMatch5(item, query));
|
|
3219
|
+
}
|
|
3220
|
+
async queryIn(query, locations = []) {
|
|
3221
|
+
logger19.debug("queryIn", { query, locations });
|
|
3222
|
+
const items = await this.allIn(locations);
|
|
3223
|
+
return items.filter((item) => isQueryMatch5(item, query));
|
|
3224
|
+
}
|
|
3225
|
+
clone() {
|
|
3226
|
+
return new _AsyncIndexDBCacheMap(this.types, this.dbName, this.storeName, this.version);
|
|
3227
|
+
}
|
|
3228
|
+
async keys() {
|
|
3229
|
+
const keys = [];
|
|
3230
|
+
try {
|
|
3231
|
+
const db = await this.getDB();
|
|
3232
|
+
const transaction = db.transaction([this.storeName], "readonly");
|
|
3233
|
+
const store = transaction.objectStore(this.storeName);
|
|
3234
|
+
return new Promise((resolve, reject) => {
|
|
3235
|
+
const request = store.openCursor();
|
|
3236
|
+
request.onerror = () => {
|
|
3237
|
+
logger19.error("Error getting keys from IndexedDB", { error: request.error });
|
|
3238
|
+
reject(request.error);
|
|
3239
|
+
};
|
|
3240
|
+
request.onsuccess = (event) => {
|
|
3241
|
+
const cursor = event.target.result;
|
|
3242
|
+
if (cursor) {
|
|
3243
|
+
const stored = cursor.value;
|
|
3244
|
+
keys.push(stored.originalKey);
|
|
3245
|
+
cursor.continue();
|
|
3246
|
+
} else {
|
|
3247
|
+
resolve(keys);
|
|
3248
|
+
}
|
|
3249
|
+
};
|
|
3250
|
+
});
|
|
3251
|
+
} catch (error) {
|
|
3252
|
+
logger19.error("Error in IndexedDB keys operation", { error });
|
|
3253
|
+
return [];
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
async values() {
|
|
3257
|
+
const values = [];
|
|
3258
|
+
try {
|
|
3259
|
+
const db = await this.getDB();
|
|
3260
|
+
const transaction = db.transaction([this.storeName], "readonly");
|
|
3261
|
+
const store = transaction.objectStore(this.storeName);
|
|
3262
|
+
return new Promise((resolve, reject) => {
|
|
3263
|
+
const request = store.openCursor();
|
|
3264
|
+
request.onerror = () => {
|
|
3265
|
+
logger19.error("Error getting values from IndexedDB", { error: request.error });
|
|
3266
|
+
reject(request.error);
|
|
3267
|
+
};
|
|
3268
|
+
request.onsuccess = (event) => {
|
|
3269
|
+
const cursor = event.target.result;
|
|
3270
|
+
if (cursor) {
|
|
3271
|
+
const stored = cursor.value;
|
|
3272
|
+
values.push(stored.value);
|
|
3273
|
+
cursor.continue();
|
|
3274
|
+
} else {
|
|
3275
|
+
resolve(values);
|
|
3276
|
+
}
|
|
3277
|
+
};
|
|
3278
|
+
});
|
|
3279
|
+
} catch (error) {
|
|
3280
|
+
logger19.error("Error in IndexedDB values operation", { error });
|
|
3281
|
+
return [];
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
async clear() {
|
|
3285
|
+
logger19.debug("Clearing IndexedDB cache");
|
|
3286
|
+
try {
|
|
3287
|
+
const db = await this.getDB();
|
|
3288
|
+
const transaction = db.transaction([this.storeName], "readwrite");
|
|
3289
|
+
const store = transaction.objectStore(this.storeName);
|
|
3290
|
+
return new Promise((resolve, reject) => {
|
|
3291
|
+
const request = store.clear();
|
|
3292
|
+
request.onerror = () => {
|
|
3293
|
+
logger19.error("Error clearing IndexedDB cache", { error: request.error });
|
|
3294
|
+
reject(request.error);
|
|
3295
|
+
};
|
|
3296
|
+
request.onsuccess = () => {
|
|
3297
|
+
resolve();
|
|
3298
|
+
};
|
|
3299
|
+
});
|
|
3300
|
+
} catch (error) {
|
|
3301
|
+
logger19.error("Error in IndexedDB clear operation", { error });
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
// Async Query result caching methods
|
|
3305
|
+
async setQueryResult(queryHash, itemKeys, ttl) {
|
|
3306
|
+
logger19.trace("setQueryResult", { queryHash, itemKeys, ttl });
|
|
3307
|
+
try {
|
|
3308
|
+
return new Promise((resolve, reject) => {
|
|
3309
|
+
const request = indexedDB.open(this.dbName, this.version);
|
|
3310
|
+
request.onerror = () => {
|
|
3311
|
+
logger19.error("Failed to open database for setQueryResult", { error: request.error });
|
|
3312
|
+
reject(request.error);
|
|
3313
|
+
};
|
|
3314
|
+
request.onsuccess = () => {
|
|
3315
|
+
const db = request.result;
|
|
3316
|
+
const transaction = db.transaction([this.storeName], "readwrite");
|
|
3317
|
+
const store = transaction.objectStore(this.storeName);
|
|
3318
|
+
const entry = {
|
|
3319
|
+
itemKeys,
|
|
3320
|
+
expiresAt: ttl ? Date.now() + ttl : null
|
|
3321
|
+
};
|
|
3322
|
+
const queryKey = `query:${queryHash}`;
|
|
3323
|
+
const putRequest = store.put(JSON.stringify(entry), queryKey);
|
|
3324
|
+
putRequest.onerror = () => {
|
|
3325
|
+
logger19.error("Failed to store query result", { queryHash, error: putRequest.error });
|
|
3326
|
+
reject(putRequest.error);
|
|
3327
|
+
};
|
|
3328
|
+
putRequest.onsuccess = () => {
|
|
3329
|
+
resolve();
|
|
3330
|
+
};
|
|
3331
|
+
};
|
|
3332
|
+
});
|
|
3333
|
+
} catch (error) {
|
|
3334
|
+
logger19.error("Error in setQueryResult", { queryHash, error });
|
|
3335
|
+
throw error;
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
async getQueryResult(queryHash) {
|
|
3339
|
+
logger19.trace("getQueryResult", { queryHash });
|
|
3340
|
+
try {
|
|
3341
|
+
return new Promise((resolve, reject) => {
|
|
3342
|
+
const request = indexedDB.open(this.dbName, this.version);
|
|
3343
|
+
request.onerror = () => {
|
|
3344
|
+
logger19.error("Failed to open database for getQueryResult", { error: request.error });
|
|
3345
|
+
reject(request.error);
|
|
3346
|
+
};
|
|
3347
|
+
request.onsuccess = () => {
|
|
3348
|
+
const db = request.result;
|
|
3349
|
+
const transaction = db.transaction([this.storeName], "readonly");
|
|
3350
|
+
const store = transaction.objectStore(this.storeName);
|
|
3351
|
+
const queryKey = `query:${queryHash}`;
|
|
3352
|
+
const getRequest = store.get(queryKey);
|
|
3353
|
+
getRequest.onerror = () => {
|
|
3354
|
+
logger19.error("Failed to retrieve query result", { queryHash, error: getRequest.error });
|
|
3355
|
+
reject(getRequest.error);
|
|
3356
|
+
};
|
|
3357
|
+
getRequest.onsuccess = () => {
|
|
3358
|
+
try {
|
|
3359
|
+
const result = getRequest.result;
|
|
3360
|
+
if (!result) {
|
|
3361
|
+
resolve(null);
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
const entry = JSON.parse(result);
|
|
3365
|
+
if (Array.isArray(entry)) {
|
|
3366
|
+
resolve(entry);
|
|
3367
|
+
return;
|
|
3368
|
+
}
|
|
3369
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
3370
|
+
logger19.trace("Query result expired, removing", { queryHash, expiresAt: entry.expiresAt });
|
|
3371
|
+
const deleteTransaction = db.transaction([this.storeName], "readwrite");
|
|
3372
|
+
const deleteStore = deleteTransaction.objectStore(this.storeName);
|
|
3373
|
+
deleteStore.delete(queryKey);
|
|
3374
|
+
resolve(null);
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
resolve(entry.itemKeys || null);
|
|
3378
|
+
} catch (parseError) {
|
|
3379
|
+
logger19.error("Failed to parse query result", { queryHash, error: parseError });
|
|
3380
|
+
resolve(null);
|
|
3381
|
+
}
|
|
3382
|
+
};
|
|
3383
|
+
};
|
|
3384
|
+
});
|
|
3385
|
+
} catch (error) {
|
|
3386
|
+
logger19.error("Error in getQueryResult", { queryHash, error });
|
|
3387
|
+
return null;
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
async hasQueryResult(queryHash) {
|
|
3391
|
+
logger19.trace("hasQueryResult", { queryHash });
|
|
3392
|
+
try {
|
|
3393
|
+
const result = await this.getQueryResult(queryHash);
|
|
3394
|
+
return result !== null;
|
|
3395
|
+
} catch (error) {
|
|
3396
|
+
logger19.error("Error in hasQueryResult", { queryHash, error });
|
|
3397
|
+
return false;
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
async deleteQueryResult(queryHash) {
|
|
3401
|
+
logger19.trace("deleteQueryResult", { queryHash });
|
|
3402
|
+
try {
|
|
3403
|
+
return new Promise((resolve, reject) => {
|
|
3404
|
+
const request = indexedDB.open(this.dbName, this.version);
|
|
3405
|
+
request.onerror = () => {
|
|
3406
|
+
logger19.error("Failed to open database for deleteQueryResult", { error: request.error });
|
|
3407
|
+
reject(request.error);
|
|
3408
|
+
};
|
|
3409
|
+
request.onsuccess = () => {
|
|
3410
|
+
const db = request.result;
|
|
3411
|
+
const transaction = db.transaction([this.storeName], "readwrite");
|
|
3412
|
+
const store = transaction.objectStore(this.storeName);
|
|
3413
|
+
const queryKey = `query:${queryHash}`;
|
|
3414
|
+
const deleteRequest = store.delete(queryKey);
|
|
3415
|
+
deleteRequest.onerror = () => {
|
|
3416
|
+
logger19.error("Failed to delete query result", { queryHash, error: deleteRequest.error });
|
|
3417
|
+
reject(deleteRequest.error);
|
|
3418
|
+
};
|
|
3419
|
+
deleteRequest.onsuccess = () => {
|
|
3420
|
+
resolve();
|
|
3421
|
+
};
|
|
3422
|
+
};
|
|
3423
|
+
});
|
|
3424
|
+
} catch (error) {
|
|
3425
|
+
logger19.error("Error in deleteQueryResult", { queryHash, error });
|
|
3426
|
+
throw error;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
async invalidateItemKeys(keys) {
|
|
3430
|
+
logger19.debug("invalidateItemKeys", { keys });
|
|
3431
|
+
for (const key of keys) {
|
|
3432
|
+
await this.delete(key);
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
async invalidateLocation(locations) {
|
|
3436
|
+
logger19.debug("invalidateLocation", { locations });
|
|
3437
|
+
if (locations.length === 0) {
|
|
3438
|
+
await this.clearQueryResults();
|
|
3439
|
+
} else {
|
|
3440
|
+
const itemsInLocation = await this.allIn(locations);
|
|
3441
|
+
const keysToInvalidate = itemsInLocation.map((item) => item.key);
|
|
3442
|
+
await this.invalidateItemKeys(keysToInvalidate);
|
|
3443
|
+
}
|
|
3444
|
+
await this.clearQueryResults();
|
|
3445
|
+
}
|
|
3446
|
+
async clearQueryResults() {
|
|
3447
|
+
logger19.trace("clearQueryResults");
|
|
3448
|
+
try {
|
|
3449
|
+
return new Promise((resolve, reject) => {
|
|
3450
|
+
const request = indexedDB.open(this.dbName, this.version);
|
|
3451
|
+
request.onerror = () => {
|
|
3452
|
+
logger19.error("Failed to open database for clearQueryResults", { error: request.error });
|
|
3453
|
+
reject(request.error);
|
|
3454
|
+
};
|
|
3455
|
+
request.onsuccess = () => {
|
|
3456
|
+
const db = request.result;
|
|
3457
|
+
const transaction = db.transaction([this.storeName], "readwrite");
|
|
3458
|
+
const store = transaction.objectStore(this.storeName);
|
|
3459
|
+
const cursorRequest = store.openCursor();
|
|
3460
|
+
const keysToDelete = [];
|
|
3461
|
+
cursorRequest.onerror = () => {
|
|
3462
|
+
logger19.error("Failed to open cursor for clearQueryResults", { error: cursorRequest.error });
|
|
3463
|
+
reject(cursorRequest.error);
|
|
3464
|
+
};
|
|
3465
|
+
cursorRequest.onsuccess = () => {
|
|
3466
|
+
const cursor = cursorRequest.result;
|
|
3467
|
+
if (cursor) {
|
|
3468
|
+
const key = cursor.key;
|
|
3469
|
+
if (typeof key === "string" && key.startsWith("query:")) {
|
|
3470
|
+
keysToDelete.push(key);
|
|
3471
|
+
}
|
|
3472
|
+
cursor.continue();
|
|
3473
|
+
} else {
|
|
3474
|
+
if (keysToDelete.length === 0) {
|
|
3475
|
+
resolve();
|
|
3476
|
+
return;
|
|
3477
|
+
}
|
|
3478
|
+
let deletedCount = 0;
|
|
3479
|
+
const totalToDelete = keysToDelete.length;
|
|
3480
|
+
keysToDelete.forEach((queryKey) => {
|
|
3481
|
+
const deleteRequest = store.delete(queryKey);
|
|
3482
|
+
deleteRequest.onerror = () => {
|
|
3483
|
+
logger19.error("Failed to delete query key", { queryKey, error: deleteRequest.error });
|
|
3484
|
+
deletedCount++;
|
|
3485
|
+
if (deletedCount === totalToDelete) {
|
|
3486
|
+
resolve();
|
|
3487
|
+
}
|
|
3488
|
+
};
|
|
3489
|
+
deleteRequest.onsuccess = () => {
|
|
3490
|
+
deletedCount++;
|
|
3491
|
+
if (deletedCount === totalToDelete) {
|
|
3492
|
+
resolve();
|
|
3493
|
+
}
|
|
3494
|
+
};
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
};
|
|
3498
|
+
};
|
|
3499
|
+
});
|
|
3500
|
+
} catch (error) {
|
|
3501
|
+
logger19.error("Error in clearQueryResults", { error });
|
|
3502
|
+
throw error;
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
};
|
|
3506
|
+
|
|
3507
|
+
// src/browser/IndexDBCacheMap.ts
|
|
3508
|
+
var IndexDBCacheMap = class _IndexDBCacheMap extends CacheMap {
|
|
3509
|
+
asyncCache;
|
|
3510
|
+
memoryCache;
|
|
3511
|
+
syncInterval = null;
|
|
3512
|
+
SYNC_INTERVAL_MS = 5e3;
|
|
3513
|
+
// Sync every 5 seconds
|
|
3514
|
+
pendingSyncOperations = /* @__PURE__ */ new Map();
|
|
3515
|
+
initializationPromise = null;
|
|
3516
|
+
isInitialized = false;
|
|
3517
|
+
MAX_RETRY_ATTEMPTS = 3;
|
|
3518
|
+
constructor(types, dbName = "fjell-indexdb-cache", storeName = "cache", version = 1) {
|
|
3519
|
+
super(types);
|
|
3520
|
+
this.asyncCache = new AsyncIndexDBCacheMap(types, dbName, storeName, version);
|
|
3521
|
+
this.memoryCache = new MemoryCacheMap(types);
|
|
3522
|
+
this.initializeFromIndexedDB();
|
|
3523
|
+
this.startPeriodicSync();
|
|
3524
|
+
}
|
|
3525
|
+
async initializeFromIndexedDB() {
|
|
3526
|
+
if (this.initializationPromise) {
|
|
3527
|
+
return this.initializationPromise;
|
|
3528
|
+
}
|
|
3529
|
+
this.initializationPromise = (async () => {
|
|
3530
|
+
try {
|
|
3531
|
+
const keys = await this.asyncCache.keys();
|
|
3532
|
+
for (const key of keys) {
|
|
3533
|
+
if (!this.memoryCache.includesKey(key)) {
|
|
3534
|
+
const value = await this.asyncCache.get(key);
|
|
3535
|
+
if (value) {
|
|
3536
|
+
this.memoryCache.set(key, value);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
this.isInitialized = true;
|
|
3541
|
+
} catch (error) {
|
|
3542
|
+
console.warn("Failed to initialize from IndexedDB:", error);
|
|
3543
|
+
this.isInitialized = true;
|
|
3544
|
+
}
|
|
3545
|
+
})();
|
|
3546
|
+
return this.initializationPromise;
|
|
3547
|
+
}
|
|
3548
|
+
startPeriodicSync() {
|
|
3549
|
+
this.syncInterval = setInterval(() => {
|
|
3550
|
+
this.syncToIndexedDB();
|
|
3551
|
+
}, this.SYNC_INTERVAL_MS);
|
|
3552
|
+
}
|
|
3553
|
+
async syncToIndexedDB() {
|
|
3554
|
+
try {
|
|
3555
|
+
await this.processPendingOperations();
|
|
3556
|
+
const memoryKeys = this.memoryCache.keys();
|
|
3557
|
+
for (const key of memoryKeys) {
|
|
3558
|
+
const value = this.memoryCache.get(key);
|
|
3559
|
+
if (value) {
|
|
3560
|
+
await this.asyncCache.set(key, value);
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
} catch (error) {
|
|
3564
|
+
console.warn("Failed to sync to IndexedDB:", error);
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
async processPendingOperations() {
|
|
3568
|
+
const pendingOps = Array.from(this.pendingSyncOperations.entries());
|
|
3569
|
+
for (const [keyStr, operation] of pendingOps) {
|
|
3570
|
+
try {
|
|
3571
|
+
if (operation.type === "set" && operation.value) {
|
|
3572
|
+
await this.asyncCache.set(operation.key, operation.value);
|
|
3573
|
+
} else if (operation.type === "delete") {
|
|
3574
|
+
await this.asyncCache.delete(operation.key);
|
|
3575
|
+
}
|
|
3576
|
+
this.pendingSyncOperations.delete(keyStr);
|
|
3577
|
+
} catch (error) {
|
|
3578
|
+
console.warn(`Failed to process pending ${operation.type} operation:`, error);
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
queueForSync(key, value) {
|
|
3583
|
+
const keyStr = JSON.stringify(key);
|
|
3584
|
+
this.pendingSyncOperations.set(keyStr, { type: "set", key, value });
|
|
3585
|
+
setTimeout(async () => {
|
|
3586
|
+
try {
|
|
3587
|
+
await this.asyncCache.set(key, value);
|
|
3588
|
+
const pending = this.pendingSyncOperations.get(keyStr);
|
|
3589
|
+
if (pending && pending.type === "set" && pending.value === value) {
|
|
3590
|
+
this.pendingSyncOperations.delete(keyStr);
|
|
3591
|
+
}
|
|
3592
|
+
} catch (error) {
|
|
3593
|
+
console.warn("Failed to sync single operation to IndexedDB:", error);
|
|
3594
|
+
}
|
|
3595
|
+
}, 0);
|
|
3596
|
+
}
|
|
3597
|
+
queueDeleteForSync(key) {
|
|
3598
|
+
const keyStr = JSON.stringify(key);
|
|
3599
|
+
this.pendingSyncOperations.set(keyStr, { type: "delete", key });
|
|
3600
|
+
setTimeout(async () => {
|
|
3601
|
+
try {
|
|
3602
|
+
await this.asyncCache.delete(key);
|
|
3603
|
+
const pending = this.pendingSyncOperations.get(keyStr);
|
|
3604
|
+
if (pending && pending.type === "delete") {
|
|
3605
|
+
this.pendingSyncOperations.delete(keyStr);
|
|
3606
|
+
}
|
|
3607
|
+
} catch (error) {
|
|
3608
|
+
console.warn("Failed to sync delete operation to IndexedDB:", error);
|
|
3609
|
+
}
|
|
3610
|
+
}, 0);
|
|
3611
|
+
}
|
|
3612
|
+
queueClearForSync() {
|
|
3613
|
+
this.pendingSyncOperations.clear();
|
|
3614
|
+
setTimeout(async () => {
|
|
3615
|
+
try {
|
|
3616
|
+
await this.asyncCache.clear();
|
|
3617
|
+
} catch (error) {
|
|
3618
|
+
console.warn("Failed to sync clear operation to IndexedDB:", error);
|
|
3619
|
+
}
|
|
3620
|
+
}, 0);
|
|
3621
|
+
}
|
|
3622
|
+
get(key) {
|
|
3623
|
+
if (!this.isInitialized && this.initializationPromise) {
|
|
3624
|
+
}
|
|
3625
|
+
return this.memoryCache.get(key);
|
|
3626
|
+
}
|
|
3627
|
+
getWithTTL(key, ttl) {
|
|
3628
|
+
return this.memoryCache.getWithTTL(key, ttl);
|
|
3629
|
+
}
|
|
3630
|
+
set(key, value) {
|
|
3631
|
+
this.memoryCache.set(key, value);
|
|
3632
|
+
this.queueForSync(key, value);
|
|
3633
|
+
}
|
|
3634
|
+
includesKey(key) {
|
|
3635
|
+
return this.memoryCache.includesKey(key);
|
|
3636
|
+
}
|
|
3637
|
+
delete(key) {
|
|
3638
|
+
this.memoryCache.delete(key);
|
|
3639
|
+
this.queueDeleteForSync(key);
|
|
3640
|
+
}
|
|
3641
|
+
allIn(locations) {
|
|
3642
|
+
return this.memoryCache.allIn(locations);
|
|
3643
|
+
}
|
|
3644
|
+
contains(query, locations) {
|
|
3645
|
+
return this.memoryCache.contains(query, locations);
|
|
3646
|
+
}
|
|
3647
|
+
queryIn(query, locations) {
|
|
3648
|
+
return this.memoryCache.queryIn(query, locations);
|
|
3649
|
+
}
|
|
3650
|
+
clone() {
|
|
3651
|
+
return new _IndexDBCacheMap(this.types);
|
|
3652
|
+
}
|
|
3653
|
+
keys() {
|
|
3654
|
+
return this.memoryCache.keys();
|
|
3655
|
+
}
|
|
3656
|
+
values() {
|
|
3657
|
+
return this.memoryCache.values();
|
|
3658
|
+
}
|
|
3659
|
+
clear() {
|
|
3660
|
+
this.memoryCache.clear();
|
|
3661
|
+
this.queueClearForSync();
|
|
3662
|
+
}
|
|
3663
|
+
// Query result caching methods implementation
|
|
3664
|
+
setQueryResult(queryHash, itemKeys, ttl) {
|
|
3665
|
+
return this.memoryCache.setQueryResult(queryHash, itemKeys, ttl);
|
|
3666
|
+
}
|
|
3667
|
+
getQueryResult(queryHash) {
|
|
3668
|
+
return this.memoryCache.getQueryResult(queryHash);
|
|
3669
|
+
}
|
|
3670
|
+
hasQueryResult(queryHash) {
|
|
3671
|
+
return this.memoryCache.hasQueryResult(queryHash);
|
|
3672
|
+
}
|
|
3673
|
+
deleteQueryResult(queryHash) {
|
|
3674
|
+
return this.memoryCache.deleteQueryResult(queryHash);
|
|
3675
|
+
}
|
|
3676
|
+
invalidateItemKeys(keys) {
|
|
3677
|
+
return this.memoryCache.invalidateItemKeys(keys);
|
|
3678
|
+
}
|
|
3679
|
+
invalidateLocation(locations) {
|
|
3680
|
+
return this.memoryCache.invalidateLocation(locations);
|
|
3681
|
+
}
|
|
3682
|
+
clearQueryResults() {
|
|
3683
|
+
return this.memoryCache.clearQueryResults();
|
|
3684
|
+
}
|
|
3685
|
+
/**
|
|
3686
|
+
* Clean up resources when the cache is no longer needed
|
|
3687
|
+
*/
|
|
3688
|
+
destroy() {
|
|
3689
|
+
if (this.syncInterval) {
|
|
3690
|
+
clearInterval(this.syncInterval);
|
|
3691
|
+
this.syncInterval = null;
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
};
|
|
3695
|
+
|
|
3696
|
+
// src/Options.ts
|
|
3697
|
+
var DEFAULT_CACHE_OPTIONS = {
|
|
3698
|
+
cacheType: "memory",
|
|
3699
|
+
enableDebugLogging: false,
|
|
3700
|
+
autoSync: true,
|
|
3701
|
+
maxRetries: 3,
|
|
3702
|
+
retryDelay: 1e3,
|
|
3703
|
+
indexedDBConfig: {
|
|
3704
|
+
dbName: "fjell-cache",
|
|
3705
|
+
version: 1,
|
|
3706
|
+
storeName: "cache",
|
|
3707
|
+
size: {
|
|
3708
|
+
evictionPolicy: "lru"
|
|
3709
|
+
}
|
|
3710
|
+
},
|
|
3711
|
+
webStorageConfig: {
|
|
3712
|
+
keyPrefix: "fjell-cache:",
|
|
3713
|
+
compress: false,
|
|
3714
|
+
size: {
|
|
3715
|
+
evictionPolicy: "lru"
|
|
3716
|
+
}
|
|
3717
|
+
},
|
|
3718
|
+
memoryConfig: {
|
|
3719
|
+
// No limits by default
|
|
3720
|
+
size: {
|
|
3721
|
+
evictionPolicy: "lru"
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
};
|
|
3725
|
+
var createOptions = (cacheOptions) => {
|
|
3726
|
+
const indexedDBConfig = cacheOptions?.indexedDBConfig ? {
|
|
3727
|
+
...DEFAULT_CACHE_OPTIONS.indexedDBConfig,
|
|
3728
|
+
...cacheOptions.indexedDBConfig,
|
|
3729
|
+
size: cacheOptions.indexedDBConfig.size ? {
|
|
3730
|
+
...DEFAULT_CACHE_OPTIONS.indexedDBConfig?.size,
|
|
3731
|
+
...cacheOptions.indexedDBConfig.size
|
|
3732
|
+
} : DEFAULT_CACHE_OPTIONS.indexedDBConfig?.size
|
|
3733
|
+
} : { ...DEFAULT_CACHE_OPTIONS.indexedDBConfig };
|
|
3734
|
+
const webStorageConfig = cacheOptions?.webStorageConfig ? {
|
|
3735
|
+
...DEFAULT_CACHE_OPTIONS.webStorageConfig,
|
|
3736
|
+
...cacheOptions.webStorageConfig,
|
|
3737
|
+
size: cacheOptions.webStorageConfig.size ? {
|
|
3738
|
+
...DEFAULT_CACHE_OPTIONS.webStorageConfig?.size,
|
|
3739
|
+
...cacheOptions.webStorageConfig.size
|
|
3740
|
+
} : DEFAULT_CACHE_OPTIONS.webStorageConfig?.size
|
|
3741
|
+
} : { ...DEFAULT_CACHE_OPTIONS.webStorageConfig };
|
|
3742
|
+
const memoryConfig = cacheOptions?.memoryConfig ? {
|
|
3743
|
+
...DEFAULT_CACHE_OPTIONS.memoryConfig,
|
|
3744
|
+
...cacheOptions.memoryConfig,
|
|
3745
|
+
size: cacheOptions.memoryConfig.size ? {
|
|
3746
|
+
...DEFAULT_CACHE_OPTIONS.memoryConfig?.size,
|
|
3747
|
+
...cacheOptions.memoryConfig.size
|
|
3748
|
+
} : DEFAULT_CACHE_OPTIONS.memoryConfig?.size
|
|
3749
|
+
} : { ...DEFAULT_CACHE_OPTIONS.memoryConfig };
|
|
3750
|
+
return {
|
|
3751
|
+
...DEFAULT_CACHE_OPTIONS,
|
|
3752
|
+
...cacheOptions,
|
|
3753
|
+
indexedDBConfig,
|
|
3754
|
+
webStorageConfig,
|
|
3755
|
+
memoryConfig
|
|
3756
|
+
};
|
|
3757
|
+
};
|
|
3758
|
+
var createCacheMap = (kta, options) => {
|
|
3759
|
+
switch (options.cacheType) {
|
|
3760
|
+
case "memory":
|
|
3761
|
+
if (options.memoryConfig?.size && (options.memoryConfig.size.maxSizeBytes || options.memoryConfig.size.maxItems)) {
|
|
3762
|
+
return new EnhancedMemoryCacheMap(
|
|
3763
|
+
kta,
|
|
3764
|
+
options.memoryConfig.size
|
|
3765
|
+
);
|
|
3766
|
+
}
|
|
3767
|
+
return new MemoryCacheMap(kta);
|
|
3768
|
+
case "localStorage":
|
|
3769
|
+
return new LocalStorageCacheMap(
|
|
3770
|
+
kta,
|
|
3771
|
+
options.webStorageConfig?.keyPrefix
|
|
3772
|
+
);
|
|
3773
|
+
case "sessionStorage":
|
|
3774
|
+
return new SessionStorageCacheMap(
|
|
3775
|
+
kta,
|
|
3776
|
+
options.webStorageConfig?.keyPrefix
|
|
3777
|
+
);
|
|
3778
|
+
case "indexedDB":
|
|
3779
|
+
return new IndexDBCacheMap(
|
|
3780
|
+
kta,
|
|
3781
|
+
options.indexedDBConfig?.dbName,
|
|
3782
|
+
options.indexedDBConfig?.storeName,
|
|
3783
|
+
options.indexedDBConfig?.version
|
|
3784
|
+
);
|
|
3785
|
+
case "custom":
|
|
3786
|
+
if (!options.customCacheMapFactory) {
|
|
3787
|
+
throw new Error('Custom cache map factory is required when cacheType is "custom"');
|
|
3788
|
+
}
|
|
3789
|
+
return options.customCacheMapFactory(kta);
|
|
3790
|
+
default:
|
|
3791
|
+
throw new Error(`Unsupported cache type: ${options.cacheType}`);
|
|
3792
|
+
}
|
|
3793
|
+
};
|
|
3794
|
+
var validateOptions = (options) => {
|
|
3795
|
+
if (options.cacheType === "custom" && !options.customCacheMapFactory) {
|
|
3796
|
+
throw new Error('customCacheMapFactory is required when cacheType is "custom"');
|
|
3797
|
+
}
|
|
3798
|
+
if (typeof options.maxRetries === "number" && options.maxRetries < 0) {
|
|
3799
|
+
throw new Error("maxRetries must be non-negative");
|
|
3800
|
+
}
|
|
3801
|
+
if (typeof options.retryDelay === "number" && options.retryDelay < 0) {
|
|
3802
|
+
throw new Error("retryDelay must be non-negative");
|
|
3803
|
+
}
|
|
3804
|
+
if (typeof options.ttl === "number" && options.ttl <= 0) {
|
|
3805
|
+
throw new Error("ttl must be positive");
|
|
3806
|
+
}
|
|
3807
|
+
if (typeof options.memoryConfig?.maxItems === "number" && options.memoryConfig.maxItems <= 0) {
|
|
3808
|
+
throw new Error("memoryConfig.maxItems must be positive");
|
|
3809
|
+
}
|
|
3810
|
+
if (typeof options.memoryConfig?.ttl === "number" && options.memoryConfig.ttl <= 0) {
|
|
3811
|
+
throw new Error("memoryConfig.ttl must be positive");
|
|
3812
|
+
}
|
|
3813
|
+
if (options.memoryConfig?.size) {
|
|
3814
|
+
validateSizeConfig(options.memoryConfig.size);
|
|
3815
|
+
}
|
|
3816
|
+
if (options.webStorageConfig?.size) {
|
|
3817
|
+
validateSizeConfig(options.webStorageConfig.size);
|
|
3818
|
+
}
|
|
3819
|
+
if (options.indexedDBConfig?.size) {
|
|
3820
|
+
validateSizeConfig(options.indexedDBConfig.size);
|
|
3821
|
+
}
|
|
3822
|
+
if (["localStorage", "sessionStorage"].includes(options.cacheType)) {
|
|
3823
|
+
if (typeof window === "undefined" || !window[options.cacheType]) {
|
|
3824
|
+
throw new Error(`${options.cacheType} is not available in non-browser environments`);
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
if (options.cacheType === "indexedDB") {
|
|
3828
|
+
if (typeof window === "undefined" || !window.indexedDB) {
|
|
3829
|
+
throw new Error(`${options.cacheType} is not available in this environment`);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
if (options.cacheType === "asyncIndexedDB") {
|
|
3833
|
+
throw new Error("asyncIndexedDB cannot be used with synchronous cache factory. Use AsyncIndexDBCacheMap directly for async operations.");
|
|
3834
|
+
}
|
|
3835
|
+
};
|
|
3836
|
+
|
|
3837
|
+
// src/ops/reset.ts
|
|
3838
|
+
var reset = async (coordinate, options) => {
|
|
3839
|
+
const cacheMap = createCacheMap(coordinate.kta, options);
|
|
3840
|
+
return [cacheMap];
|
|
3841
|
+
};
|
|
3842
|
+
|
|
3843
|
+
// src/Operations.ts
|
|
3844
|
+
var createOperations = (api, coordinate, cacheMap, pkType, options) => {
|
|
3845
|
+
const context = createCacheContext(api, cacheMap, pkType, options);
|
|
3846
|
+
return {
|
|
3847
|
+
all: (query, locations) => all(query, locations, context).then(([ctx, result]) => [ctx.cacheMap, result]),
|
|
3848
|
+
one: (query, locations) => one(query, locations, context).then(([ctx, result]) => [ctx.cacheMap, result]),
|
|
3849
|
+
create: (item, locations) => create(item, locations, context).then(([ctx, result]) => [ctx.cacheMap, result]),
|
|
3850
|
+
get: (key) => get(key, context).then(([ctx, result]) => [ctx.cacheMap, result]),
|
|
3851
|
+
retrieve: (key) => retrieve(key, context).then(([ctx, result]) => [ctx ? ctx.cacheMap : null, result]),
|
|
3852
|
+
remove: (key) => remove(key, context).then((ctx) => ctx.cacheMap),
|
|
3853
|
+
update: (key, item) => update(key, item, context).then(([ctx, result]) => [ctx.cacheMap, result]),
|
|
3854
|
+
action: (key, actionName, body) => action(key, actionName, body, context).then(([ctx, result]) => [ctx.cacheMap, result]),
|
|
3855
|
+
allAction: (actionName, body, locations) => allAction(actionName, body, locations, context).then(([ctx, result]) => [ctx.cacheMap, result]),
|
|
3856
|
+
facet: (key, facetName, params) => facet(key, facetName, params, context).then((result) => [context.cacheMap, result]),
|
|
3857
|
+
allFacet: (facetName, params, locations) => allFacet(facetName, params, locations, context).then((result) => [context.cacheMap, result]),
|
|
3858
|
+
find: (finder, params, locations) => find(finder, params, locations, context).then(([ctx, result]) => [ctx.cacheMap, result]),
|
|
3859
|
+
findOne: (finder, params, locations) => findOne(finder, params, locations, context).then(([ctx, result]) => [ctx.cacheMap, result]),
|
|
3860
|
+
set: (key, item) => set(key, item, context).then(([ctx, result]) => [ctx.cacheMap, result]),
|
|
3861
|
+
reset: () => reset(coordinate, options)
|
|
3862
|
+
};
|
|
3863
|
+
};
|
|
3864
|
+
|
|
3865
|
+
// src/Cache.ts
|
|
3866
|
+
var logger20 = logger_default.get("Cache");
|
|
3867
|
+
var createCache = (api, coordinate, registry, options) => {
|
|
3868
|
+
logger20.debug("createCache", { coordinate, registry, options });
|
|
3869
|
+
const completeOptions = createOptions(options);
|
|
3870
|
+
const cacheMap = createCacheMap(coordinate.kta, completeOptions);
|
|
3871
|
+
const pkType = coordinate.kta[0];
|
|
3872
|
+
const operations = createOperations(api, coordinate, cacheMap, pkType, completeOptions);
|
|
3873
|
+
return {
|
|
3874
|
+
coordinate,
|
|
3875
|
+
registry,
|
|
3876
|
+
api,
|
|
3877
|
+
cacheMap,
|
|
3878
|
+
operations,
|
|
3879
|
+
options: completeOptions
|
|
3880
|
+
};
|
|
3881
|
+
};
|
|
3882
|
+
var isCache = (cache) => {
|
|
3883
|
+
return cache !== null && typeof cache === "object" && "coordinate" in cache && "registry" in cache && "api" in cache && "cacheMap" in cache && "operations" in cache;
|
|
3884
|
+
};
|
|
3885
|
+
|
|
3886
|
+
// src/InstanceFactory.ts
|
|
3887
|
+
var logger21 = logger_default.get("InstanceFactory");
|
|
3888
|
+
var createInstanceFactory = (api, options) => {
|
|
3889
|
+
const templateOptions = createOptions(options);
|
|
3890
|
+
validateOptions(templateOptions);
|
|
3891
|
+
return (coordinate, context) => {
|
|
3892
|
+
const instanceOptions = createOptions(options);
|
|
3893
|
+
logger21.debug("Creating cache instance", {
|
|
3894
|
+
coordinate,
|
|
3895
|
+
registry: context.registry,
|
|
3896
|
+
api,
|
|
3897
|
+
cacheType: instanceOptions.cacheType,
|
|
3898
|
+
options: instanceOptions
|
|
3899
|
+
});
|
|
3900
|
+
const cacheMap = createCacheMap(coordinate.kta, instanceOptions);
|
|
3901
|
+
const pkType = coordinate.kta[0];
|
|
3902
|
+
const operations = createOperations(api, coordinate, cacheMap, pkType, instanceOptions);
|
|
3903
|
+
return {
|
|
3904
|
+
coordinate,
|
|
3905
|
+
registry: context.registry,
|
|
3906
|
+
api,
|
|
3907
|
+
cacheMap,
|
|
3908
|
+
operations,
|
|
3909
|
+
options: instanceOptions
|
|
3910
|
+
};
|
|
3911
|
+
};
|
|
3912
|
+
};
|
|
3913
|
+
|
|
3914
|
+
// src/Instance.ts
|
|
3915
|
+
var logger22 = logger_default.get("Instance");
|
|
3916
|
+
var createInstance = (registry, coordinate, api, options) => {
|
|
3917
|
+
logger22.debug("createInstance", { coordinate, api, registry, options });
|
|
3918
|
+
return createCache(api, coordinate, registry, options);
|
|
3919
|
+
};
|
|
3920
|
+
var isInstance = (instance) => {
|
|
3921
|
+
return instance !== null && typeof instance === "object" && "coordinate" in instance && "registry" in instance && "api" in instance && "cacheMap" in instance && "operations" in instance;
|
|
3922
|
+
};
|
|
3923
|
+
|
|
3924
|
+
// src/Aggregator.ts
|
|
3925
|
+
var logger23 = logger_default.get("ItemAggregator");
|
|
3926
|
+
var toCacheConfig = (config) => {
|
|
3927
|
+
let cacheConfig;
|
|
3928
|
+
if (config.optional === void 0) {
|
|
3929
|
+
cacheConfig = { cache: config, optional: false };
|
|
3930
|
+
} else {
|
|
3931
|
+
cacheConfig = config;
|
|
3932
|
+
}
|
|
3933
|
+
return cacheConfig;
|
|
3934
|
+
};
|
|
3935
|
+
var createAggregator = async (cache, { aggregates = {}, events = {} }) => {
|
|
3936
|
+
const populate = async (item) => {
|
|
3937
|
+
logger23.default("populate", { item });
|
|
3938
|
+
for (const key in aggregates) {
|
|
3939
|
+
await populateAggregate(key, item);
|
|
3940
|
+
}
|
|
3941
|
+
for (const key in events) {
|
|
3942
|
+
await populateEvent(key, item);
|
|
3943
|
+
}
|
|
3944
|
+
logger23.default("populate done", { item });
|
|
3945
|
+
return item;
|
|
3946
|
+
};
|
|
3947
|
+
const populateAggregate = async (key, item) => {
|
|
3948
|
+
logger23.default("populate aggregate key", { key });
|
|
3949
|
+
const cacheConfig = toCacheConfig(aggregates[key]);
|
|
3950
|
+
if (item.refs === void 0) {
|
|
3951
|
+
if (cacheConfig.optional === false) {
|
|
3952
|
+
logger23.error("Item does not have refs an is not optional " + JSON.stringify(item));
|
|
3953
|
+
throw new Error("Item does not have refs an is not optional " + JSON.stringify(item));
|
|
3954
|
+
} else {
|
|
3955
|
+
if (item.events && Object.prototype.hasOwnProperty.call(item.events, key)) {
|
|
3956
|
+
delete item.events[key];
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
} else if (item.refs[key] === void 0) {
|
|
3960
|
+
if (cacheConfig.optional === false) {
|
|
3961
|
+
logger23.error("Item does not have mandatory ref with key, not optional " + key + " " + JSON.stringify(item));
|
|
3962
|
+
throw new Error("Item does not have mandatory ref with key, not optional " + key + " " + JSON.stringify(item));
|
|
3963
|
+
} else {
|
|
3964
|
+
if (item.events && Object.prototype.hasOwnProperty.call(item.events, key)) {
|
|
3965
|
+
delete item.events[key];
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
} else {
|
|
3969
|
+
const ref = item.refs[key];
|
|
3970
|
+
logger23.default("AGG Retrieving Item in Populate", { key: ref });
|
|
3971
|
+
const [, newItem] = await cacheConfig.cache.operations.retrieve(ref);
|
|
3972
|
+
if (newItem) {
|
|
3973
|
+
if (item.aggs === void 0) {
|
|
3974
|
+
item.aggs = {};
|
|
3975
|
+
}
|
|
3976
|
+
item.aggs[key] = {
|
|
3977
|
+
key: ref,
|
|
3978
|
+
item: newItem
|
|
3979
|
+
};
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
};
|
|
3983
|
+
const populateEvent = async (key, item) => {
|
|
3984
|
+
logger23.default("populate event key", { key });
|
|
3985
|
+
const cacheConfig = toCacheConfig(events[key]);
|
|
3986
|
+
if (item.events === void 0) {
|
|
3987
|
+
throw new Error("Item does not have events " + JSON.stringify(item));
|
|
3988
|
+
} else if (item.events[key] === void 0) {
|
|
3989
|
+
if (cacheConfig.optional === false) {
|
|
3990
|
+
logger23.error("Item does not have mandatory event with key " + key + " " + JSON.stringify(item));
|
|
3991
|
+
throw new Error("Item does not have mandatory event with key " + key + " " + JSON.stringify(item));
|
|
3992
|
+
}
|
|
3993
|
+
} else {
|
|
3994
|
+
const event = item.events[key];
|
|
3995
|
+
if (event.by === void 0) {
|
|
3996
|
+
logger23.error(
|
|
3997
|
+
"populateEvent with an Event that does not have by",
|
|
3998
|
+
{ event, ik: item.key, eventKey: key }
|
|
3999
|
+
);
|
|
4000
|
+
throw new Error("populateEvent with an Event that does not have by: " + JSON.stringify({ key, event }));
|
|
4001
|
+
}
|
|
4002
|
+
logger23.default("EVENT Retrieving Item in Populate", { key: event.by });
|
|
4003
|
+
const [, newItem] = await cacheConfig.cache.operations.retrieve(event.by);
|
|
4004
|
+
if (newItem) {
|
|
4005
|
+
event.agg = newItem;
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
};
|
|
4009
|
+
const all2 = async (query = {}, locations = []) => {
|
|
4010
|
+
logger23.default("all", { query, locations });
|
|
4011
|
+
const [cacheMap, items] = await cache.operations.all(query, locations);
|
|
4012
|
+
const populatedItems = await Promise.all(items.map(async (item) => populate(item)));
|
|
4013
|
+
return [cacheMap, populatedItems];
|
|
4014
|
+
};
|
|
4015
|
+
const one2 = async (query = {}, locations = []) => {
|
|
4016
|
+
logger23.default("one", { query, locations });
|
|
4017
|
+
const [cacheMap, item] = await cache.operations.one(query, locations);
|
|
4018
|
+
let populatedItem = null;
|
|
4019
|
+
if (item) {
|
|
4020
|
+
populatedItem = await populate(item);
|
|
4021
|
+
}
|
|
4022
|
+
return [cacheMap, populatedItem];
|
|
4023
|
+
};
|
|
4024
|
+
const action2 = async (key, action3, body = {}) => {
|
|
4025
|
+
logger23.default("action", { key, action: action3, body });
|
|
4026
|
+
const [cacheMap, item] = await cache.operations.action(key, action3, body);
|
|
4027
|
+
const populatedItem = await populate(item);
|
|
4028
|
+
return [cacheMap, populatedItem];
|
|
4029
|
+
};
|
|
4030
|
+
const allAction2 = async (action3, body = {}, locations = []) => {
|
|
4031
|
+
logger23.default("action", { action: action3, body, locations });
|
|
4032
|
+
const [cacheMap, items] = await cache.operations.allAction(action3, body, locations);
|
|
4033
|
+
const populatedItems = await Promise.all(items.map(async (item) => populate(item)));
|
|
4034
|
+
return [cacheMap, populatedItems];
|
|
4035
|
+
};
|
|
4036
|
+
const allFacet2 = async (facet3, params = {}, locations = []) => {
|
|
4037
|
+
logger23.default("allFacet", { facet: facet3, params, locations });
|
|
4038
|
+
const [cacheMap, response] = await cache.operations.allFacet(facet3, params, locations);
|
|
4039
|
+
return [cacheMap, response];
|
|
4040
|
+
};
|
|
4041
|
+
const create2 = async (v, locations = []) => {
|
|
4042
|
+
logger23.default("create", { v, locations });
|
|
4043
|
+
const [cacheMap, item] = await cache.operations.create(v, locations);
|
|
4044
|
+
const populatedItem = await populate(item);
|
|
4045
|
+
return [cacheMap, populatedItem];
|
|
4046
|
+
};
|
|
4047
|
+
const get2 = async (key) => {
|
|
4048
|
+
logger23.default("get", { key });
|
|
4049
|
+
const [cacheMap, item] = await cache.operations.get(key);
|
|
4050
|
+
let populatedItem = null;
|
|
4051
|
+
if (item) {
|
|
4052
|
+
populatedItem = await populate(item);
|
|
4053
|
+
}
|
|
4054
|
+
return [cacheMap, populatedItem];
|
|
4055
|
+
};
|
|
4056
|
+
const retrieve2 = async (key) => {
|
|
4057
|
+
logger23.default("retrieve", { key });
|
|
4058
|
+
const [cacheMap, item] = await cache.operations.retrieve(key);
|
|
4059
|
+
let populatedItem = null;
|
|
4060
|
+
if (item) {
|
|
4061
|
+
populatedItem = await populate(item);
|
|
4062
|
+
}
|
|
4063
|
+
return [cacheMap, populatedItem];
|
|
4064
|
+
};
|
|
4065
|
+
const remove2 = async (key) => {
|
|
4066
|
+
logger23.default("remove", { key });
|
|
4067
|
+
const cacheMap = await cache.operations.remove(key);
|
|
4068
|
+
return cacheMap;
|
|
4069
|
+
};
|
|
4070
|
+
const update2 = async (key, v) => {
|
|
4071
|
+
logger23.default("update", { key, v });
|
|
4072
|
+
const [cacheMap, item] = await cache.operations.update(key, v);
|
|
4073
|
+
const populatedItem = await populate(item);
|
|
4074
|
+
return [cacheMap, populatedItem];
|
|
4075
|
+
};
|
|
4076
|
+
const facet2 = async (key, facet3) => {
|
|
4077
|
+
logger23.default("facet", { key, facet: facet3 });
|
|
4078
|
+
const [cacheMap, response] = await cache.operations.facet(key, facet3);
|
|
4079
|
+
return [cacheMap, response];
|
|
4080
|
+
};
|
|
4081
|
+
const find2 = async (finder, finderParams = {}, locations = []) => {
|
|
4082
|
+
logger23.default("find", { finder, finderParams, locations });
|
|
4083
|
+
const [cacheMap, items] = await cache.operations.find(finder, finderParams, locations);
|
|
4084
|
+
const populatedItems = await Promise.all(items.map(async (item) => populate(item)));
|
|
4085
|
+
return [cacheMap, populatedItems];
|
|
4086
|
+
};
|
|
4087
|
+
const findOne2 = async (finder, finderParams = {}, locations = []) => {
|
|
4088
|
+
logger23.default("find", { finder, finderParams, locations });
|
|
4089
|
+
const [cacheMap, item] = await cache.operations.findOne(finder, finderParams, locations);
|
|
4090
|
+
const populatedItem = await populate(item);
|
|
4091
|
+
return [cacheMap, populatedItem];
|
|
4092
|
+
};
|
|
4093
|
+
const set2 = async (key, v) => {
|
|
4094
|
+
logger23.default("set", { key, v });
|
|
4095
|
+
const [cacheMap, item] = await cache.operations.set(key, v);
|
|
4096
|
+
const populatedItem = await populate(item);
|
|
4097
|
+
return [cacheMap, populatedItem];
|
|
4098
|
+
};
|
|
4099
|
+
const reset2 = async () => {
|
|
4100
|
+
const cacheMap = await cache.operations.reset();
|
|
4101
|
+
return cacheMap;
|
|
4102
|
+
};
|
|
4103
|
+
return {
|
|
4104
|
+
// Cache properties
|
|
4105
|
+
coordinate: cache.coordinate,
|
|
4106
|
+
registry: cache.registry,
|
|
4107
|
+
api: cache.api,
|
|
4108
|
+
cacheMap: cache.cacheMap,
|
|
4109
|
+
operations: cache.operations,
|
|
4110
|
+
// Cache operations exposed directly
|
|
4111
|
+
all: all2,
|
|
4112
|
+
one: one2,
|
|
4113
|
+
action: action2,
|
|
4114
|
+
allAction: allAction2,
|
|
4115
|
+
allFacet: allFacet2,
|
|
4116
|
+
create: create2,
|
|
4117
|
+
get: get2,
|
|
4118
|
+
retrieve: retrieve2,
|
|
4119
|
+
remove: remove2,
|
|
4120
|
+
update: update2,
|
|
4121
|
+
facet: facet2,
|
|
4122
|
+
find: find2,
|
|
4123
|
+
findOne: findOne2,
|
|
4124
|
+
reset: reset2,
|
|
4125
|
+
set: set2,
|
|
4126
|
+
// Aggregator-specific operations
|
|
4127
|
+
populate,
|
|
4128
|
+
populateAggregate,
|
|
4129
|
+
populateEvent
|
|
718
4130
|
};
|
|
719
4131
|
};
|
|
720
4132
|
export {
|
|
4133
|
+
AsyncIndexDBCacheMap,
|
|
721
4134
|
CacheMap,
|
|
4135
|
+
EnhancedMemoryCacheMap,
|
|
4136
|
+
IndexDBCacheMap,
|
|
4137
|
+
LocalStorageCacheMap,
|
|
4138
|
+
MemoryCacheMap,
|
|
4139
|
+
SessionStorageCacheMap,
|
|
722
4140
|
createAggregator,
|
|
723
4141
|
createCache,
|
|
4142
|
+
createCacheMap,
|
|
4143
|
+
createEvictionStrategy,
|
|
724
4144
|
createInstance,
|
|
725
4145
|
createInstanceFactory,
|
|
4146
|
+
createNormalizedHashFunction,
|
|
726
4147
|
createOperations,
|
|
727
|
-
|
|
728
|
-
|
|
4148
|
+
createOptions,
|
|
4149
|
+
createValidatedConfig,
|
|
4150
|
+
estimateValueSize,
|
|
4151
|
+
formatBytes,
|
|
729
4152
|
isCache,
|
|
730
4153
|
isInstance,
|
|
731
|
-
|
|
4154
|
+
isLocKeyArrayEqual,
|
|
4155
|
+
normalizeKeyValue,
|
|
4156
|
+
normalizeLocKeyItem,
|
|
4157
|
+
parseSizeString,
|
|
4158
|
+
toCacheConfig,
|
|
4159
|
+
validateARCConfig,
|
|
4160
|
+
validateEvictionStrategyConfig,
|
|
4161
|
+
validateLFUConfig,
|
|
4162
|
+
validateOptions,
|
|
4163
|
+
validateSizeConfig,
|
|
4164
|
+
validateTwoQueueConfig
|
|
732
4165
|
};
|
|
733
4166
|
//# sourceMappingURL=index.js.map
|