@open-mercato/cache 0.3.2
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/ENV.md +173 -0
- package/README.md +177 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +9 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/service.d.ts +40 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +321 -0
- package/dist/strategies/jsonfile.d.ts +10 -0
- package/dist/strategies/jsonfile.d.ts.map +1 -0
- package/dist/strategies/jsonfile.js +205 -0
- package/dist/strategies/memory.d.ts +9 -0
- package/dist/strategies/memory.d.ts.map +1 -0
- package/dist/strategies/memory.js +166 -0
- package/dist/strategies/redis.d.ts +5 -0
- package/dist/strategies/redis.d.ts.map +1 -0
- package/dist/strategies/redis.js +388 -0
- package/dist/strategies/sqlite.d.ts +13 -0
- package/dist/strategies/sqlite.d.ts.map +1 -0
- package/dist/strategies/sqlite.js +217 -0
- package/dist/tenantContext.d.ts +4 -0
- package/dist/tenantContext.d.ts.map +1 -0
- package/dist/tenantContext.js +9 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/jest.config.js +19 -0
- package/package.json +39 -0
- package/src/__tests__/memory.strategy.test.ts +245 -0
- package/src/__tests__/service.test.ts +189 -0
- package/src/errors.ts +14 -0
- package/src/index.ts +7 -0
- package/src/service.ts +367 -0
- package/src/strategies/jsonfile.ts +249 -0
- package/src/strategies/memory.ts +185 -0
- package/src/strategies/redis.ts +443 -0
- package/src/strategies/sqlite.ts +285 -0
- package/src/tenantContext.ts +13 -0
- package/src/types.ts +100 -0
- package/tsconfig.build.json +5 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory cache strategy with tag support
|
|
3
|
+
* Fast but data is lost when process restarts
|
|
4
|
+
*/
|
|
5
|
+
export function createMemoryStrategy(options) {
|
|
6
|
+
const store = new Map();
|
|
7
|
+
const tagIndex = new Map(); // tag -> Set of keys
|
|
8
|
+
const defaultTtl = options === null || options === void 0 ? void 0 : options.defaultTtl;
|
|
9
|
+
function isExpired(entry) {
|
|
10
|
+
if (entry.expiresAt === null)
|
|
11
|
+
return false;
|
|
12
|
+
return Date.now() > entry.expiresAt;
|
|
13
|
+
}
|
|
14
|
+
function cleanupExpiredEntry(key, entry) {
|
|
15
|
+
store.delete(key);
|
|
16
|
+
// Remove from tag index
|
|
17
|
+
for (const tag of entry.tags) {
|
|
18
|
+
const keys = tagIndex.get(tag);
|
|
19
|
+
if (keys) {
|
|
20
|
+
keys.delete(key);
|
|
21
|
+
if (keys.size === 0) {
|
|
22
|
+
tagIndex.delete(tag);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function addToTagIndex(key, tags) {
|
|
28
|
+
for (const tag of tags) {
|
|
29
|
+
if (!tagIndex.has(tag)) {
|
|
30
|
+
tagIndex.set(tag, new Set());
|
|
31
|
+
}
|
|
32
|
+
tagIndex.get(tag).add(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function removeFromTagIndex(key, tags) {
|
|
36
|
+
for (const tag of tags) {
|
|
37
|
+
const keys = tagIndex.get(tag);
|
|
38
|
+
if (keys) {
|
|
39
|
+
keys.delete(key);
|
|
40
|
+
if (keys.size === 0) {
|
|
41
|
+
tagIndex.delete(tag);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function matchPattern(key, pattern) {
|
|
47
|
+
const regexPattern = pattern
|
|
48
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
|
|
49
|
+
.replace(/\*/g, '.*') // * matches any characters
|
|
50
|
+
.replace(/\?/g, '.'); // ? matches single character
|
|
51
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
52
|
+
return regex.test(key);
|
|
53
|
+
}
|
|
54
|
+
const get = async (key, options) => {
|
|
55
|
+
const entry = store.get(key);
|
|
56
|
+
if (!entry)
|
|
57
|
+
return null;
|
|
58
|
+
if (isExpired(entry)) {
|
|
59
|
+
if (options === null || options === void 0 ? void 0 : options.returnExpired) {
|
|
60
|
+
return entry.value;
|
|
61
|
+
}
|
|
62
|
+
cleanupExpiredEntry(key, entry);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return entry.value;
|
|
66
|
+
};
|
|
67
|
+
const set = async (key, value, options) => {
|
|
68
|
+
var _a;
|
|
69
|
+
// Remove old entry from tag index if it exists
|
|
70
|
+
const oldEntry = store.get(key);
|
|
71
|
+
if (oldEntry) {
|
|
72
|
+
removeFromTagIndex(key, oldEntry.tags);
|
|
73
|
+
}
|
|
74
|
+
const ttl = (_a = options === null || options === void 0 ? void 0 : options.ttl) !== null && _a !== void 0 ? _a : defaultTtl;
|
|
75
|
+
const tags = (options === null || options === void 0 ? void 0 : options.tags) || [];
|
|
76
|
+
const expiresAt = ttl ? Date.now() + ttl : null;
|
|
77
|
+
const entry = {
|
|
78
|
+
key,
|
|
79
|
+
value,
|
|
80
|
+
tags,
|
|
81
|
+
expiresAt,
|
|
82
|
+
createdAt: Date.now(),
|
|
83
|
+
};
|
|
84
|
+
store.set(key, entry);
|
|
85
|
+
addToTagIndex(key, tags);
|
|
86
|
+
};
|
|
87
|
+
const has = async (key) => {
|
|
88
|
+
const entry = store.get(key);
|
|
89
|
+
if (!entry)
|
|
90
|
+
return false;
|
|
91
|
+
if (isExpired(entry)) {
|
|
92
|
+
cleanupExpiredEntry(key, entry);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
};
|
|
97
|
+
const deleteKey = async (key) => {
|
|
98
|
+
const entry = store.get(key);
|
|
99
|
+
if (!entry)
|
|
100
|
+
return false;
|
|
101
|
+
removeFromTagIndex(key, entry.tags);
|
|
102
|
+
return store.delete(key);
|
|
103
|
+
};
|
|
104
|
+
const deleteByTags = async (tags) => {
|
|
105
|
+
const keysToDelete = new Set();
|
|
106
|
+
// Collect all keys that have any of the specified tags
|
|
107
|
+
for (const tag of tags) {
|
|
108
|
+
const keys = tagIndex.get(tag);
|
|
109
|
+
if (keys) {
|
|
110
|
+
for (const key of keys) {
|
|
111
|
+
keysToDelete.add(key);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Delete all collected keys
|
|
116
|
+
let deleted = 0;
|
|
117
|
+
for (const key of keysToDelete) {
|
|
118
|
+
const success = await deleteKey(key);
|
|
119
|
+
if (success)
|
|
120
|
+
deleted++;
|
|
121
|
+
}
|
|
122
|
+
return deleted;
|
|
123
|
+
};
|
|
124
|
+
const clear = async () => {
|
|
125
|
+
const size = store.size;
|
|
126
|
+
store.clear();
|
|
127
|
+
tagIndex.clear();
|
|
128
|
+
return size;
|
|
129
|
+
};
|
|
130
|
+
const keys = async (pattern) => {
|
|
131
|
+
const allKeys = Array.from(store.keys());
|
|
132
|
+
if (!pattern)
|
|
133
|
+
return allKeys;
|
|
134
|
+
return allKeys.filter((key) => matchPattern(key, pattern));
|
|
135
|
+
};
|
|
136
|
+
const stats = async () => {
|
|
137
|
+
let expired = 0;
|
|
138
|
+
for (const entry of store.values()) {
|
|
139
|
+
if (isExpired(entry)) {
|
|
140
|
+
expired++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { size: store.size, expired };
|
|
144
|
+
};
|
|
145
|
+
const cleanup = async () => {
|
|
146
|
+
let removed = 0;
|
|
147
|
+
for (const [key, entry] of store.entries()) {
|
|
148
|
+
if (isExpired(entry)) {
|
|
149
|
+
cleanupExpiredEntry(key, entry);
|
|
150
|
+
removed++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return removed;
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
get,
|
|
157
|
+
set,
|
|
158
|
+
has,
|
|
159
|
+
delete: deleteKey,
|
|
160
|
+
deleteByTags,
|
|
161
|
+
clear,
|
|
162
|
+
keys,
|
|
163
|
+
stats,
|
|
164
|
+
cleanup,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/strategies/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAA4D,MAAM,UAAU,CAAA;AAwJvG,wBAAgB,mBAAmB,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAkSvG"}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { CacheDependencyUnavailableError } from '../errors';
|
|
2
|
+
/**
|
|
3
|
+
* Redis cache strategy with tag support
|
|
4
|
+
* Persistent across process restarts, can be shared across multiple instances
|
|
5
|
+
*
|
|
6
|
+
* Uses Redis data structures:
|
|
7
|
+
* - Hash for storing cache entries: cache:{key} -> {value, tags, expiresAt, createdAt}
|
|
8
|
+
* - Sets for tag index: tag:{tag} -> Set of keys
|
|
9
|
+
*/
|
|
10
|
+
let redisModulePromise = null;
|
|
11
|
+
const redisRegistry = new Map();
|
|
12
|
+
function resolveRequire() {
|
|
13
|
+
const nonWebpack = globalThis.__non_webpack_require__;
|
|
14
|
+
if (typeof nonWebpack === 'function')
|
|
15
|
+
return nonWebpack;
|
|
16
|
+
if (typeof require === 'function')
|
|
17
|
+
return require;
|
|
18
|
+
if (typeof module !== 'undefined' && typeof module.require === 'function') {
|
|
19
|
+
return module.require.bind(module);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const maybeRequire = Function('return typeof require !== "undefined" ? require : undefined')();
|
|
23
|
+
if (typeof maybeRequire === 'function')
|
|
24
|
+
return maybeRequire;
|
|
25
|
+
}
|
|
26
|
+
catch (_a) {
|
|
27
|
+
// ignore
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function loadRedisModuleViaRequire() {
|
|
32
|
+
const resolver = resolveRequire();
|
|
33
|
+
if (!resolver)
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
return resolver('ioredis');
|
|
37
|
+
}
|
|
38
|
+
catch (_a) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function pickRedisConstructor(mod) {
|
|
43
|
+
var _a;
|
|
44
|
+
const queue = [mod];
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
while (queue.length) {
|
|
47
|
+
const current = queue.shift();
|
|
48
|
+
if (!current || seen.has(current))
|
|
49
|
+
continue;
|
|
50
|
+
seen.add(current);
|
|
51
|
+
if (typeof current === 'function')
|
|
52
|
+
return current;
|
|
53
|
+
if (typeof current === 'object') {
|
|
54
|
+
queue.push(current.default);
|
|
55
|
+
queue.push(current.Redis);
|
|
56
|
+
queue.push((_a = current.module) === null || _a === void 0 ? void 0 : _a.exports);
|
|
57
|
+
queue.push(current.exports);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
async function loadRedisModule() {
|
|
63
|
+
if (!redisModulePromise) {
|
|
64
|
+
redisModulePromise = (async () => {
|
|
65
|
+
var _a;
|
|
66
|
+
const required = (_a = loadRedisModuleViaRequire()) !== null && _a !== void 0 ? _a : (await import('ioredis'));
|
|
67
|
+
return required;
|
|
68
|
+
})().catch((error) => {
|
|
69
|
+
redisModulePromise = null;
|
|
70
|
+
throw new CacheDependencyUnavailableError('redis', 'ioredis', error);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return redisModulePromise;
|
|
74
|
+
}
|
|
75
|
+
function retainRedisEntry(key) {
|
|
76
|
+
let entry = redisRegistry.get(key);
|
|
77
|
+
if (!entry) {
|
|
78
|
+
entry = { refs: 0 };
|
|
79
|
+
redisRegistry.set(key, entry);
|
|
80
|
+
}
|
|
81
|
+
entry.refs += 1;
|
|
82
|
+
return entry;
|
|
83
|
+
}
|
|
84
|
+
async function acquireRedisClient(key, entry) {
|
|
85
|
+
if (entry.client)
|
|
86
|
+
return entry.client;
|
|
87
|
+
if (entry.creating)
|
|
88
|
+
return entry.creating;
|
|
89
|
+
entry.creating = loadRedisModule()
|
|
90
|
+
.then((mod) => {
|
|
91
|
+
var _a;
|
|
92
|
+
const ctor = pickRedisConstructor(mod);
|
|
93
|
+
if (!ctor) {
|
|
94
|
+
throw new CacheDependencyUnavailableError('redis', 'ioredis', new Error('No usable Redis constructor'));
|
|
95
|
+
}
|
|
96
|
+
const client = new ctor(key);
|
|
97
|
+
entry.client = client;
|
|
98
|
+
entry.creating = undefined;
|
|
99
|
+
(_a = client.once) === null || _a === void 0 ? void 0 : _a.call(client, 'end', () => {
|
|
100
|
+
if (redisRegistry.get(key) === entry && entry.refs === 0) {
|
|
101
|
+
redisRegistry.delete(key);
|
|
102
|
+
}
|
|
103
|
+
else if (redisRegistry.get(key) === entry) {
|
|
104
|
+
entry.client = undefined;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return client;
|
|
108
|
+
})
|
|
109
|
+
.catch((error) => {
|
|
110
|
+
entry.creating = undefined;
|
|
111
|
+
throw error;
|
|
112
|
+
});
|
|
113
|
+
return entry.creating;
|
|
114
|
+
}
|
|
115
|
+
async function releaseRedisEntry(key, entry) {
|
|
116
|
+
entry.refs = Math.max(0, entry.refs - 1);
|
|
117
|
+
if (entry.refs > 0)
|
|
118
|
+
return;
|
|
119
|
+
redisRegistry.delete(key);
|
|
120
|
+
if (entry.client) {
|
|
121
|
+
try {
|
|
122
|
+
await entry.client.quit();
|
|
123
|
+
}
|
|
124
|
+
catch (_a) {
|
|
125
|
+
// ignore shutdown errors
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
entry.client = undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function createRedisStrategy(redisUrl, options) {
|
|
133
|
+
var _a;
|
|
134
|
+
const defaultTtl = options === null || options === void 0 ? void 0 : options.defaultTtl;
|
|
135
|
+
const keyPrefix = 'cache:';
|
|
136
|
+
const tagPrefix = 'tag:';
|
|
137
|
+
const connectionUrl = redisUrl || process.env.REDIS_URL || process.env.CACHE_REDIS_URL || 'redis://localhost:6379';
|
|
138
|
+
const registryEntry = retainRedisEntry(connectionUrl);
|
|
139
|
+
let redis = (_a = registryEntry.client) !== null && _a !== void 0 ? _a : null;
|
|
140
|
+
async function getRedisClient() {
|
|
141
|
+
if (redis)
|
|
142
|
+
return redis;
|
|
143
|
+
redis = await acquireRedisClient(connectionUrl, registryEntry);
|
|
144
|
+
return redis;
|
|
145
|
+
}
|
|
146
|
+
function getCacheKey(key) {
|
|
147
|
+
return `${keyPrefix}${key}`;
|
|
148
|
+
}
|
|
149
|
+
function getTagKey(tag) {
|
|
150
|
+
return `${tagPrefix}${tag}`;
|
|
151
|
+
}
|
|
152
|
+
function isExpired(entry) {
|
|
153
|
+
if (entry.expiresAt === null)
|
|
154
|
+
return false;
|
|
155
|
+
return Date.now() > entry.expiresAt;
|
|
156
|
+
}
|
|
157
|
+
function matchPattern(key, pattern) {
|
|
158
|
+
const regexPattern = pattern
|
|
159
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
160
|
+
.replace(/\*/g, '.*')
|
|
161
|
+
.replace(/\?/g, '.');
|
|
162
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
163
|
+
return regex.test(key);
|
|
164
|
+
}
|
|
165
|
+
const get = async (key, options) => {
|
|
166
|
+
const client = await getRedisClient();
|
|
167
|
+
const cacheKey = getCacheKey(key);
|
|
168
|
+
const data = await client.get(cacheKey);
|
|
169
|
+
if (!data)
|
|
170
|
+
return null;
|
|
171
|
+
try {
|
|
172
|
+
const entry = JSON.parse(data);
|
|
173
|
+
if (isExpired(entry)) {
|
|
174
|
+
if (options === null || options === void 0 ? void 0 : options.returnExpired) {
|
|
175
|
+
return entry.value;
|
|
176
|
+
}
|
|
177
|
+
// Clean up expired entry
|
|
178
|
+
await deleteKey(key);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return entry.value;
|
|
182
|
+
}
|
|
183
|
+
catch (_a) {
|
|
184
|
+
// Invalid JSON, remove it
|
|
185
|
+
await client.del(cacheKey);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const set = async (key, value, options) => {
|
|
190
|
+
var _a;
|
|
191
|
+
const client = await getRedisClient();
|
|
192
|
+
const cacheKey = getCacheKey(key);
|
|
193
|
+
// Remove old entry from tag index if it exists
|
|
194
|
+
const oldData = await client.get(cacheKey);
|
|
195
|
+
if (oldData) {
|
|
196
|
+
try {
|
|
197
|
+
const oldEntry = JSON.parse(oldData);
|
|
198
|
+
// Remove from old tags
|
|
199
|
+
const pipeline = client.pipeline();
|
|
200
|
+
for (const tag of oldEntry.tags) {
|
|
201
|
+
pipeline.srem(getTagKey(tag), key);
|
|
202
|
+
}
|
|
203
|
+
await pipeline.exec();
|
|
204
|
+
}
|
|
205
|
+
catch (_b) {
|
|
206
|
+
// Ignore parse errors
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const ttl = (_a = options === null || options === void 0 ? void 0 : options.ttl) !== null && _a !== void 0 ? _a : defaultTtl;
|
|
210
|
+
const tags = (options === null || options === void 0 ? void 0 : options.tags) || [];
|
|
211
|
+
const expiresAt = ttl ? Date.now() + ttl : null;
|
|
212
|
+
const entry = {
|
|
213
|
+
key,
|
|
214
|
+
value,
|
|
215
|
+
tags,
|
|
216
|
+
expiresAt,
|
|
217
|
+
createdAt: Date.now(),
|
|
218
|
+
};
|
|
219
|
+
const pipeline = client.pipeline();
|
|
220
|
+
// Store the entry
|
|
221
|
+
const serialized = JSON.stringify(entry);
|
|
222
|
+
if (ttl) {
|
|
223
|
+
pipeline.setex(cacheKey, Math.ceil(ttl / 1000), serialized);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
pipeline.set(cacheKey, serialized);
|
|
227
|
+
}
|
|
228
|
+
// Add to tag index
|
|
229
|
+
for (const tag of tags) {
|
|
230
|
+
pipeline.sadd(getTagKey(tag), key);
|
|
231
|
+
}
|
|
232
|
+
await pipeline.exec();
|
|
233
|
+
};
|
|
234
|
+
const has = async (key) => {
|
|
235
|
+
const client = await getRedisClient();
|
|
236
|
+
const cacheKey = getCacheKey(key);
|
|
237
|
+
const exists = await client.exists(cacheKey);
|
|
238
|
+
if (!exists)
|
|
239
|
+
return false;
|
|
240
|
+
// Check if expired
|
|
241
|
+
const data = await client.get(cacheKey);
|
|
242
|
+
if (!data)
|
|
243
|
+
return false;
|
|
244
|
+
try {
|
|
245
|
+
const entry = JSON.parse(data);
|
|
246
|
+
if (isExpired(entry)) {
|
|
247
|
+
await deleteKey(key);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
catch (_a) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const deleteKey = async (key) => {
|
|
257
|
+
const client = await getRedisClient();
|
|
258
|
+
const cacheKey = getCacheKey(key);
|
|
259
|
+
// Get entry to remove from tag index
|
|
260
|
+
const data = await client.get(cacheKey);
|
|
261
|
+
if (!data)
|
|
262
|
+
return false;
|
|
263
|
+
try {
|
|
264
|
+
const entry = JSON.parse(data);
|
|
265
|
+
const pipeline = client.pipeline();
|
|
266
|
+
// Remove from tag index
|
|
267
|
+
for (const tag of entry.tags) {
|
|
268
|
+
pipeline.srem(getTagKey(tag), key);
|
|
269
|
+
}
|
|
270
|
+
// Delete the cache entry
|
|
271
|
+
pipeline.del(cacheKey);
|
|
272
|
+
await pipeline.exec();
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
catch (_a) {
|
|
276
|
+
// Just delete the key if we can't parse it
|
|
277
|
+
await client.del(cacheKey);
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
const deleteByTags = async (tags) => {
|
|
282
|
+
const client = await getRedisClient();
|
|
283
|
+
const keysToDelete = new Set();
|
|
284
|
+
// Collect all keys that have any of the specified tags
|
|
285
|
+
for (const tag of tags) {
|
|
286
|
+
const tagKey = getTagKey(tag);
|
|
287
|
+
const keys = await client.smembers(tagKey);
|
|
288
|
+
for (const key of keys) {
|
|
289
|
+
keysToDelete.add(key);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Delete all collected keys
|
|
293
|
+
let deleted = 0;
|
|
294
|
+
for (const key of keysToDelete) {
|
|
295
|
+
const success = await deleteKey(key);
|
|
296
|
+
if (success)
|
|
297
|
+
deleted++;
|
|
298
|
+
}
|
|
299
|
+
return deleted;
|
|
300
|
+
};
|
|
301
|
+
const clear = async () => {
|
|
302
|
+
const client = await getRedisClient();
|
|
303
|
+
// Get all cache keys
|
|
304
|
+
const cacheKeys = await client.keys(`${keyPrefix}*`);
|
|
305
|
+
const tagKeys = await client.keys(`${tagPrefix}*`);
|
|
306
|
+
if (cacheKeys.length === 0 && tagKeys.length === 0)
|
|
307
|
+
return 0;
|
|
308
|
+
const pipeline = client.pipeline();
|
|
309
|
+
for (const key of [...cacheKeys, ...tagKeys]) {
|
|
310
|
+
pipeline.del(key);
|
|
311
|
+
}
|
|
312
|
+
await pipeline.exec();
|
|
313
|
+
return cacheKeys.length;
|
|
314
|
+
};
|
|
315
|
+
const keys = async (pattern) => {
|
|
316
|
+
const client = await getRedisClient();
|
|
317
|
+
const searchPattern = pattern
|
|
318
|
+
? `${keyPrefix}${pattern}`
|
|
319
|
+
: `${keyPrefix}*`;
|
|
320
|
+
const cacheKeys = await client.keys(searchPattern);
|
|
321
|
+
// Remove prefix from keys
|
|
322
|
+
const result = cacheKeys.map((key) => key.substring(keyPrefix.length));
|
|
323
|
+
if (!pattern)
|
|
324
|
+
return result;
|
|
325
|
+
// Apply pattern matching (Redis KEYS command uses glob pattern, but we want our pattern)
|
|
326
|
+
return result.filter((key) => matchPattern(key, pattern));
|
|
327
|
+
};
|
|
328
|
+
const stats = async () => {
|
|
329
|
+
const client = await getRedisClient();
|
|
330
|
+
const cacheKeys = await client.keys(`${keyPrefix}*`);
|
|
331
|
+
let expired = 0;
|
|
332
|
+
for (const cacheKey of cacheKeys) {
|
|
333
|
+
const data = await client.get(cacheKey);
|
|
334
|
+
if (data) {
|
|
335
|
+
try {
|
|
336
|
+
const entry = JSON.parse(data);
|
|
337
|
+
if (isExpired(entry)) {
|
|
338
|
+
expired++;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (_a) {
|
|
342
|
+
// Ignore parse errors
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return { size: cacheKeys.length, expired };
|
|
347
|
+
};
|
|
348
|
+
const cleanup = async () => {
|
|
349
|
+
const client = await getRedisClient();
|
|
350
|
+
const cacheKeys = await client.keys(`${keyPrefix}*`);
|
|
351
|
+
let removed = 0;
|
|
352
|
+
for (const cacheKey of cacheKeys) {
|
|
353
|
+
const data = await client.get(cacheKey);
|
|
354
|
+
if (data) {
|
|
355
|
+
try {
|
|
356
|
+
const entry = JSON.parse(data);
|
|
357
|
+
if (isExpired(entry)) {
|
|
358
|
+
const key = cacheKey.substring(keyPrefix.length);
|
|
359
|
+
await deleteKey(key);
|
|
360
|
+
removed++;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (_a) {
|
|
364
|
+
// Remove invalid entries
|
|
365
|
+
await client.del(cacheKey);
|
|
366
|
+
removed++;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return removed;
|
|
371
|
+
};
|
|
372
|
+
const close = async () => {
|
|
373
|
+
await releaseRedisEntry(connectionUrl, registryEntry);
|
|
374
|
+
redis = null;
|
|
375
|
+
};
|
|
376
|
+
return {
|
|
377
|
+
get,
|
|
378
|
+
set,
|
|
379
|
+
has,
|
|
380
|
+
delete: deleteKey,
|
|
381
|
+
deleteByTags,
|
|
382
|
+
clear,
|
|
383
|
+
keys,
|
|
384
|
+
stats,
|
|
385
|
+
cleanup,
|
|
386
|
+
close,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CacheStrategy } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* SQLite cache strategy with tag support
|
|
4
|
+
* Persistent across process restarts, stored in a SQLite database file
|
|
5
|
+
*
|
|
6
|
+
* Uses two tables:
|
|
7
|
+
* - cache_entries: stores cache data
|
|
8
|
+
* - cache_tags: stores tag associations (many-to-many)
|
|
9
|
+
*/
|
|
10
|
+
export declare function createSqliteStrategy(dbPath?: string, options?: {
|
|
11
|
+
defaultTtl?: number;
|
|
12
|
+
}): CacheStrategy;
|
|
13
|
+
//# sourceMappingURL=sqlite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/strategies/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAgD,MAAM,UAAU,CAAA;AAuB3F;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CA6PtG"}
|