@warlock.js/cache 5.19.1 → 5.21.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/CHANGELOG.md +61 -1
- package/cjs/index.cjs +1439 -331
- package/cjs/index.cjs.map +1 -1
- package/esm/cache-manager.d.mts +4 -0
- package/esm/cache-manager.d.mts.map +1 -1
- package/esm/cache-manager.mjs +23 -5
- package/esm/cache-manager.mjs.map +1 -1
- package/esm/cached/auto-key.d.mts +14 -14
- package/esm/cached/auto-key.d.mts.map +1 -1
- package/esm/cached/auto-key.mjs +50 -34
- package/esm/cached/auto-key.mjs.map +1 -1
- package/esm/cached/cached.mjs +4 -0
- package/esm/cached/cached.mjs.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +84 -8
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +152 -42
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/file-cache-driver.d.mts +15 -0
- package/esm/drivers/file-cache-driver.d.mts.map +1 -1
- package/esm/drivers/file-cache-driver.mjs +68 -18
- package/esm/drivers/file-cache-driver.mjs.map +1 -1
- package/esm/drivers/in-memory-tag-index.d.mts +44 -0
- package/esm/drivers/in-memory-tag-index.d.mts.map +1 -0
- package/esm/drivers/in-memory-tag-index.mjs +73 -0
- package/esm/drivers/in-memory-tag-index.mjs.map +1 -0
- package/esm/drivers/index.d.mts +1 -1
- package/esm/drivers/index.mjs +1 -1
- package/esm/drivers/lru-memory-cache-driver.d.mts +43 -0
- package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/lru-memory-cache-driver.mjs +96 -0
- package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-cache-driver.d.mts +87 -30
- package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-cache-driver.mjs +214 -96
- package/esm/drivers/memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.d.mts +9 -5
- package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.mjs +10 -23
- package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -1
- package/esm/drivers/mock-cache-driver.d.mts +41 -0
- package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
- package/esm/drivers/mock-cache-driver.mjs +105 -8
- package/esm/drivers/mock-cache-driver.mjs.map +1 -1
- package/esm/drivers/null-cache-driver.d.mts +8 -2
- package/esm/drivers/null-cache-driver.d.mts.map +1 -1
- package/esm/drivers/null-cache-driver.mjs +10 -2
- package/esm/drivers/null-cache-driver.mjs.map +1 -1
- package/esm/drivers/pg-cache-driver.d.mts +77 -3
- package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
- package/esm/drivers/pg-cache-driver.mjs +229 -7
- package/esm/drivers/pg-cache-driver.mjs.map +1 -1
- package/esm/drivers/redis-cache-driver.d.mts +86 -1
- package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
- package/esm/drivers/redis-cache-driver.mjs +252 -16
- package/esm/drivers/redis-cache-driver.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/index.mjs +2 -2
- package/esm/list/memory-cache-list.d.mts +11 -8
- package/esm/list/memory-cache-list.d.mts.map +1 -1
- package/esm/list/memory-cache-list.mjs +39 -37
- package/esm/list/memory-cache-list.mjs.map +1 -1
- package/esm/scoped-cache.d.mts +9 -3
- package/esm/scoped-cache.d.mts.map +1 -1
- package/esm/scoped-cache.mjs +19 -7
- package/esm/scoped-cache.mjs.map +1 -1
- package/esm/tagged-cache.d.mts +20 -0
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +62 -24
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/tagged-scoped-cache.d.mts +7 -3
- package/esm/tagged-scoped-cache.d.mts.map +1 -1
- package/esm/tagged-scoped-cache.mjs +19 -7
- package/esm/tagged-scoped-cache.mjs.map +1 -1
- package/esm/types.d.mts +41 -3
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.d.mts +9 -3
- package/esm/utils.d.mts.map +1 -1
- package/esm/utils.mjs +16 -6
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +1685 -1666
- package/llms.txt +18 -18
- package/package.json +4 -4
- package/skills/apply-cache-patterns/SKILL.md +98 -97
- package/skills/configure-pg-cache/SKILL.md +6 -5
- package/skills/configure-set-options/SKILL.md +102 -96
- package/skills/handle-cache-errors/SKILL.md +92 -92
- package/skills/index.md +20 -0
- package/skills/observe-cache/SKILL.md +103 -103
- package/skills/pick-cache-driver/SKILL.md +129 -123
- package/skills/test-cache-code/SKILL.md +219 -219
- package/skills/use-cache-atomic/SKILL.md +57 -65
- package/skills/use-cache-bulk/SKILL.md +57 -57
- package/skills/use-cache-list/SKILL.md +85 -85
- package/skills/use-cache-lock/SKILL.md +113 -104
- package/skills/use-cache-namespace/SKILL.md +88 -88
- package/skills/use-cache-similarity/SKILL.md +94 -94
- package/skills/use-cache-tags/SKILL.md +7 -4
- package/skills/use-cache-update-merge/SKILL.md +84 -84
- package/skills/use-cache-utils/SKILL.md +89 -89
- package/skills/use-cached-hof/SKILL.md +100 -99
- package/skills/use-swr/SKILL.md +104 -104
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { CacheConfigurationError, CacheError, CacheUnsupportedError } from "../types.mjs";
|
|
2
2
|
import { BaseCacheDriver } from "./base-cache-driver.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { atomicWriteJsonAsync, ensureDirectoryAsync, getJsonFileAsync, listDirectoriesAsync, removeDirectoryAsync } from "@warlock.js/fs";
|
|
4
6
|
import path from "path";
|
|
5
7
|
|
|
6
8
|
//#region ../cache/src/drivers/file-cache-driver.ts
|
|
@@ -43,6 +45,7 @@ var FileCacheDriver = class extends BaseCacheDriver {
|
|
|
43
45
|
* only ever produces a single filesystem component here.
|
|
44
46
|
*/
|
|
45
47
|
keyDirectory(parsedKey) {
|
|
48
|
+
if (parsedKey === "") throw new CacheConfigurationError("Cache key must not be empty.");
|
|
46
49
|
return this.containedPath(this.encodeKeySegment(parsedKey));
|
|
47
50
|
}
|
|
48
51
|
/**
|
|
@@ -107,18 +110,17 @@ var FileCacheDriver = class extends BaseCacheDriver {
|
|
|
107
110
|
}
|
|
108
111
|
/**
|
|
109
112
|
* {@inheritdoc}
|
|
113
|
+
*
|
|
114
|
+
* `onConflict: "create"` is exclusive (temp file + hard link, so `lock()`
|
|
115
|
+
* is safe across concurrent writers). `onConflict: "update"` is NOT atomic:
|
|
116
|
+
* the existence probe and the write are separate steps.
|
|
110
117
|
*/
|
|
111
118
|
async set(key, value, ttlOrOptions) {
|
|
112
119
|
const parsedKey = this.parseKey(key);
|
|
113
120
|
const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);
|
|
114
121
|
if (vector) throw new CacheUnsupportedError("'file' driver does not support similarity retrieval — use a memory driver, 'pg' (with pgvector), or 'redis' (with RediSearch).");
|
|
115
122
|
this.log("caching", parsedKey);
|
|
116
|
-
const
|
|
117
|
-
const exists = existing !== null;
|
|
118
|
-
if (onConflict === "create" && exists) return {
|
|
119
|
-
wasSet: false,
|
|
120
|
-
existing
|
|
121
|
-
};
|
|
123
|
+
const exists = (onConflict === "upsert" || onConflict === "create" ? null : await this.get(key)) !== null;
|
|
122
124
|
if (onConflict === "update" && !exists) return {
|
|
123
125
|
wasSet: false,
|
|
124
126
|
existing: null
|
|
@@ -126,7 +128,14 @@ var FileCacheDriver = class extends BaseCacheDriver {
|
|
|
126
128
|
const data = this.prepareDataForStorage(value, ttl, staleAt);
|
|
127
129
|
const fileDirectory = this.keyDirectory(parsedKey);
|
|
128
130
|
await ensureDirectoryAsync(fileDirectory);
|
|
129
|
-
|
|
131
|
+
const filePath = path.resolve(fileDirectory, this.fileName);
|
|
132
|
+
if (onConflict === "create") {
|
|
133
|
+
const conflict = await this.writeExclusive(filePath, data);
|
|
134
|
+
if (conflict) return {
|
|
135
|
+
wasSet: false,
|
|
136
|
+
existing: conflict.existing
|
|
137
|
+
};
|
|
138
|
+
} else await atomicWriteJsonAsync(filePath, data);
|
|
130
139
|
if (tags && tags.length > 0) await this.applyTags(key, tags);
|
|
131
140
|
this.log("cached", parsedKey);
|
|
132
141
|
await this.emit("set", {
|
|
@@ -141,6 +150,44 @@ var FileCacheDriver = class extends BaseCacheDriver {
|
|
|
141
150
|
return this;
|
|
142
151
|
}
|
|
143
152
|
/**
|
|
153
|
+
* Create `filePath` only if it does not exist. Writes a temp file, then
|
|
154
|
+
* hard-links it to the target (EEXIST = already present). An existing but
|
|
155
|
+
* expired entry is removed (the file only, never the directory) and the
|
|
156
|
+
* link retried once.
|
|
157
|
+
*
|
|
158
|
+
* Returns `null` when written, or `{ existing }` on conflict.
|
|
159
|
+
*/
|
|
160
|
+
async writeExclusive(filePath, data) {
|
|
161
|
+
const tempPath = `${filePath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
162
|
+
try {
|
|
163
|
+
await fs.promises.writeFile(tempPath, JSON.stringify(data, null, 2));
|
|
164
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
165
|
+
try {
|
|
166
|
+
await fs.promises.link(tempPath, filePath);
|
|
167
|
+
return null;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error?.code !== "EEXIST") throw error;
|
|
170
|
+
}
|
|
171
|
+
let current;
|
|
172
|
+
try {
|
|
173
|
+
current = await getJsonFileAsync(filePath);
|
|
174
|
+
} catch {
|
|
175
|
+
current = void 0;
|
|
176
|
+
}
|
|
177
|
+
const expired = current?.expiresAt !== void 0 && current.expiresAt !== null && current.expiresAt <= Date.now();
|
|
178
|
+
if (current && !expired) return { existing: current.data ?? null };
|
|
179
|
+
if (attempt === 0 && expired) {
|
|
180
|
+
await fs.promises.rm(filePath, { force: true });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
return { existing: null };
|
|
184
|
+
}
|
|
185
|
+
return { existing: null };
|
|
186
|
+
} finally {
|
|
187
|
+
await fs.promises.unlink(tempPath).catch(() => void 0);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
144
191
|
* {@inheritdoc}
|
|
145
192
|
*
|
|
146
193
|
* File driver does not yet ship with a file-lock primitive, so concurrent
|
|
@@ -181,21 +228,24 @@ var FileCacheDriver = class extends BaseCacheDriver {
|
|
|
181
228
|
const parsedKey = this.parseKey(key);
|
|
182
229
|
this.log("fetching", parsedKey);
|
|
183
230
|
const fileDirectory = this.keyDirectory(parsedKey);
|
|
231
|
+
let value;
|
|
184
232
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
value: result
|
|
191
|
-
});
|
|
192
|
-
return result;
|
|
193
|
-
} catch (error) {
|
|
233
|
+
value = await getJsonFileAsync(path.resolve(fileDirectory, this.fileName));
|
|
234
|
+
} catch {
|
|
235
|
+
value = void 0;
|
|
236
|
+
}
|
|
237
|
+
if (value === void 0 || value === null || typeof value !== "object") {
|
|
194
238
|
this.log("notFound", parsedKey);
|
|
195
239
|
await this.emit("miss", { key: parsedKey });
|
|
196
|
-
await this.remove(key);
|
|
197
240
|
return null;
|
|
198
241
|
}
|
|
242
|
+
const result = await this.parseCachedData(parsedKey, value);
|
|
243
|
+
if (result === null) await this.emit("miss", { key: parsedKey });
|
|
244
|
+
else await this.emit("hit", {
|
|
245
|
+
key: parsedKey,
|
|
246
|
+
value: result
|
|
247
|
+
});
|
|
248
|
+
return result;
|
|
199
249
|
}
|
|
200
250
|
/**
|
|
201
251
|
* {@inheritdoc}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/file-cache-driver.ts"],"sourcesContent":["import {\n ensureDirectoryAsync,\n getJsonFileAsync,\n listDirectoriesAsync,\n putJsonFileAsync,\n removeDirectoryAsync,\n} from \"@warlock.js/fs\";\nimport path from \"path\";\nimport type {\n CacheData,\n CacheDriver,\n CacheKey,\n CacheSetOptions,\n CacheSetResult,\n CacheTtl,\n FileCacheOptions,\n} from \"../types\";\nimport { CacheConfigurationError, CacheError, CacheUnsupportedError } from \"../types\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\nexport class FileCacheDriver\n extends BaseCacheDriver<FileCacheDriver, FileCacheOptions>\n implements CacheDriver<FileCacheDriver, FileCacheOptions>\n{\n /**\n * {@inheritdoc}\n */\n public name = \"file\";\n\n /**\n * {@inheritdoc}\n */\n public setOptions(options: FileCacheOptions) {\n if (!options.directory) {\n throw new CacheConfigurationError(\n \"File driver requires 'directory' option to be configured.\",\n );\n }\n\n return super.setOptions(options);\n }\n\n /**\n * Get the cache directory\n */\n public get directory() {\n const directory = this.options.directory;\n\n if (typeof directory === \"function\") {\n return directory();\n }\n\n throw new CacheConfigurationError(\n \"Cache directory is not defined, please define it in the file driver options\",\n );\n }\n\n /**\n * Get file name\n */\n public get fileName() {\n const fileName = this.options.fileName;\n\n if (typeof fileName === \"function\") {\n return fileName();\n }\n\n return \"cache.json\";\n }\n\n /**\n * Map a parsed cache key (or namespace) to its on-disk directory.\n *\n * The key must become exactly one directory component: `%` and the path\n * separators are percent-encoded so a hostile key (`../../etc`,\n * `..\\\\..\\\\evil`) turns into an inert directory name instead of a path\n * traversal, while distinct keys can never collide after encoding. Dots are\n * left untouched — the `.`-delimited namespace scheme is purely logical and\n * only ever produces a single filesystem component here.\n */\n protected keyDirectory(parsedKey: string): string {\n return this.containedPath(this.encodeKeySegment(parsedKey));\n }\n\n /**\n * Percent-encode a parsed key into an inert, single-component directory\n * name. Dots are left untouched — see {@link keyDirectory}.\n */\n protected encodeKeySegment(parsedKey: string): string {\n return parsedKey\n .replace(/%/g, \"%25\")\n .replace(/\\//g, \"%2F\")\n .replace(/\\\\/g, \"%5C\");\n }\n\n /**\n * Reverse of {@link encodeKeySegment} — recovers the logical, dot-delimited\n * parsed key from an on-disk directory name. Used by {@link removeNamespace}\n * to test which sibling directories logically belong to a namespace, since\n * dotted keys (`ns.a`) are stored as sibling directories rather than nested\n * ones (see {@link keyDirectory}'s doc comment).\n */\n protected decodeKeySegment(encoded: string): string {\n return encoded\n .replace(/%2F/g, \"/\")\n .replace(/%5C/g, \"\\\\\")\n .replace(/%25/g, \"%\");\n }\n\n /**\n * Resolve `segment` against the cache root and throw when the result lands\n * outside it — the last line of defense against path traversal, independent\n * of how the key was encoded.\n */\n protected containedPath(segment: string): string {\n const base = path.resolve(this.directory);\n const resolved = path.resolve(base, segment);\n const relative = path.relative(base, resolved);\n\n if (\n relative === \"..\" ||\n relative.startsWith(`..${path.sep}`) ||\n path.isAbsolute(relative)\n ) {\n throw new CacheError(\n `Cache key resolves outside the cache directory: \"${segment}\"`,\n );\n }\n\n return resolved;\n }\n\n /**\n * {@inheritdoc}\n *\n * Dotted keys (`ns.a`) are stored as sibling directories, not nested ones\n * (see {@link keyDirectory}) — a directory literally named `<namespace>`\n * rarely exists on its own. Namespace removal therefore has to scan the\n * cache root's immediate children and remove every directory whose\n * *logical* (decoded) name equals the namespace or starts with\n * `<namespace>.`, mirroring the `key = $1 OR key LIKE $2` boundary\n * semantics the `pg` driver already uses for the same contract.\n */\n public async removeNamespace(namespace: string) {\n const parsedNamespace = this.parseKey(namespace);\n\n this.log(\"clearing\", namespace);\n\n try {\n const root = this.containedPath(\"\");\n\n if (parsedNamespace === \"\") {\n await removeDirectoryAsync(root);\n this.log(\"cleared\", namespace);\n return this;\n }\n\n const prefix = `${parsedNamespace}.`;\n const entries = await listDirectoriesAsync(root).catch(() => [] as string[]);\n\n await Promise.all(\n entries.map(async (entryPath) => {\n const decoded = this.decodeKeySegment(path.basename(entryPath));\n\n if (decoded === parsedNamespace || decoded.startsWith(prefix)) {\n await removeDirectoryAsync(entryPath);\n }\n }),\n );\n\n this.log(\"cleared\", namespace);\n } catch (error) {\n //\n }\n\n return this;\n }\n\n /**\n * {@inheritdoc}\n */\n public async set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const parsedKey = this.parseKey(key);\n const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);\n\n if (vector) {\n throw new CacheUnsupportedError(\n \"'file' driver does not support similarity retrieval — use a memory driver, 'pg' (with pgvector), or 'redis' (with RediSearch).\",\n );\n }\n\n this.log(\"caching\", parsedKey);\n\n const existing = onConflict === \"upsert\" ? null : await this.get(key);\n const exists = existing !== null;\n\n if (onConflict === \"create\" && exists) {\n const result: CacheSetResult = { wasSet: false, existing };\n return result;\n }\n\n if (onConflict === \"update\" && !exists) {\n const result: CacheSetResult = { wasSet: false, existing: null };\n return result;\n }\n\n const data = this.prepareDataForStorage(value, ttl, staleAt);\n\n const fileDirectory = this.keyDirectory(parsedKey);\n\n await ensureDirectoryAsync(fileDirectory);\n\n await putJsonFileAsync(path.resolve(fileDirectory, this.fileName), data);\n\n if (tags && tags.length > 0) {\n await this.applyTags(key, tags);\n }\n\n this.log(\"cached\", parsedKey);\n\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n\n if (onConflict === \"create\" || onConflict === \"update\") {\n const result: CacheSetResult = { wasSet: true, existing: null };\n return result;\n }\n\n return this;\n }\n\n /**\n * {@inheritdoc}\n *\n * File driver does not yet ship with a file-lock primitive, so concurrent\n * writers could clobber each other. Rather than ship an unsafe default, we\n * throw — consumers can fall back to memory/redis for `update` until a\n * proper file lock lands (tracked in `domains/cache/backlog.md`).\n */\n public async update(): Promise<never> {\n throw new CacheUnsupportedError(\n \"`update()` is not supported on the file driver. Use the memory or redis driver, or wait for the file-lock primitive (see domains/cache/backlog.md).\",\n );\n }\n\n /**\n * {@inheritdoc}\n */\n public async merge(): Promise<never> {\n throw new CacheUnsupportedError(\n \"`merge()` is not supported on the file driver. Use the memory or redis driver.\",\n );\n }\n\n /**\n * Read the raw {@link CacheData} wrapper from disk, including `staleAt`\n * metadata. Returns `null` for missing or expired files — `swr()`\n * consumes this to branch on freshness.\n */\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\n const parsedKey = this.parseKey(key);\n const fileDirectory = this.keyDirectory(parsedKey);\n\n try {\n const entry = (await getJsonFileAsync(path.resolve(fileDirectory, this.fileName))) as\n | CacheData\n | undefined;\n\n if (!entry) {\n return null;\n }\n\n if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) {\n return null;\n }\n\n return entry;\n } catch {\n return null;\n }\n }\n\n /**\n * {@inheritdoc}\n */\n public async get(key: CacheKey) {\n const parsedKey = this.parseKey(key);\n\n this.log(\"fetching\", parsedKey);\n\n const fileDirectory = this.keyDirectory(parsedKey);\n\n try {\n const value = await getJsonFileAsync(path.resolve(fileDirectory, this.fileName));\n\n const result = await this.parseCachedData(parsedKey, value as CacheData);\n\n if (result === null) {\n // Expired\n await this.emit(\"miss\", { key: parsedKey });\n } else {\n // Emit hit event\n await this.emit(\"hit\", { key: parsedKey, value: result });\n }\n\n return result;\n } catch (error) {\n this.log(\"notFound\", parsedKey);\n // Emit miss event\n await this.emit(\"miss\", { key: parsedKey });\n // Await the cleanup so it fully settles before returning. Leaving this\n // fire-and-forget let the async directory removal race a follow-up write\n // to the same key (e.g. the existence probe inside a `set({ onConflict })`\n // call), surfacing as an ENOENT mkdir/rm collision on Windows.\n await this.remove(key);\n return null;\n }\n }\n\n /**\n * {@inheritdoc}\n */\n public async remove(key: CacheKey) {\n const parsedKey = this.parseKey(key);\n this.log(\"removing\", parsedKey);\n\n const fileDirectory = this.keyDirectory(parsedKey);\n\n try {\n await removeDirectoryAsync(fileDirectory);\n\n this.log(\"removed\", parsedKey);\n // Emit removed event\n await this.emit(\"removed\", { key: parsedKey });\n } catch (error) {\n //\n }\n }\n\n /**\n * {@inheritdoc}\n */\n public async flush() {\n this.log(\"flushing\");\n\n if (this.options.globalPrefix) {\n await this.removeNamespace(\"\");\n } else {\n await removeDirectoryAsync(this.directory);\n }\n\n this.log(\"flushed\");\n\n // Emit flushed event\n await this.emit(\"flushed\");\n }\n\n /**\n * {@inheritdoc}\n */\n public async connect() {\n this.log(\"connecting\");\n await ensureDirectoryAsync(this.directory);\n this.log(\"connected\");\n await this.emit(\"connected\");\n }\n}\n"],"mappings":";;;;;;AAoBA,IAAa,kBAAb,cACU,gBAEV;;;cAIgB;;;;;CAKd,AAAO,WAAW,SAA2B;EAC3C,IAAI,CAAC,QAAQ,WACX,MAAM,IAAI,wBACR,2DACF;EAGF,OAAO,MAAM,WAAW,OAAO;CACjC;;;;CAKA,IAAW,YAAY;EACrB,MAAM,YAAY,KAAK,QAAQ;EAE/B,IAAI,OAAO,cAAc,YACvB,OAAO,UAAU;EAGnB,MAAM,IAAI,wBACR,6EACF;CACF;;;;CAKA,IAAW,WAAW;EACpB,MAAM,WAAW,KAAK,QAAQ;EAE9B,IAAI,OAAO,aAAa,YACtB,OAAO,SAAS;EAGlB,OAAO;CACT;;;;;;;;;;;CAYA,AAAU,aAAa,WAA2B;EAChD,OAAO,KAAK,cAAc,KAAK,iBAAiB,SAAS,CAAC;CAC5D;;;;;CAMA,AAAU,iBAAiB,WAA2B;EACpD,OAAO,UACJ,QAAQ,MAAM,KAAK,CAAC,CACpB,QAAQ,OAAO,KAAK,CAAC,CACrB,QAAQ,OAAO,KAAK;CACzB;;;;;;;;CASA,AAAU,iBAAiB,SAAyB;EAClD,OAAO,QACJ,QAAQ,QAAQ,GAAG,CAAC,CACpB,QAAQ,QAAQ,IAAI,CAAC,CACrB,QAAQ,QAAQ,GAAG;CACxB;;;;;;CAOA,AAAU,cAAc,SAAyB;EAC/C,MAAM,OAAO,KAAK,QAAQ,KAAK,SAAS;EACxC,MAAM,WAAW,KAAK,QAAQ,MAAM,OAAO;EAC3C,MAAM,WAAW,KAAK,SAAS,MAAM,QAAQ;EAE7C,IACE,aAAa,QACb,SAAS,WAAW,KAAK,KAAK,KAAK,KACnC,KAAK,WAAW,QAAQ,GAExB,MAAM,IAAI,WACR,oDAAoD,QAAQ,EAC9D;EAGF,OAAO;CACT;;;;;;;;;;;;CAaA,MAAa,gBAAgB,WAAmB;EAC9C,MAAM,kBAAkB,KAAK,SAAS,SAAS;EAE/C,KAAK,IAAI,YAAY,SAAS;EAE9B,IAAI;GACF,MAAM,OAAO,KAAK,cAAc,EAAE;GAElC,IAAI,oBAAoB,IAAI;IAC1B,MAAM,qBAAqB,IAAI;IAC/B,KAAK,IAAI,WAAW,SAAS;IAC7B,OAAO;GACT;GAEA,MAAM,SAAS,GAAG,gBAAgB;GAClC,MAAM,UAAU,MAAM,qBAAqB,IAAI,CAAC,CAAC,YAAY,CAAC,CAAa;GAE3E,MAAM,QAAQ,IACZ,QAAQ,IAAI,OAAO,cAAc;IAC/B,MAAM,UAAU,KAAK,iBAAiB,KAAK,SAAS,SAAS,CAAC;IAE9D,IAAI,YAAY,mBAAmB,QAAQ,WAAW,MAAM,GAC1D,MAAM,qBAAqB,SAAS;GAExC,CAAC,CACH;GAEA,KAAK,IAAI,WAAW,SAAS;EAC/B,SAAS,OAAO,CAEhB;EAEA,OAAO;CACT;;;;CAKA,MAAa,IACX,KACA,OACA,cACc;EACd,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,MAAM,EAAE,KAAK,MAAM,YAAY,QAAQ,YAAY,KAAK,kBAAkB,YAAY;EAEtF,IAAI,QACF,MAAM,IAAI,sBACR,gIACF;EAGF,KAAK,IAAI,WAAW,SAAS;EAE7B,MAAM,WAAW,eAAe,WAAW,OAAO,MAAM,KAAK,IAAI,GAAG;EACpE,MAAM,SAAS,aAAa;EAE5B,IAAI,eAAe,YAAY,QAE7B,OAAO;GAD0B,QAAQ;GAAO;EACpC;EAGd,IAAI,eAAe,YAAY,CAAC,QAE9B,OAAO;GAD0B,QAAQ;GAAO,UAAU;EAC9C;EAGd,MAAM,OAAO,KAAK,sBAAsB,OAAO,KAAK,OAAO;EAE3D,MAAM,gBAAgB,KAAK,aAAa,SAAS;EAEjD,MAAM,qBAAqB,aAAa;EAExC,MAAM,iBAAiB,KAAK,QAAQ,eAAe,KAAK,QAAQ,GAAG,IAAI;EAEvE,IAAI,QAAQ,KAAK,SAAS,GACxB,MAAM,KAAK,UAAU,KAAK,IAAI;EAGhC,KAAK,IAAI,UAAU,SAAS;EAE5B,MAAM,KAAK,KAAK,OAAO;GAAE,KAAK;GAAW;GAAO;EAAI,CAAC;EAErD,IAAI,eAAe,YAAY,eAAe,UAE5C,OAAO;GAD0B,QAAQ;GAAM,UAAU;EAC7C;EAGd,OAAO;CACT;;;;;;;;;CAUA,MAAa,SAAyB;EACpC,MAAM,IAAI,sBACR,qJACF;CACF;;;;CAKA,MAAa,QAAwB;EACnC,MAAM,IAAI,sBACR,gFACF;CACF;;;;;;CAOA,MAAgB,SAAS,KAA0C;EACjE,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,MAAM,gBAAgB,KAAK,aAAa,SAAS;EAEjD,IAAI;GACF,MAAM,QAAS,MAAM,iBAAiB,KAAK,QAAQ,eAAe,KAAK,QAAQ,CAAC;GAIhF,IAAI,CAAC,OACH,OAAO;GAGT,IAAI,MAAM,cAAc,UAAa,MAAM,aAAa,KAAK,IAAI,GAC/D,OAAO;GAGT,OAAO;EACT,QAAQ;GACN,OAAO;EACT;CACF;;;;CAKA,MAAa,IAAI,KAAe;EAC9B,MAAM,YAAY,KAAK,SAAS,GAAG;EAEnC,KAAK,IAAI,YAAY,SAAS;EAE9B,MAAM,gBAAgB,KAAK,aAAa,SAAS;EAEjD,IAAI;GACF,MAAM,QAAQ,MAAM,iBAAiB,KAAK,QAAQ,eAAe,KAAK,QAAQ,CAAC;GAE/E,MAAM,SAAS,MAAM,KAAK,gBAAgB,WAAW,KAAkB;GAEvE,IAAI,WAAW,MAEb,MAAM,KAAK,KAAK,QAAQ,EAAE,KAAK,UAAU,CAAC;QAG1C,MAAM,KAAK,KAAK,OAAO;IAAE,KAAK;IAAW,OAAO;GAAO,CAAC;GAG1D,OAAO;EACT,SAAS,OAAO;GACd,KAAK,IAAI,YAAY,SAAS;GAE9B,MAAM,KAAK,KAAK,QAAQ,EAAE,KAAK,UAAU,CAAC;GAK1C,MAAM,KAAK,OAAO,GAAG;GACrB,OAAO;EACT;CACF;;;;CAKA,MAAa,OAAO,KAAe;EACjC,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,KAAK,IAAI,YAAY,SAAS;EAE9B,MAAM,gBAAgB,KAAK,aAAa,SAAS;EAEjD,IAAI;GACF,MAAM,qBAAqB,aAAa;GAExC,KAAK,IAAI,WAAW,SAAS;GAE7B,MAAM,KAAK,KAAK,WAAW,EAAE,KAAK,UAAU,CAAC;EAC/C,SAAS,OAAO,CAEhB;CACF;;;;CAKA,MAAa,QAAQ;EACnB,KAAK,IAAI,UAAU;EAEnB,IAAI,KAAK,QAAQ,cACf,MAAM,KAAK,gBAAgB,EAAE;OAE7B,MAAM,qBAAqB,KAAK,SAAS;EAG3C,KAAK,IAAI,SAAS;EAGlB,MAAM,KAAK,KAAK,SAAS;CAC3B;;;;CAKA,MAAa,UAAU;EACrB,KAAK,IAAI,YAAY;EACrB,MAAM,qBAAqB,KAAK,SAAS;EACzC,KAAK,IAAI,WAAW;EACpB,MAAM,KAAK,KAAK,WAAW;CAC7B;AACF"}
|
|
1
|
+
{"version":3,"file":"file-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/file-cache-driver.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport {\n atomicWriteJsonAsync,\n ensureDirectoryAsync,\n getJsonFileAsync,\n listDirectoriesAsync,\n removeDirectoryAsync,\n} from \"@warlock.js/fs\";\nimport path from \"path\";\nimport type {\n CacheData,\n CacheDriver,\n CacheKey,\n CacheSetOptions,\n CacheSetResult,\n CacheTtl,\n FileCacheOptions,\n} from \"../types\";\nimport { CacheConfigurationError, CacheError, CacheUnsupportedError } from \"../types\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\nexport class FileCacheDriver\n extends BaseCacheDriver<FileCacheDriver, FileCacheOptions>\n implements CacheDriver<FileCacheDriver, FileCacheOptions>\n{\n /**\n * {@inheritdoc}\n */\n public name = \"file\";\n\n /**\n * {@inheritdoc}\n */\n public setOptions(options: FileCacheOptions) {\n if (!options.directory) {\n throw new CacheConfigurationError(\n \"File driver requires 'directory' option to be configured.\",\n );\n }\n\n return super.setOptions(options);\n }\n\n /**\n * Get the cache directory\n */\n public get directory() {\n const directory = this.options.directory;\n\n if (typeof directory === \"function\") {\n return directory();\n }\n\n throw new CacheConfigurationError(\n \"Cache directory is not defined, please define it in the file driver options\",\n );\n }\n\n /**\n * Get file name\n */\n public get fileName() {\n const fileName = this.options.fileName;\n\n if (typeof fileName === \"function\") {\n return fileName();\n }\n\n return \"cache.json\";\n }\n\n /**\n * Map a parsed cache key (or namespace) to its on-disk directory.\n *\n * The key must become exactly one directory component: `%` and the path\n * separators are percent-encoded so a hostile key (`../../etc`,\n * `..\\\\..\\\\evil`) turns into an inert directory name instead of a path\n * traversal, while distinct keys can never collide after encoding. Dots are\n * left untouched — the `.`-delimited namespace scheme is purely logical and\n * only ever produces a single filesystem component here.\n */\n protected keyDirectory(parsedKey: string): string {\n // An empty key would resolve to the cache root — `remove(\"{}\")` must never\n // be able to wipe it.\n if (parsedKey === \"\") {\n throw new CacheConfigurationError(\"Cache key must not be empty.\");\n }\n\n return this.containedPath(this.encodeKeySegment(parsedKey));\n }\n\n /**\n * Percent-encode a parsed key into an inert, single-component directory\n * name. Dots are left untouched — see {@link keyDirectory}.\n */\n protected encodeKeySegment(parsedKey: string): string {\n return parsedKey\n .replace(/%/g, \"%25\")\n .replace(/\\//g, \"%2F\")\n .replace(/\\\\/g, \"%5C\");\n }\n\n /**\n * Reverse of {@link encodeKeySegment} — recovers the logical, dot-delimited\n * parsed key from an on-disk directory name. Used by {@link removeNamespace}\n * to test which sibling directories logically belong to a namespace, since\n * dotted keys (`ns.a`) are stored as sibling directories rather than nested\n * ones (see {@link keyDirectory}'s doc comment).\n */\n protected decodeKeySegment(encoded: string): string {\n return encoded\n .replace(/%2F/g, \"/\")\n .replace(/%5C/g, \"\\\\\")\n .replace(/%25/g, \"%\");\n }\n\n /**\n * Resolve `segment` against the cache root and throw when the result lands\n * outside it — the last line of defense against path traversal, independent\n * of how the key was encoded.\n */\n protected containedPath(segment: string): string {\n const base = path.resolve(this.directory);\n const resolved = path.resolve(base, segment);\n const relative = path.relative(base, resolved);\n\n if (\n relative === \"..\" ||\n relative.startsWith(`..${path.sep}`) ||\n path.isAbsolute(relative)\n ) {\n throw new CacheError(\n `Cache key resolves outside the cache directory: \"${segment}\"`,\n );\n }\n\n return resolved;\n }\n\n /**\n * {@inheritdoc}\n *\n * Dotted keys (`ns.a`) are stored as sibling directories, not nested ones\n * (see {@link keyDirectory}) — a directory literally named `<namespace>`\n * rarely exists on its own. Namespace removal therefore has to scan the\n * cache root's immediate children and remove every directory whose\n * *logical* (decoded) name equals the namespace or starts with\n * `<namespace>.`, mirroring the `key = $1 OR key LIKE $2` boundary\n * semantics the `pg` driver already uses for the same contract.\n */\n public async removeNamespace(namespace: string) {\n const parsedNamespace = this.parseKey(namespace);\n\n this.log(\"clearing\", namespace);\n\n try {\n const root = this.containedPath(\"\");\n\n if (parsedNamespace === \"\") {\n await removeDirectoryAsync(root);\n this.log(\"cleared\", namespace);\n return this;\n }\n\n const prefix = `${parsedNamespace}.`;\n const entries = await listDirectoriesAsync(root).catch(() => [] as string[]);\n\n await Promise.all(\n entries.map(async (entryPath) => {\n const decoded = this.decodeKeySegment(path.basename(entryPath));\n\n if (decoded === parsedNamespace || decoded.startsWith(prefix)) {\n await removeDirectoryAsync(entryPath);\n }\n }),\n );\n\n this.log(\"cleared\", namespace);\n } catch (error) {\n //\n }\n\n return this;\n }\n\n /**\n * {@inheritdoc}\n *\n * `onConflict: \"create\"` is exclusive (temp file + hard link, so `lock()`\n * is safe across concurrent writers). `onConflict: \"update\"` is NOT atomic:\n * the existence probe and the write are separate steps.\n */\n public async set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const parsedKey = this.parseKey(key);\n const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);\n\n if (vector) {\n throw new CacheUnsupportedError(\n \"'file' driver does not support similarity retrieval — use a memory driver, 'pg' (with pgvector), or 'redis' (with RediSearch).\",\n );\n }\n\n this.log(\"caching\", parsedKey);\n\n const existing = onConflict === \"upsert\" || onConflict === \"create\" ? null : await this.get(key);\n const exists = existing !== null;\n\n if (onConflict === \"update\" && !exists) {\n const result: CacheSetResult = { wasSet: false, existing: null };\n return result;\n }\n\n const data = this.prepareDataForStorage(value, ttl, staleAt);\n\n const fileDirectory = this.keyDirectory(parsedKey);\n\n await ensureDirectoryAsync(fileDirectory);\n\n const filePath = path.resolve(fileDirectory, this.fileName);\n\n if (onConflict === \"create\") {\n const conflict = await this.writeExclusive(filePath, data);\n\n if (conflict) {\n const result: CacheSetResult = { wasSet: false, existing: conflict.existing };\n return result;\n }\n } else {\n await atomicWriteJsonAsync(filePath, data);\n }\n\n if (tags && tags.length > 0) {\n await this.applyTags(key, tags);\n }\n\n this.log(\"cached\", parsedKey);\n\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n\n if (onConflict === \"create\" || onConflict === \"update\") {\n const result: CacheSetResult = { wasSet: true, existing: null };\n return result;\n }\n\n return this;\n }\n\n /**\n * Create `filePath` only if it does not exist. Writes a temp file, then\n * hard-links it to the target (EEXIST = already present). An existing but\n * expired entry is removed (the file only, never the directory) and the\n * link retried once.\n *\n * Returns `null` when written, or `{ existing }` on conflict.\n */\n protected async writeExclusive(\n filePath: string,\n data: CacheData,\n ): Promise<{ existing: any } | null> {\n const tempPath = `${filePath}.${randomBytes(6).toString(\"hex\")}.tmp`;\n\n try {\n await fs.promises.writeFile(tempPath, JSON.stringify(data, null, 2));\n\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n await fs.promises.link(tempPath, filePath);\n return null;\n } catch (error: any) {\n if (error?.code !== \"EEXIST\") {\n throw error;\n }\n }\n\n let current: CacheData | undefined;\n\n try {\n current = (await getJsonFileAsync(filePath)) as CacheData | undefined;\n } catch {\n current = undefined;\n }\n\n const expired =\n current?.expiresAt !== undefined && current.expiresAt !== null && current.expiresAt <= Date.now();\n\n if (current && !expired) {\n return { existing: current.data ?? null };\n }\n\n if (attempt === 0 && expired) {\n await fs.promises.rm(filePath, { force: true });\n continue;\n }\n\n // Unreadable (corrupt) file: treat as held, never delete it.\n return { existing: null };\n }\n\n return { existing: null };\n } finally {\n await fs.promises.unlink(tempPath).catch(() => undefined);\n }\n }\n\n /**\n * {@inheritdoc}\n *\n * File driver does not yet ship with a file-lock primitive, so concurrent\n * writers could clobber each other. Rather than ship an unsafe default, we\n * throw — consumers can fall back to memory/redis for `update` until a\n * proper file lock lands (tracked in `domains/cache/backlog.md`).\n */\n public async update(): Promise<never> {\n throw new CacheUnsupportedError(\n \"`update()` is not supported on the file driver. Use the memory or redis driver, or wait for the file-lock primitive (see domains/cache/backlog.md).\",\n );\n }\n\n /**\n * {@inheritdoc}\n */\n public async merge(): Promise<never> {\n throw new CacheUnsupportedError(\n \"`merge()` is not supported on the file driver. Use the memory or redis driver.\",\n );\n }\n\n /**\n * Read the raw {@link CacheData} wrapper from disk, including `staleAt`\n * metadata. Returns `null` for missing or expired files — `swr()`\n * consumes this to branch on freshness.\n */\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\n const parsedKey = this.parseKey(key);\n const fileDirectory = this.keyDirectory(parsedKey);\n\n try {\n const entry = (await getJsonFileAsync(path.resolve(fileDirectory, this.fileName))) as\n | CacheData\n | undefined;\n\n if (!entry) {\n return null;\n }\n\n if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) {\n return null;\n }\n\n return entry;\n } catch {\n return null;\n }\n }\n\n /**\n * {@inheritdoc}\n */\n public async get(key: CacheKey) {\n const parsedKey = this.parseKey(key);\n\n this.log(\"fetching\", parsedKey);\n\n const fileDirectory = this.keyDirectory(parsedKey);\n\n let value: CacheData | undefined;\n\n try {\n value = (await getJsonFileAsync(path.resolve(fileDirectory, this.fileName))) as\n | CacheData\n | undefined;\n } catch {\n // Missing file (ENOENT) or corrupt JSON: a plain miss. Never remove the\n // directory here — it may be mid-write by another process.\n value = undefined;\n }\n\n if (value === undefined || value === null || typeof value !== \"object\") {\n this.log(\"notFound\", parsedKey);\n await this.emit(\"miss\", { key: parsedKey });\n return null;\n }\n\n // Only a successfully parsed but expired entry is removed (inside\n // parseCachedData).\n const result = await this.parseCachedData(parsedKey, value);\n\n if (result === null) {\n await this.emit(\"miss\", { key: parsedKey });\n } else {\n await this.emit(\"hit\", { key: parsedKey, value: result });\n }\n\n return result;\n }\n\n /**\n * {@inheritdoc}\n */\n public async remove(key: CacheKey) {\n const parsedKey = this.parseKey(key);\n this.log(\"removing\", parsedKey);\n\n const fileDirectory = this.keyDirectory(parsedKey);\n\n try {\n await removeDirectoryAsync(fileDirectory);\n\n this.log(\"removed\", parsedKey);\n // Emit removed event\n await this.emit(\"removed\", { key: parsedKey });\n } catch (error) {\n //\n }\n }\n\n /**\n * {@inheritdoc}\n */\n public async flush() {\n this.log(\"flushing\");\n\n if (this.options.globalPrefix) {\n await this.removeNamespace(\"\");\n } else {\n await removeDirectoryAsync(this.directory);\n }\n\n this.log(\"flushed\");\n\n // Emit flushed event\n await this.emit(\"flushed\");\n }\n\n /**\n * {@inheritdoc}\n */\n public async connect() {\n this.log(\"connecting\");\n await ensureDirectoryAsync(this.directory);\n this.log(\"connected\");\n await this.emit(\"connected\");\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region ../cache/src/drivers/in-memory-tag-index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Tag index for the in-process drivers (memory, memoryExtended, lru, mock).
|
|
4
|
+
*
|
|
5
|
+
* Lives beside the driver's entry storage, never inside it: index sets are not
|
|
6
|
+
* entries, so they are never subject to `maxSize` / LRU eviction or TTL
|
|
7
|
+
* sweeps, and every operation is synchronous — concurrent tagged writes in one
|
|
8
|
+
* process can never drop a member.
|
|
9
|
+
*
|
|
10
|
+
* Keys are the driver-parsed tag keys (`<prefix>.cache.tags.<tag>`); members
|
|
11
|
+
* are the caller's normalized, un-prefixed cache keys.
|
|
12
|
+
*/
|
|
13
|
+
declare class InMemoryTagIndex {
|
|
14
|
+
protected readonly sets: Map<string, Set<string>>;
|
|
15
|
+
/**
|
|
16
|
+
* Add members to a tag set, creating it on first use.
|
|
17
|
+
*/
|
|
18
|
+
add(tagKey: string, members: readonly string[]): void;
|
|
19
|
+
/**
|
|
20
|
+
* The members of a tag set (a copy; empty when the set does not exist).
|
|
21
|
+
*/
|
|
22
|
+
members(tagKey: string): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Remove members from a tag set; an emptied set is dropped.
|
|
25
|
+
*/
|
|
26
|
+
remove(tagKey: string, members: readonly string[]): void;
|
|
27
|
+
/**
|
|
28
|
+
* Drop a whole tag set.
|
|
29
|
+
*/
|
|
30
|
+
delete(tagKey: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Drop every tag set whose key equals `namespace` or sits under
|
|
33
|
+
* `namespace.` — the same rule the drivers use for entries. An empty
|
|
34
|
+
* namespace clears everything.
|
|
35
|
+
*/
|
|
36
|
+
removeNamespace(namespace: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Drop every tag set.
|
|
39
|
+
*/
|
|
40
|
+
clear(): void;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { InMemoryTagIndex };
|
|
44
|
+
//# sourceMappingURL=in-memory-tag-index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-tag-index.d.mts","names":[],"sources":["../../../../../../../cache/src/drivers/in-memory-tag-index.ts"],"mappings":";;AAWA;;;;;;;;;;cAAa,gBAAA;EAAA,mBACQ,IAAA,EAAM,GAAG,SAAS,GAAA;EAgC9B;;;EA3BA,GAAA,CAAI,MAAA,UAAgB,OAAA;EA8Cb;;;EA1BP,OAAA,CAAQ,MAAA;EAsDH;AAAA;;EA/CL,MAAA,CAAO,MAAA,UAAgB,OAAA;;;;EAmBvB,MAAA,CAAO,MAAA;;;;;;EASP,eAAA,CAAgB,SAAA;;;;EAmBhB,KAAA;AAAA"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//#region ../cache/src/drivers/in-memory-tag-index.ts
|
|
2
|
+
/**
|
|
3
|
+
* Tag index for the in-process drivers (memory, memoryExtended, lru, mock).
|
|
4
|
+
*
|
|
5
|
+
* Lives beside the driver's entry storage, never inside it: index sets are not
|
|
6
|
+
* entries, so they are never subject to `maxSize` / LRU eviction or TTL
|
|
7
|
+
* sweeps, and every operation is synchronous — concurrent tagged writes in one
|
|
8
|
+
* process can never drop a member.
|
|
9
|
+
*
|
|
10
|
+
* Keys are the driver-parsed tag keys (`<prefix>.cache.tags.<tag>`); members
|
|
11
|
+
* are the caller's normalized, un-prefixed cache keys.
|
|
12
|
+
*/
|
|
13
|
+
var InMemoryTagIndex = class {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.sets = /* @__PURE__ */ new Map();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Add members to a tag set, creating it on first use.
|
|
19
|
+
*/
|
|
20
|
+
add(tagKey, members) {
|
|
21
|
+
if (members.length === 0) return;
|
|
22
|
+
let set = this.sets.get(tagKey);
|
|
23
|
+
if (!set) {
|
|
24
|
+
set = /* @__PURE__ */ new Set();
|
|
25
|
+
this.sets.set(tagKey, set);
|
|
26
|
+
}
|
|
27
|
+
for (const member of members) set.add(member);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The members of a tag set (a copy; empty when the set does not exist).
|
|
31
|
+
*/
|
|
32
|
+
members(tagKey) {
|
|
33
|
+
return [...this.sets.get(tagKey) ?? []];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Remove members from a tag set; an emptied set is dropped.
|
|
37
|
+
*/
|
|
38
|
+
remove(tagKey, members) {
|
|
39
|
+
const set = this.sets.get(tagKey);
|
|
40
|
+
if (!set) return;
|
|
41
|
+
for (const member of members) set.delete(member);
|
|
42
|
+
if (set.size === 0) this.sets.delete(tagKey);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Drop a whole tag set.
|
|
46
|
+
*/
|
|
47
|
+
delete(tagKey) {
|
|
48
|
+
this.sets.delete(tagKey);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Drop every tag set whose key equals `namespace` or sits under
|
|
52
|
+
* `namespace.` — the same rule the drivers use for entries. An empty
|
|
53
|
+
* namespace clears everything.
|
|
54
|
+
*/
|
|
55
|
+
removeNamespace(namespace) {
|
|
56
|
+
if (namespace === "") {
|
|
57
|
+
this.sets.clear();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const prefix = namespace + ".";
|
|
61
|
+
for (const key of [...this.sets.keys()]) if (key === namespace || key.startsWith(prefix)) this.sets.delete(key);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Drop every tag set.
|
|
65
|
+
*/
|
|
66
|
+
clear() {
|
|
67
|
+
this.sets.clear();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
export { InMemoryTagIndex };
|
|
73
|
+
//# sourceMappingURL=in-memory-tag-index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-tag-index.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/in-memory-tag-index.ts"],"sourcesContent":["/**\n * Tag index for the in-process drivers (memory, memoryExtended, lru, mock).\n *\n * Lives beside the driver's entry storage, never inside it: index sets are not\n * entries, so they are never subject to `maxSize` / LRU eviction or TTL\n * sweeps, and every operation is synchronous — concurrent tagged writes in one\n * process can never drop a member.\n *\n * Keys are the driver-parsed tag keys (`<prefix>.cache.tags.<tag>`); members\n * are the caller's normalized, un-prefixed cache keys.\n */\nexport class InMemoryTagIndex {\n protected readonly sets: Map<string, Set<string>> = new Map();\n\n /**\n * Add members to a tag set, creating it on first use.\n */\n public add(tagKey: string, members: readonly string[]): void {\n if (members.length === 0) {\n return;\n }\n\n let set = this.sets.get(tagKey);\n\n if (!set) {\n set = new Set();\n this.sets.set(tagKey, set);\n }\n\n for (const member of members) {\n set.add(member);\n }\n }\n\n /**\n * The members of a tag set (a copy; empty when the set does not exist).\n */\n public members(tagKey: string): string[] {\n return [...(this.sets.get(tagKey) ?? [])];\n }\n\n /**\n * Remove members from a tag set; an emptied set is dropped.\n */\n public remove(tagKey: string, members: readonly string[]): void {\n const set = this.sets.get(tagKey);\n\n if (!set) {\n return;\n }\n\n for (const member of members) {\n set.delete(member);\n }\n\n if (set.size === 0) {\n this.sets.delete(tagKey);\n }\n }\n\n /**\n * Drop a whole tag set.\n */\n public delete(tagKey: string): void {\n this.sets.delete(tagKey);\n }\n\n /**\n * Drop every tag set whose key equals `namespace` or sits under\n * `namespace.` — the same rule the drivers use for entries. An empty\n * namespace clears everything.\n */\n public removeNamespace(namespace: string): void {\n if (namespace === \"\") {\n this.sets.clear();\n\n return;\n }\n\n const prefix = namespace + \".\";\n\n for (const key of [...this.sets.keys()]) {\n if (key === namespace || key.startsWith(prefix)) {\n this.sets.delete(key);\n }\n }\n }\n\n /**\n * Drop every tag set.\n */\n public clear(): void {\n this.sets.clear();\n }\n}\n"],"mappings":";;;;;;;;;;;;AAWA,IAAa,mBAAb,MAA8B;;8BACwB,IAAI,IAAI;;;;;CAK5D,AAAO,IAAI,QAAgB,SAAkC;EAC3D,IAAI,QAAQ,WAAW,GACrB;EAGF,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM;EAE9B,IAAI,CAAC,KAAK;GACR,sBAAM,IAAI,IAAI;GACd,KAAK,KAAK,IAAI,QAAQ,GAAG;EAC3B;EAEA,KAAK,MAAM,UAAU,SACnB,IAAI,IAAI,MAAM;CAElB;;;;CAKA,AAAO,QAAQ,QAA0B;EACvC,OAAO,CAAC,GAAI,KAAK,KAAK,IAAI,MAAM,KAAK,CAAC,CAAE;CAC1C;;;;CAKA,AAAO,OAAO,QAAgB,SAAkC;EAC9D,MAAM,MAAM,KAAK,KAAK,IAAI,MAAM;EAEhC,IAAI,CAAC,KACH;EAGF,KAAK,MAAM,UAAU,SACnB,IAAI,OAAO,MAAM;EAGnB,IAAI,IAAI,SAAS,GACf,KAAK,KAAK,OAAO,MAAM;CAE3B;;;;CAKA,AAAO,OAAO,QAAsB;EAClC,KAAK,KAAK,OAAO,MAAM;CACzB;;;;;;CAOA,AAAO,gBAAgB,WAAyB;EAC9C,IAAI,cAAc,IAAI;GACpB,KAAK,KAAK,MAAM;GAEhB;EACF;EAEA,MAAM,SAAS,YAAY;EAE3B,KAAK,MAAM,OAAO,CAAC,GAAG,KAAK,KAAK,KAAK,CAAC,GACpC,IAAI,QAAQ,aAAa,IAAI,WAAW,MAAM,GAC5C,KAAK,KAAK,OAAO,GAAG;CAG1B;;;;CAKA,AAAO,QAAc;EACnB,KAAK,KAAK,MAAM;CAClB;AACF"}
|
package/esm/drivers/index.d.mts
CHANGED
|
@@ -6,4 +6,4 @@ import { MemoryExtendedCacheDriver } from "./memory-extended-cache-driver.mjs";
|
|
|
6
6
|
import { MockCacheDriver } from "./mock-cache-driver.mjs";
|
|
7
7
|
import { NullCacheDriver } from "./null-cache-driver.mjs";
|
|
8
8
|
import { PgCacheDriver } from "./pg-cache-driver.mjs";
|
|
9
|
-
import { RedisCacheDriver } from "./redis-cache-driver.mjs";
|
|
9
|
+
import { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver } from "./redis-cache-driver.mjs";
|
package/esm/drivers/index.mjs
CHANGED
|
@@ -6,6 +6,6 @@ import { MemoryExtendedCacheDriver } from "./memory-extended-cache-driver.mjs";
|
|
|
6
6
|
import { MockCacheDriver } from "./mock-cache-driver.mjs";
|
|
7
7
|
import { NullCacheDriver } from "./null-cache-driver.mjs";
|
|
8
8
|
import { PgCacheDriver } from "./pg-cache-driver.mjs";
|
|
9
|
-
import { RedisCacheDriver } from "./redis-cache-driver.mjs";
|
|
9
|
+
import { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver } from "./redis-cache-driver.mjs";
|
|
10
10
|
|
|
11
11
|
export { };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseCacheDriver } from "./base-cache-driver.mjs";
|
|
2
|
+
import { InMemoryTagIndex } from "./in-memory-tag-index.mjs";
|
|
2
3
|
import { CacheData, CacheDriver, CacheKey, CacheSetOptions, CacheSimilarHit, CacheSimilarOptions, CacheTtl, LRUMemoryCacheOptions } from "../types.mjs";
|
|
3
4
|
|
|
4
5
|
//#region ../cache/src/drivers/lru-memory-cache-driver.d.ts
|
|
@@ -36,6 +37,10 @@ declare class LRUMemoryCacheDriver extends BaseCacheDriver<LRUMemoryCacheDriver,
|
|
|
36
37
|
* Cache map
|
|
37
38
|
*/
|
|
38
39
|
protected cache: Map<string, CacheNode>;
|
|
40
|
+
/**
|
|
41
|
+
* Tag index, kept out of the LRU list so capacity pressure can never evict it.
|
|
42
|
+
*/
|
|
43
|
+
protected tagIndex: InMemoryTagIndex;
|
|
39
44
|
/**
|
|
40
45
|
* Head of the cache
|
|
41
46
|
*/
|
|
@@ -96,6 +101,26 @@ declare class LRUMemoryCacheDriver extends BaseCacheDriver<LRUMemoryCacheDriver,
|
|
|
96
101
|
* path.
|
|
97
102
|
*/
|
|
98
103
|
protected getEntry(key: CacheKey): Promise<CacheData | null>;
|
|
104
|
+
/**
|
|
105
|
+
* Live node for `parsedKey`, or `undefined` when missing / expired (an
|
|
106
|
+
* expired node is unlinked). Synchronous.
|
|
107
|
+
*/
|
|
108
|
+
protected liveNode(parsedKey: string): CacheNode | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* {@inheritdoc}
|
|
111
|
+
*
|
|
112
|
+
* Read, add and write run synchronously (no `await` in between), so
|
|
113
|
+
* concurrent increments in this process never lose an update. An existing
|
|
114
|
+
* node keeps its expiry; a missing key starts from 0 with the default TTL.
|
|
115
|
+
*/
|
|
116
|
+
increment(key: CacheKey, value?: number): Promise<number>;
|
|
117
|
+
/**
|
|
118
|
+
* {@inheritdoc}
|
|
119
|
+
*
|
|
120
|
+
* Read and unlink run synchronously, so a value is handed out exactly once
|
|
121
|
+
* even when concurrent callers pull the same key.
|
|
122
|
+
*/
|
|
123
|
+
pull(key: CacheKey): Promise<any | null>;
|
|
99
124
|
/**
|
|
100
125
|
* {@inheritdoc}
|
|
101
126
|
*/
|
|
@@ -104,6 +129,24 @@ declare class LRUMemoryCacheDriver extends BaseCacheDriver<LRUMemoryCacheDriver,
|
|
|
104
129
|
* {@inheritdoc}
|
|
105
130
|
*/
|
|
106
131
|
remove(key: CacheKey): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* {@inheritdoc}
|
|
134
|
+
*
|
|
135
|
+
* Synchronous in-process set, never evicted or expired with the entries.
|
|
136
|
+
*/
|
|
137
|
+
tagAdd(tagKey: CacheKey, members: string[]): Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* {@inheritdoc}
|
|
140
|
+
*/
|
|
141
|
+
tagMembers(tagKey: CacheKey): Promise<string[]>;
|
|
142
|
+
/**
|
|
143
|
+
* {@inheritdoc}
|
|
144
|
+
*/
|
|
145
|
+
tagRemove(tagKey: CacheKey, members: string[]): Promise<void>;
|
|
146
|
+
/**
|
|
147
|
+
* {@inheritdoc}
|
|
148
|
+
*/
|
|
149
|
+
tagDelete(tagKey: CacheKey): Promise<void>;
|
|
107
150
|
/**
|
|
108
151
|
* {@inheritdoc}
|
|
109
152
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lru-memory-cache-driver.d.mts","names":[],"sources":["../../../../../../../cache/src/drivers/lru-memory-cache-driver.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"lru-memory-cache-driver.d.mts","names":[],"sources":["../../../../../../../cache/src/drivers/lru-memory-cache-driver.ts"],"mappings":";;;;;cAeM,SAAA;EAeK,GAAA;EACA,KAAA;EAfF,IAAA,EAAM,SAAA;EACN,IAAA,EAAM,SAAS;EACf,SAAA;EAYE;;;;EAPF,OAAA;EANM;;;;EAWN,MAAA;cAEE,GAAA,UACA,KAAA,OACP,GAAA;EAAA,IAOS,SAAA;AAAA;;;AAAS;AAWtB;;;cAAa,oBAAA,SACH,eAAA,CAAgB,oBAAA,EAAsB,qBAAA,aACnC,WAAA,CAAY,oBAAA,EAAsB,qBAAA;EADC;;;EAMvC,IAAA;EAKU;;;EAAA,UAAP,KAAA,EAAO,GAAA,SAAY,SAAA;EAoBD;;;EAAA,UAflB,QAAA,EAAU,gBAAA;EAyHQ;;;EAAA,UApHlB,IAAA,EAAM,SAAA;EAmNQ;;;EAAA,UA9Md,IAAA,EAAM,SAAA;EAyPuB;;;EAAA,UApP7B,eAAA,GAAkB,MAAA,CAAO,OAAA;EAgTD;;;;EAqFD;;;EAtX1B,IAAA;EAoZoC;;;EA5YpC,YAAA;EA0ZmC;;;;;;;;EAlX7B,eAAA,CAAgB,SAAA,WAAiB,OAAA;EA7FxB;;;EAqIT,GAAA,CACX,GAAA,EAAK,QAAA,EACL,KAAA,OACA,YAAA,GAAe,QAAA,GAAW,eAAA,GACzB,OAAA;EA1I2C;;;EAAA,UAwNpC,QAAA,CAAS,IAAA,EAAM,SAAA;EAlNlB;;;EAAA,UA0NG,UAAA,CAAW,IAAA,EAAM,SAAA;EAhNjB;;;EAAA,UAwNA,OAAA,CAAQ,IAAA,EAAM,SAAA;EA9Md;;;EAAA,UAwNA,UAAA;EAnNyB;;;;;;EAAA,UAiOnB,QAAA,CAAS,GAAA,EAAK,QAAA,GAAW,OAAA,CAAQ,SAAA;EA1HpC;;;;EAAA,UA6IH,QAAA,CAAS,SAAA,WAAoB,SAAA;EA1IX;;;;;;;EAkKf,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;EA3ErC;;;;;;EA+GR,IAAA,CAAK,GAAA,EAAK,QAAA,GAAW,OAAA;EA/ET;;;EA0GZ,GAAA,CAAI,GAAA,EAAK,QAAA,GAAQ,OAAA;EAvFX;;;EAiJN,MAAA,CAAO,GAAA,EAAK,QAAA,GAAQ,OAAA;EAzHV;;;;;EAgJV,MAAA,CAAO,MAAA,EAAQ,QAAA,EAAU,OAAA,aAAoB,OAAA;EA5GxB;;;EAmHrB,UAAA,CAAW,MAAA,EAAQ,QAAA,GAAW,OAAA;EAxFb;;;EA+FjB,SAAA,CAAU,MAAA,EAAQ,QAAA,EAAU,OAAA,aAAoB,OAAA;EArC5B;;;EA4CpB,SAAA,CAAU,MAAA,EAAQ,QAAA,GAAW,OAAA;EArBJ;;;;;;;EAgCzB,KAAA,IAAK,OAAA;EAlBK;;;;;;;;;EA2CV,OAAA,UACX,MAAA,YACA,OAAA,EAAS,mBAAA,GACR,OAAA,CAAQ,eAAA,CAAgB,CAAA;EAHN;;;EAAA,IA2CV,QAAA;EAxCR;;;EA+CU,UAAA,IAAU,OAAA;AAAA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cosineSimilarity } from "../utils.mjs";
|
|
2
2
|
import { BaseCacheDriver } from "./base-cache-driver.mjs";
|
|
3
|
+
import { InMemoryTagIndex } from "./in-memory-tag-index.mjs";
|
|
3
4
|
|
|
4
5
|
//#region ../cache/src/drivers/lru-memory-cache-driver.ts
|
|
5
6
|
var CacheNode = class {
|
|
@@ -28,6 +29,7 @@ var LRUMemoryCacheDriver = class extends BaseCacheDriver {
|
|
|
28
29
|
super();
|
|
29
30
|
this.name = "lru";
|
|
30
31
|
this.cache = /* @__PURE__ */ new Map();
|
|
32
|
+
this.tagIndex = new InMemoryTagIndex();
|
|
31
33
|
this.head = new CacheNode("", null);
|
|
32
34
|
this.tail = new CacheNode("", null);
|
|
33
35
|
this.init();
|
|
@@ -73,6 +75,7 @@ var LRUMemoryCacheDriver = class extends BaseCacheDriver {
|
|
|
73
75
|
const parsedNamespace = this.parseKey(namespace);
|
|
74
76
|
this.log("clearing", parsedNamespace || "(all)");
|
|
75
77
|
const removed = [];
|
|
78
|
+
this.tagIndex.removeNamespace(parsedNamespace);
|
|
76
79
|
if (parsedNamespace === "") for (const key of this.cache.keys()) removed.push(key);
|
|
77
80
|
else {
|
|
78
81
|
const prefix = parsedNamespace + ".";
|
|
@@ -187,6 +190,72 @@ var LRUMemoryCacheDriver = class extends BaseCacheDriver {
|
|
|
187
190
|
};
|
|
188
191
|
}
|
|
189
192
|
/**
|
|
193
|
+
* Live node for `parsedKey`, or `undefined` when missing / expired (an
|
|
194
|
+
* expired node is unlinked). Synchronous.
|
|
195
|
+
*/
|
|
196
|
+
liveNode(parsedKey) {
|
|
197
|
+
const node = this.cache.get(parsedKey);
|
|
198
|
+
if (!node) return;
|
|
199
|
+
if (node.isExpired) {
|
|
200
|
+
this.removeNode(node);
|
|
201
|
+
this.cache.delete(parsedKey);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
return node;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* {@inheritdoc}
|
|
208
|
+
*
|
|
209
|
+
* Read, add and write run synchronously (no `await` in between), so
|
|
210
|
+
* concurrent increments in this process never lose an update. An existing
|
|
211
|
+
* node keeps its expiry; a missing key starts from 0 with the default TTL.
|
|
212
|
+
*/
|
|
213
|
+
async increment(key, value = 1) {
|
|
214
|
+
const parsedKey = this.parseKey(key);
|
|
215
|
+
const node = this.liveNode(parsedKey);
|
|
216
|
+
const current = node ? node.value : 0;
|
|
217
|
+
if (typeof current !== "number") throw new Error(`Cannot increment non-numeric value for key: ${parsedKey}`);
|
|
218
|
+
const newValue = current + value;
|
|
219
|
+
if (node) {
|
|
220
|
+
node.value = newValue;
|
|
221
|
+
this.moveHead(node);
|
|
222
|
+
} else {
|
|
223
|
+
const newNode = new CacheNode(parsedKey, newValue, this.ttl);
|
|
224
|
+
this.cache.set(parsedKey, newNode);
|
|
225
|
+
this.addNode(newNode);
|
|
226
|
+
if (this.cache.size > this.capacity) this.removeTail();
|
|
227
|
+
}
|
|
228
|
+
await this.emit("set", {
|
|
229
|
+
key: parsedKey,
|
|
230
|
+
value: newValue,
|
|
231
|
+
ttl: this.ttl
|
|
232
|
+
});
|
|
233
|
+
return newValue;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* {@inheritdoc}
|
|
237
|
+
*
|
|
238
|
+
* Read and unlink run synchronously, so a value is handed out exactly once
|
|
239
|
+
* even when concurrent callers pull the same key.
|
|
240
|
+
*/
|
|
241
|
+
async pull(key) {
|
|
242
|
+
const parsedKey = this.parseKey(key);
|
|
243
|
+
const node = this.liveNode(parsedKey);
|
|
244
|
+
if (!node) {
|
|
245
|
+
await this.emit("miss", { key: parsedKey });
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
this.removeNode(node);
|
|
249
|
+
this.cache.delete(parsedKey);
|
|
250
|
+
const value = node.value !== null && typeof node.value === "object" ? structuredClone(node.value) : node.value;
|
|
251
|
+
await this.emit("hit", {
|
|
252
|
+
key: parsedKey,
|
|
253
|
+
value
|
|
254
|
+
});
|
|
255
|
+
await this.emit("removed", { key: parsedKey });
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
190
259
|
* {@inheritdoc}
|
|
191
260
|
*/
|
|
192
261
|
async get(key) {
|
|
@@ -247,6 +316,32 @@ var LRUMemoryCacheDriver = class extends BaseCacheDriver {
|
|
|
247
316
|
/**
|
|
248
317
|
* {@inheritdoc}
|
|
249
318
|
*
|
|
319
|
+
* Synchronous in-process set, never evicted or expired with the entries.
|
|
320
|
+
*/
|
|
321
|
+
async tagAdd(tagKey, members) {
|
|
322
|
+
this.tagIndex.add(this.parseKey(tagKey), members);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* {@inheritdoc}
|
|
326
|
+
*/
|
|
327
|
+
async tagMembers(tagKey) {
|
|
328
|
+
return this.tagIndex.members(this.parseKey(tagKey));
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* {@inheritdoc}
|
|
332
|
+
*/
|
|
333
|
+
async tagRemove(tagKey, members) {
|
|
334
|
+
this.tagIndex.remove(this.parseKey(tagKey), members);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* {@inheritdoc}
|
|
338
|
+
*/
|
|
339
|
+
async tagDelete(tagKey) {
|
|
340
|
+
this.tagIndex.delete(this.parseKey(tagKey));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* {@inheritdoc}
|
|
344
|
+
*
|
|
250
345
|
* When a `globalPrefix` is configured, `flush` scopes itself to that prefix
|
|
251
346
|
* so multi-tenant caches don't accidentally wipe sibling tenants. Without
|
|
252
347
|
* a prefix, clears everything.
|
|
@@ -256,6 +351,7 @@ var LRUMemoryCacheDriver = class extends BaseCacheDriver {
|
|
|
256
351
|
if (this.options.globalPrefix) await this.removeNamespace("");
|
|
257
352
|
else {
|
|
258
353
|
this.cache.clear();
|
|
354
|
+
this.tagIndex.clear();
|
|
259
355
|
this.init();
|
|
260
356
|
}
|
|
261
357
|
this.log("flushed");
|