@open-mercato/cache 0.3.2 → 0.4.2-canary-c02407ff85
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/.test-cache.json +4 -0
- package/build.mjs +61 -0
- package/dist/errors.js +12 -8
- package/dist/errors.js.map +7 -0
- package/dist/index.js +16 -7
- package/dist/index.js.map +7 -0
- package/dist/service.js +240 -287
- package/dist/service.js.map +7 -0
- package/dist/strategies/jsonfile.js +179 -194
- package/dist/strategies/jsonfile.js.map +7 -0
- package/dist/strategies/memory.js +143 -157
- package/dist/strategies/memory.js.map +7 -0
- package/dist/strategies/redis.js +292 -359
- package/dist/strategies/redis.js.map +7 -0
- package/dist/strategies/sqlite.js +164 -191
- package/dist/strategies/sqlite.js.map +7 -0
- package/dist/tenantContext.js +10 -6
- package/dist/tenantContext.js.map +7 -0
- package/dist/types.js +1 -1
- package/dist/types.js.map +7 -0
- package/jest.config.cjs +19 -0
- package/package.json +40 -12
- package/src/__tests__/memory.strategy.test.ts +15 -7
- package/tsconfig.build.json +9 -1
- package/tsconfig.json +4 -7
- package/watch.mjs +6 -0
- package/dist/errors.d.ts +0 -7
- package/dist/errors.d.ts.map +0 -1
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/service.d.ts +0 -40
- package/dist/service.d.ts.map +0 -1
- package/dist/strategies/jsonfile.d.ts +0 -10
- package/dist/strategies/jsonfile.d.ts.map +0 -1
- package/dist/strategies/memory.d.ts +0 -9
- package/dist/strategies/memory.d.ts.map +0 -1
- package/dist/strategies/redis.d.ts +0 -5
- package/dist/strategies/redis.d.ts.map +0 -1
- package/dist/strategies/sqlite.d.ts +0 -13
- package/dist/strategies/sqlite.d.ts.map +0 -1
- package/dist/tenantContext.d.ts +0 -4
- package/dist/tenantContext.d.ts.map +0 -1
- package/dist/types.d.ts +0 -86
- package/dist/types.d.ts.map +0 -1
- package/jest.config.js +0 -19
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/strategies/redis.ts"],
|
|
4
|
+
"sourcesContent": ["import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport { CacheDependencyUnavailableError } from '../errors'\n\ntype RedisPipeline = {\n set(key: string, value: string): RedisPipeline\n setex(key: string, ttlSeconds: number, value: string): RedisPipeline\n sadd(key: string, value: string): RedisPipeline\n srem(key: string, member: string): RedisPipeline\n del(key: string): RedisPipeline\n exec(): Promise<unknown>\n}\n\ntype RedisClient = {\n get(key: string): Promise<string | null>\n set(key: string, value: string): Promise<unknown>\n setex(key: string, ttlSeconds: number, value: string): Promise<unknown>\n del(key: string): Promise<unknown>\n exists(key: string): Promise<number>\n keys(pattern: string): Promise<string[]>\n smembers(key: string): Promise<string[]>\n pipeline(): RedisPipeline\n quit(): Promise<void>\n once?(event: 'end', listener: () => void): void\n}\n\ntype RedisConstructor = new (url?: string) => RedisClient\ntype PossibleRedisModule = unknown\n\ntype RequireFn = (id: string) => unknown\n\n/**\n * Redis cache strategy with tag support\n * Persistent across process restarts, can be shared across multiple instances\n * \n * Uses Redis data structures:\n * - Hash for storing cache entries: cache:{key} -> {value, tags, expiresAt, createdAt}\n * - Sets for tag index: tag:{tag} -> Set of keys\n */\nlet redisModulePromise: Promise<PossibleRedisModule> | null = null\ntype RedisRegistryEntry = { client?: RedisClient; creating?: Promise<RedisClient>; refs: number }\nconst redisRegistry = new Map<string, RedisRegistryEntry>()\n\nfunction resolveRequire(): RequireFn | null {\n const nonWebpack = (globalThis as { __non_webpack_require__?: unknown }).__non_webpack_require__\n if (typeof nonWebpack === 'function') return nonWebpack as RequireFn\n if (typeof require === 'function') return require as RequireFn\n if (typeof module !== 'undefined' && typeof module.require === 'function') {\n return module.require.bind(module)\n }\n try {\n const maybeRequire = Function('return typeof require !== \"undefined\" ? require : undefined')()\n if (typeof maybeRequire === 'function') return maybeRequire as RequireFn\n } catch {\n // ignore\n }\n return null\n}\n\nfunction loadRedisModuleViaRequire(): PossibleRedisModule | null {\n const resolver = resolveRequire()\n if (!resolver) return null\n try {\n return resolver('ioredis') as PossibleRedisModule\n } catch {\n return null\n }\n}\n\nfunction pickRedisConstructor(mod: PossibleRedisModule): RedisConstructor | null {\n const queue: unknown[] = [mod]\n const seen = new Set<unknown>()\n while (queue.length) {\n const current = queue.shift()\n if (!current || seen.has(current)) continue\n seen.add(current)\n if (typeof current === 'function') return current as RedisConstructor\n if (typeof current === 'object') {\n queue.push((current as { default?: unknown }).default)\n queue.push((current as { Redis?: unknown }).Redis)\n queue.push((current as { module?: { exports?: unknown } }).module?.exports)\n queue.push((current as { exports?: unknown }).exports)\n }\n }\n return null\n}\n\nasync function loadRedisModule(): Promise<PossibleRedisModule> {\n if (!redisModulePromise) {\n redisModulePromise = (async () => {\n const required = loadRedisModuleViaRequire() ?? (await import('ioredis'))\n return required as PossibleRedisModule\n })().catch((error) => {\n redisModulePromise = null\n throw new CacheDependencyUnavailableError('redis', 'ioredis', error)\n })\n }\n return redisModulePromise\n}\n\nfunction retainRedisEntry(key: string): RedisRegistryEntry {\n let entry = redisRegistry.get(key)\n if (!entry) {\n entry = { refs: 0 }\n redisRegistry.set(key, entry)\n }\n entry.refs += 1\n return entry\n}\n\nasync function acquireRedisClient(key: string, entry: RedisRegistryEntry): Promise<RedisClient> {\n if (entry.client) return entry.client\n if (entry.creating) return entry.creating\n entry.creating = loadRedisModule()\n .then((mod) => {\n const ctor = pickRedisConstructor(mod)\n if (!ctor) {\n throw new CacheDependencyUnavailableError('redis', 'ioredis', new Error('No usable Redis constructor'))\n }\n const client = new ctor(key)\n entry.client = client\n entry.creating = undefined\n client.once?.('end', () => {\n if (redisRegistry.get(key) === entry && entry.refs === 0) {\n redisRegistry.delete(key)\n } else if (redisRegistry.get(key) === entry) {\n entry.client = undefined\n }\n })\n return client\n })\n .catch((error) => {\n entry.creating = undefined\n throw error\n })\n return entry.creating\n}\n\nasync function releaseRedisEntry(key: string, entry: RedisRegistryEntry): Promise<void> {\n entry.refs = Math.max(0, entry.refs - 1)\n if (entry.refs > 0) return\n redisRegistry.delete(key)\n if (entry.client) {\n try {\n await entry.client.quit()\n } catch {\n // ignore shutdown errors\n } finally {\n entry.client = undefined\n }\n }\n}\n\nexport function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?: number }): CacheStrategy {\n const defaultTtl = options?.defaultTtl\n const keyPrefix = 'cache:'\n const tagPrefix = 'tag:'\n const connectionUrl =\n redisUrl || process.env.REDIS_URL || process.env.CACHE_REDIS_URL || 'redis://localhost:6379'\n const registryEntry = retainRedisEntry(connectionUrl)\n let redis: RedisClient | null = registryEntry.client ?? null\n\n async function getRedisClient(): Promise<RedisClient> {\n if (redis) return redis\n\n redis = await acquireRedisClient(connectionUrl, registryEntry)\n return redis\n }\n\n function getCacheKey(key: string): string {\n return `${keyPrefix}${key}`\n }\n\n function getTagKey(tag: string): string {\n return `${tagPrefix}${tag}`\n }\n\n function isExpired(entry: CacheEntry): boolean {\n if (entry.expiresAt === null) return false\n return Date.now() > entry.expiresAt\n }\n\n function matchPattern(key: string, pattern: string): boolean {\n const regexPattern = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*/g, '.*')\n .replace(/\\?/g, '.')\n const regex = new RegExp(`^${regexPattern}$`)\n return regex.test(key)\n }\n\n const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n const data = await client.get(cacheKey)\n\n if (!data) return null\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n\n if (isExpired(entry)) {\n if (options?.returnExpired) {\n return entry.value\n }\n // Clean up expired entry\n await deleteKey(key)\n return null\n }\n\n return entry.value\n } catch {\n // Invalid JSON, remove it\n await client.del(cacheKey)\n return null\n }\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n\n // Remove old entry from tag index if it exists\n const oldData = await client.get(cacheKey)\n if (oldData) {\n try {\n const oldEntry: CacheEntry = JSON.parse(oldData)\n // Remove from old tags\n const pipeline = client.pipeline()\n for (const tag of oldEntry.tags) {\n pipeline.srem(getTagKey(tag), key)\n }\n await pipeline.exec()\n } catch {\n // Ignore parse errors\n }\n }\n\n const ttl = options?.ttl ?? defaultTtl\n const tags = options?.tags || []\n const expiresAt = ttl ? Date.now() + ttl : null\n\n const entry: CacheEntry = {\n key,\n value,\n tags,\n expiresAt,\n createdAt: Date.now(),\n }\n\n const pipeline = client.pipeline()\n\n // Store the entry\n const serialized = JSON.stringify(entry)\n if (ttl) {\n pipeline.setex(cacheKey, Math.ceil(ttl / 1000), serialized)\n } else {\n pipeline.set(cacheKey, serialized)\n }\n\n // Add to tag index\n for (const tag of tags) {\n pipeline.sadd(getTagKey(tag), key)\n }\n\n await pipeline.exec()\n }\n\n const has = async (key: string): Promise<boolean> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n const exists = await client.exists(cacheKey)\n\n if (!exists) return false\n\n // Check if expired\n const data = await client.get(cacheKey)\n if (!data) return false\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n await deleteKey(key)\n return false\n }\n return true\n } catch {\n return false\n }\n }\n\n const deleteKey = async (key: string): Promise<boolean> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n\n // Get entry to remove from tag index\n const data = await client.get(cacheKey)\n if (!data) return false\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n const pipeline = client.pipeline()\n\n // Remove from tag index\n for (const tag of entry.tags) {\n pipeline.srem(getTagKey(tag), key)\n }\n\n // Delete the cache entry\n pipeline.del(cacheKey)\n\n await pipeline.exec()\n return true\n } catch {\n // Just delete the key if we can't parse it\n await client.del(cacheKey)\n return true\n }\n }\n\n const deleteByTags = async (tags: string[]): Promise<number> => {\n const client = await getRedisClient()\n const keysToDelete = new Set<string>()\n\n // Collect all keys that have any of the specified tags\n for (const tag of tags) {\n const tagKey = getTagKey(tag)\n const keys = await client.smembers(tagKey)\n for (const key of keys) {\n keysToDelete.add(key)\n }\n }\n\n // Delete all collected keys\n let deleted = 0\n for (const key of keysToDelete) {\n const success = await deleteKey(key)\n if (success) deleted++\n }\n\n return deleted\n }\n\n const clear = async (): Promise<number> => {\n const client = await getRedisClient()\n \n // Get all cache keys\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n const tagKeys = await client.keys(`${tagPrefix}*`)\n\n if (cacheKeys.length === 0 && tagKeys.length === 0) return 0\n\n const pipeline = client.pipeline()\n for (const key of [...cacheKeys, ...tagKeys]) {\n pipeline.del(key)\n }\n\n await pipeline.exec()\n return cacheKeys.length\n }\n\n const keys = async (pattern?: string): Promise<string[]> => {\n const client = await getRedisClient()\n const searchPattern = pattern \n ? `${keyPrefix}${pattern}` \n : `${keyPrefix}*`\n \n const cacheKeys = await client.keys(searchPattern)\n \n // Remove prefix from keys\n const result = cacheKeys.map((key: string) => key.substring(keyPrefix.length))\n \n if (!pattern) return result\n \n // Apply pattern matching (Redis KEYS command uses glob pattern, but we want our pattern)\n return result.filter((key: string) => matchPattern(key, pattern))\n }\n\n const stats = async (): Promise<{ size: number; expired: number }> => {\n const client = await getRedisClient()\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n \n let expired = 0\n for (const cacheKey of cacheKeys) {\n const data = await client.get(cacheKey)\n if (data) {\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n expired++\n }\n } catch {\n // Ignore parse errors\n }\n }\n }\n\n return { size: cacheKeys.length, expired }\n }\n\n const cleanup = async (): Promise<number> => {\n const client = await getRedisClient()\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n \n let removed = 0\n for (const cacheKey of cacheKeys) {\n const data = await client.get(cacheKey)\n if (data) {\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n const key = cacheKey.substring(keyPrefix.length)\n await deleteKey(key)\n removed++\n }\n } catch {\n // Remove invalid entries\n await client.del(cacheKey)\n removed++\n }\n }\n }\n\n return removed\n }\n\n const close = async (): Promise<void> => {\n await releaseRedisEntry(connectionUrl, registryEntry)\n redis = null\n }\n\n return {\n get,\n set,\n has,\n delete: deleteKey,\n deleteByTags,\n clear,\n keys,\n stats,\n cleanup,\n close,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["options", "pipeline", "keys"]
|
|
7
|
+
}
|
|
@@ -1,32 +1,21 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { CacheDependencyUnavailableError } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const imported = await import('better-sqlite3');
|
|
21
|
-
const Database = typeof imported === 'function' ? imported : imported.default;
|
|
22
|
-
// Ensure directory exists
|
|
23
|
-
const dir = path.dirname(filePath);
|
|
24
|
-
if (!fs.existsSync(dir)) {
|
|
25
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
-
}
|
|
27
|
-
db = new Database(filePath);
|
|
28
|
-
// Create tables
|
|
29
|
-
db.exec(`
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CacheDependencyUnavailableError } from "../errors.js";
|
|
4
|
+
function createSqliteStrategy(dbPath, options) {
|
|
5
|
+
let db = null;
|
|
6
|
+
const defaultTtl = options?.defaultTtl;
|
|
7
|
+
const filePath = dbPath || process.env.CACHE_SQLITE_PATH || ".cache.db";
|
|
8
|
+
async function getDb() {
|
|
9
|
+
if (db) return db;
|
|
10
|
+
try {
|
|
11
|
+
const imported = await import("better-sqlite3");
|
|
12
|
+
const Database = typeof imported === "function" ? imported : imported.default;
|
|
13
|
+
const dir = path.dirname(filePath);
|
|
14
|
+
if (!fs.existsSync(dir)) {
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
db = new Database(filePath);
|
|
18
|
+
db.exec(`
|
|
30
19
|
CREATE TABLE IF NOT EXISTS cache_entries (
|
|
31
20
|
key TEXT PRIMARY KEY,
|
|
32
21
|
value TEXT NOT NULL,
|
|
@@ -44,174 +33,158 @@ export function createSqliteStrategy(dbPath, options) {
|
|
|
44
33
|
CREATE INDEX IF NOT EXISTS idx_cache_tags_tag ON cache_tags(tag);
|
|
45
34
|
CREATE INDEX IF NOT EXISTS idx_cache_entries_expires_at ON cache_entries(expires_at);
|
|
46
35
|
`);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
throw new CacheDependencyUnavailableError('sqlite', 'better-sqlite3', error);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
function isExpired(expiresAt) {
|
|
54
|
-
if (expiresAt === null)
|
|
55
|
-
return false;
|
|
56
|
-
return Date.now() > expiresAt;
|
|
36
|
+
return db;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new CacheDependencyUnavailableError("sqlite", "better-sqlite3", error);
|
|
57
39
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Clean up expired entry
|
|
80
|
-
await deleteKey(key);
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
return value;
|
|
84
|
-
}
|
|
85
|
-
catch (_a) {
|
|
86
|
-
// Invalid JSON, remove it
|
|
87
|
-
await deleteKey(key);
|
|
88
|
-
return null;
|
|
40
|
+
}
|
|
41
|
+
function isExpired(expiresAt) {
|
|
42
|
+
if (expiresAt === null) return false;
|
|
43
|
+
return Date.now() > expiresAt;
|
|
44
|
+
}
|
|
45
|
+
function matchPattern(key, pattern) {
|
|
46
|
+
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
47
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
48
|
+
return regex.test(key);
|
|
49
|
+
}
|
|
50
|
+
const get = async (key, options2) => {
|
|
51
|
+
const database = await getDb();
|
|
52
|
+
const stmt = database.prepare("SELECT value, expires_at FROM cache_entries WHERE key = ?");
|
|
53
|
+
const row = stmt.get(key);
|
|
54
|
+
if (!row) return null;
|
|
55
|
+
try {
|
|
56
|
+
const value = JSON.parse(row.value);
|
|
57
|
+
const expiresAt = row.expires_at;
|
|
58
|
+
if (isExpired(expiresAt)) {
|
|
59
|
+
if (options2?.returnExpired) {
|
|
60
|
+
return value;
|
|
89
61
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
62
|
+
await deleteKey(key);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
} catch {
|
|
67
|
+
await deleteKey(key);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const set = async (key, value, options2) => {
|
|
72
|
+
const database = await getDb();
|
|
73
|
+
const ttl = options2?.ttl ?? defaultTtl;
|
|
74
|
+
const tags = options2?.tags || [];
|
|
75
|
+
const expiresAt = ttl ? Date.now() + ttl : null;
|
|
76
|
+
const createdAt = Date.now();
|
|
77
|
+
const serialized = JSON.stringify(value);
|
|
78
|
+
database.transaction(() => {
|
|
79
|
+
database.prepare("DELETE FROM cache_tags WHERE key = ?").run(key);
|
|
80
|
+
database.prepare(`
|
|
104
81
|
INSERT OR REPLACE INTO cache_entries (key, value, expires_at, created_at)
|
|
105
82
|
VALUES (?, ?, ?, ?)
|
|
106
83
|
`).run(key, serialized, expiresAt, createdAt);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
insertTag.run(key, tag);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
})();
|
|
115
|
-
};
|
|
116
|
-
const has = async (key) => {
|
|
117
|
-
const database = await getDb();
|
|
118
|
-
const stmt = database.prepare('SELECT expires_at FROM cache_entries WHERE key = ?');
|
|
119
|
-
const row = stmt.get(key);
|
|
120
|
-
if (!row)
|
|
121
|
-
return false;
|
|
122
|
-
if (isExpired(row.expires_at)) {
|
|
123
|
-
await deleteKey(key);
|
|
124
|
-
return false;
|
|
84
|
+
if (tags.length > 0) {
|
|
85
|
+
const insertTag = database.prepare("INSERT INTO cache_tags (key, tag) VALUES (?, ?)");
|
|
86
|
+
for (const tag of tags) {
|
|
87
|
+
insertTag.run(key, tag);
|
|
125
88
|
}
|
|
126
|
-
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
};
|
|
92
|
+
const has = async (key) => {
|
|
93
|
+
const database = await getDb();
|
|
94
|
+
const stmt = database.prepare("SELECT expires_at FROM cache_entries WHERE key = ?");
|
|
95
|
+
const row = stmt.get(key);
|
|
96
|
+
if (!row) return false;
|
|
97
|
+
if (isExpired(row.expires_at)) {
|
|
98
|
+
await deleteKey(key);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
};
|
|
103
|
+
const deleteKey = async (key) => {
|
|
104
|
+
const database = await getDb();
|
|
105
|
+
const result = database.transaction(() => {
|
|
106
|
+
database.prepare("DELETE FROM cache_tags WHERE key = ?").run(key);
|
|
107
|
+
const info = database.prepare("DELETE FROM cache_entries WHERE key = ?").run(key);
|
|
108
|
+
return info.changes > 0;
|
|
109
|
+
})();
|
|
110
|
+
return result;
|
|
111
|
+
};
|
|
112
|
+
const deleteByTags = async (tags) => {
|
|
113
|
+
const database = await getDb();
|
|
114
|
+
const placeholders = tags.map(() => "?").join(",");
|
|
115
|
+
const stmt = database.prepare(`
|
|
142
116
|
SELECT DISTINCT key FROM cache_tags WHERE tag IN (${placeholders})
|
|
143
117
|
`);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
keys,
|
|
213
|
-
stats,
|
|
214
|
-
cleanup,
|
|
215
|
-
close,
|
|
216
|
-
};
|
|
118
|
+
const rows = stmt.all(...tags);
|
|
119
|
+
let deleted = 0;
|
|
120
|
+
for (const row of rows) {
|
|
121
|
+
const success = await deleteKey(row.key);
|
|
122
|
+
if (success) deleted++;
|
|
123
|
+
}
|
|
124
|
+
return deleted;
|
|
125
|
+
};
|
|
126
|
+
const clear = async () => {
|
|
127
|
+
const database = await getDb();
|
|
128
|
+
const result = database.transaction(() => {
|
|
129
|
+
const countStmt = database.prepare("SELECT COUNT(*) as count FROM cache_entries");
|
|
130
|
+
const count = countStmt.get().count;
|
|
131
|
+
database.prepare("DELETE FROM cache_tags").run();
|
|
132
|
+
database.prepare("DELETE FROM cache_entries").run();
|
|
133
|
+
return count;
|
|
134
|
+
})();
|
|
135
|
+
return result;
|
|
136
|
+
};
|
|
137
|
+
const keys = async (pattern) => {
|
|
138
|
+
const database = await getDb();
|
|
139
|
+
const stmt = database.prepare("SELECT key FROM cache_entries");
|
|
140
|
+
const rows = stmt.all();
|
|
141
|
+
const allKeys = rows.map((row) => row.key);
|
|
142
|
+
if (!pattern) return allKeys;
|
|
143
|
+
return allKeys.filter((key) => matchPattern(key, pattern));
|
|
144
|
+
};
|
|
145
|
+
const stats = async () => {
|
|
146
|
+
const database = await getDb();
|
|
147
|
+
const sizeStmt = database.prepare("SELECT COUNT(*) as count FROM cache_entries");
|
|
148
|
+
const size = sizeStmt.get().count;
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const expiredStmt = database.prepare("SELECT COUNT(*) as count FROM cache_entries WHERE expires_at IS NOT NULL AND expires_at < ?");
|
|
151
|
+
const expired = expiredStmt.get(now).count;
|
|
152
|
+
return { size, expired };
|
|
153
|
+
};
|
|
154
|
+
const cleanup = async () => {
|
|
155
|
+
const database = await getDb();
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
const result = database.transaction(() => {
|
|
158
|
+
const stmt = database.prepare("SELECT key FROM cache_entries WHERE expires_at IS NOT NULL AND expires_at < ?");
|
|
159
|
+
const rows = stmt.all(now);
|
|
160
|
+
for (const row of rows) {
|
|
161
|
+
database.prepare("DELETE FROM cache_tags WHERE key = ?").run(row.key);
|
|
162
|
+
}
|
|
163
|
+
const info = database.prepare("DELETE FROM cache_entries WHERE expires_at IS NOT NULL AND expires_at < ?").run(now);
|
|
164
|
+
return info.changes;
|
|
165
|
+
})();
|
|
166
|
+
return result;
|
|
167
|
+
};
|
|
168
|
+
const close = async () => {
|
|
169
|
+
if (db) {
|
|
170
|
+
db.close();
|
|
171
|
+
db = null;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
get,
|
|
176
|
+
set,
|
|
177
|
+
has,
|
|
178
|
+
delete: deleteKey,
|
|
179
|
+
deleteByTags,
|
|
180
|
+
clear,
|
|
181
|
+
keys,
|
|
182
|
+
stats,
|
|
183
|
+
cleanup,
|
|
184
|
+
close
|
|
185
|
+
};
|
|
217
186
|
}
|
|
187
|
+
export {
|
|
188
|
+
createSqliteStrategy
|
|
189
|
+
};
|
|
190
|
+
//# sourceMappingURL=sqlite.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/strategies/sqlite.ts"],
|
|
4
|
+
"sourcesContent": ["import type { CacheStrategy, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { CacheDependencyUnavailableError } from '../errors'\n\ntype SqliteStatement<TResult = unknown> = {\n get(...args: unknown[]): TResult | undefined\n all(...args: unknown[]): TResult[]\n run(...args: unknown[]): { changes: number }\n}\n\ntype SqliteTransaction<TResult = unknown> = () => TResult\n\ntype SqliteDatabase = {\n prepare<TResult = unknown>(sql: string): SqliteStatement<TResult>\n exec(sql: string): unknown\n transaction<TResult>(fn: () => TResult): SqliteTransaction<TResult>\n close(): void\n}\n\ntype SqliteConstructor = new (file: string) => SqliteDatabase\ntype SqliteModule = SqliteConstructor | { default: SqliteConstructor }\n\n/**\n * SQLite cache strategy with tag support\n * Persistent across process restarts, stored in a SQLite database file\n * \n * Uses two tables:\n * - cache_entries: stores cache data\n * - cache_tags: stores tag associations (many-to-many)\n */\nexport function createSqliteStrategy(dbPath?: string, options?: { defaultTtl?: number }): CacheStrategy {\n let db: SqliteDatabase | null = null\n const defaultTtl = options?.defaultTtl\n const filePath = dbPath || process.env.CACHE_SQLITE_PATH || '.cache.db'\n\n async function getDb(): Promise<SqliteDatabase> {\n if (db) return db\n\n try {\n const imported = await import('better-sqlite3') as SqliteModule\n const Database = typeof imported === 'function' ? imported : imported.default\n \n // Ensure directory exists\n const dir = path.dirname(filePath)\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n\n db = new Database(filePath)\n\n // Create tables\n db.exec(`\n CREATE TABLE IF NOT EXISTS cache_entries (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n expires_at INTEGER,\n created_at INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS cache_tags (\n key TEXT NOT NULL,\n tag TEXT NOT NULL,\n PRIMARY KEY (key, tag),\n FOREIGN KEY (key) REFERENCES cache_entries(key) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_cache_tags_tag ON cache_tags(tag);\n CREATE INDEX IF NOT EXISTS idx_cache_entries_expires_at ON cache_entries(expires_at);\n `)\n\n return db\n } catch (error) {\n throw new CacheDependencyUnavailableError('sqlite', 'better-sqlite3', error)\n }\n }\n\n function isExpired(expiresAt: number | null): boolean {\n if (expiresAt === null) return false\n return Date.now() > expiresAt\n }\n\n function matchPattern(key: string, pattern: string): boolean {\n const regexPattern = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*/g, '.*')\n .replace(/\\?/g, '.')\n const regex = new RegExp(`^${regexPattern}$`)\n return regex.test(key)\n }\n\n type EntryRow = { value: string; expires_at: number | null }\n type ExpiresRow = { expires_at: number | null }\n type CountRow = { count: number }\n type KeyRow = { key: string }\n\n const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {\n const database = await getDb()\n \n const stmt = database.prepare('SELECT value, expires_at FROM cache_entries WHERE key = ?')\n const row = stmt.get(key) as EntryRow | undefined\n\n if (!row) return null\n\n try {\n const value = JSON.parse(row.value) as CacheValue\n const expiresAt = row.expires_at\n\n if (isExpired(expiresAt)) {\n if (options?.returnExpired) {\n return value\n }\n // Clean up expired entry\n await deleteKey(key)\n return null\n }\n\n return value\n } catch {\n // Invalid JSON, remove it\n await deleteKey(key)\n return null\n }\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> => {\n const database = await getDb()\n const ttl = options?.ttl ?? defaultTtl\n const tags = options?.tags || []\n const expiresAt = ttl ? Date.now() + ttl : null\n const createdAt = Date.now()\n\n const serialized = JSON.stringify(value)\n\n database.transaction(() => {\n // Delete old tags\n database.prepare('DELETE FROM cache_tags WHERE key = ?').run(key)\n\n // Insert or replace cache entry\n database.prepare(`\n INSERT OR REPLACE INTO cache_entries (key, value, expires_at, created_at)\n VALUES (?, ?, ?, ?)\n `).run(key, serialized, expiresAt, createdAt)\n\n // Insert new tags\n if (tags.length > 0) {\n const insertTag = database.prepare('INSERT INTO cache_tags (key, tag) VALUES (?, ?)')\n for (const tag of tags) {\n insertTag.run(key, tag)\n }\n }\n })()\n }\n\n const has = async (key: string): Promise<boolean> => {\n const database = await getDb()\n \n const stmt = database.prepare('SELECT expires_at FROM cache_entries WHERE key = ?')\n const row = stmt.get(key) as ExpiresRow | undefined\n\n if (!row) return false\n\n if (isExpired(row.expires_at)) {\n await deleteKey(key)\n return false\n }\n\n return true\n }\n\n const deleteKey = async (key: string): Promise<boolean> => {\n const database = await getDb()\n \n const result = database.transaction(() => {\n database.prepare('DELETE FROM cache_tags WHERE key = ?').run(key)\n const info = database.prepare('DELETE FROM cache_entries WHERE key = ?').run(key)\n return info.changes > 0\n })()\n\n return result\n }\n\n const deleteByTags = async (tags: string[]): Promise<number> => {\n const database = await getDb()\n \n // Get all unique keys that have any of the specified tags\n const placeholders = tags.map(() => '?').join(',')\n const stmt = database.prepare(`\n SELECT DISTINCT key FROM cache_tags WHERE tag IN (${placeholders})\n `)\n const rows = stmt.all(...tags) as KeyRow[]\n\n let deleted = 0\n for (const row of rows) {\n const success = await deleteKey(row.key)\n if (success) deleted++\n }\n\n return deleted\n }\n\n const clear = async (): Promise<number> => {\n const database = await getDb()\n \n const result = database.transaction(() => {\n const countStmt = database.prepare('SELECT COUNT(*) as count FROM cache_entries')\n const count = (countStmt.get() as CountRow).count\n\n database.prepare('DELETE FROM cache_tags').run()\n database.prepare('DELETE FROM cache_entries').run()\n\n return count\n })()\n\n return result\n }\n\n const keys = async (pattern?: string): Promise<string[]> => {\n const database = await getDb()\n \n const stmt = database.prepare('SELECT key FROM cache_entries')\n const rows = stmt.all() as KeyRow[]\n \n const allKeys = rows.map((row) => row.key)\n \n if (!pattern) return allKeys\n \n return allKeys.filter((key: string) => matchPattern(key, pattern))\n }\n\n const stats = async (): Promise<{ size: number; expired: number }> => {\n const database = await getDb()\n \n const sizeStmt = database.prepare('SELECT COUNT(*) as count FROM cache_entries')\n const size = (sizeStmt.get() as CountRow).count\n\n const now = Date.now()\n const expiredStmt = database.prepare('SELECT COUNT(*) as count FROM cache_entries WHERE expires_at IS NOT NULL AND expires_at < ?')\n const expired = (expiredStmt.get(now) as CountRow).count\n\n return { size, expired }\n }\n\n const cleanup = async (): Promise<number> => {\n const database = await getDb()\n const now = Date.now()\n \n const result = database.transaction(() => {\n // Get keys to delete\n const stmt = database.prepare('SELECT key FROM cache_entries WHERE expires_at IS NOT NULL AND expires_at < ?')\n const rows = stmt.all(now) as KeyRow[]\n\n // Delete tags for expired keys\n for (const row of rows) {\n database.prepare('DELETE FROM cache_tags WHERE key = ?').run(row.key)\n }\n\n // Delete expired entries\n const info = database.prepare('DELETE FROM cache_entries WHERE expires_at IS NOT NULL AND expires_at < ?').run(now)\n return info.changes\n })()\n\n return result\n }\n\n const close = async (): Promise<void> => {\n if (db) {\n db.close()\n db = null\n }\n }\n\n return {\n get,\n set,\n has,\n delete: deleteKey,\n deleteByTags,\n clear,\n keys,\n stats,\n cleanup,\n close,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["options"]
|
|
7
|
+
}
|
package/dist/tenantContext.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
const tenantStorage = new AsyncLocalStorage();
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
function runWithCacheTenant(tenantId, fn) {
|
|
4
|
+
return tenantStorage.run(tenantId ?? null, fn);
|
|
5
5
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return (_a = tenantStorage.getStore()) !== null && _a !== void 0 ? _a : null;
|
|
6
|
+
function getCurrentCacheTenant() {
|
|
7
|
+
return tenantStorage.getStore() ?? null;
|
|
9
8
|
}
|
|
9
|
+
export {
|
|
10
|
+
getCurrentCacheTenant,
|
|
11
|
+
runWithCacheTenant
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=tenantContext.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/tenantContext.ts"],
|
|
4
|
+
"sourcesContent": ["import { AsyncLocalStorage } from 'node:async_hooks'\n\nconst tenantStorage = new AsyncLocalStorage<string | null>()\n\nexport function runWithCacheTenant<T>(tenantId: string | null, fn: () => T): T\nexport function runWithCacheTenant<T>(tenantId: string | null, fn: () => Promise<T>): Promise<T>\nexport function runWithCacheTenant<T>(tenantId: string | null, fn: () => T | Promise<T>): T | Promise<T> {\n return tenantStorage.run(tenantId ?? null, fn)\n}\n\nexport function getCurrentCacheTenant(): string | null {\n return tenantStorage.getStore() ?? null\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,yBAAyB;AAElC,MAAM,gBAAgB,IAAI,kBAAiC;AAIpD,SAAS,mBAAsB,UAAyB,IAA0C;AACvG,SAAO,cAAc,IAAI,YAAY,MAAM,EAAE;AAC/C;AAEO,SAAS,wBAAuC;AACrD,SAAO,cAAc,SAAS,KAAK;AACrC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
package/jest.config.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
preset: 'ts-jest',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
rootDir: '.',
|
|
6
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
|
7
|
+
transform: {
|
|
8
|
+
'^.+\\.(t|j)sx?$': [
|
|
9
|
+
'ts-jest',
|
|
10
|
+
{
|
|
11
|
+
tsconfig: {
|
|
12
|
+
jsx: 'react-jsx',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
testMatch: ['<rootDir>/src/**/__tests__/**/*.test.(ts|tsx)'],
|
|
18
|
+
passWithNoTests: true,
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cache",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2-canary-c02407ff85",
|
|
4
4
|
"description": "Multi-strategy cache service with tag-based invalidation support",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
8
7
|
"scripts": {
|
|
9
|
-
"
|
|
8
|
+
"build": "node build.mjs",
|
|
9
|
+
"watch": "node watch.mjs",
|
|
10
|
+
"test": "jest --config jest.config.cjs",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./src/index.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./types": {
|
|
19
|
+
"types": "./src/types.ts",
|
|
20
|
+
"default": "./dist/types.js"
|
|
21
|
+
},
|
|
22
|
+
"./*": {
|
|
23
|
+
"types": [
|
|
24
|
+
"./src/*.ts"
|
|
25
|
+
],
|
|
26
|
+
"default": "./dist/*.js"
|
|
27
|
+
},
|
|
28
|
+
"./*/*": {
|
|
29
|
+
"types": [
|
|
30
|
+
"./src/*/*.ts"
|
|
31
|
+
],
|
|
32
|
+
"default": "./dist/*/*.js"
|
|
33
|
+
}
|
|
10
34
|
},
|
|
11
35
|
"keywords": [
|
|
12
36
|
"cache",
|
|
@@ -18,22 +42,26 @@
|
|
|
18
42
|
"invalidation"
|
|
19
43
|
],
|
|
20
44
|
"peerDependencies": {
|
|
21
|
-
"
|
|
22
|
-
"
|
|
45
|
+
"better-sqlite3": ">=9.0.0",
|
|
46
|
+
"ioredis": "^5.0.0"
|
|
23
47
|
},
|
|
24
48
|
"peerDependenciesMeta": {
|
|
25
|
-
"
|
|
49
|
+
"better-sqlite3": {
|
|
26
50
|
"optional": true
|
|
27
51
|
},
|
|
28
|
-
"
|
|
52
|
+
"ioredis": {
|
|
29
53
|
"optional": true
|
|
30
54
|
}
|
|
31
55
|
},
|
|
32
56
|
"devDependencies": {
|
|
33
57
|
"@types/better-sqlite3": "^7.6.0",
|
|
34
|
-
"@types/
|
|
58
|
+
"@types/jest": "^30.0.0",
|
|
59
|
+
"@types/node": "^20.0.0",
|
|
60
|
+
"jest": "^30.2.0",
|
|
61
|
+
"ts-jest": "^29.4.6"
|
|
35
62
|
},
|
|
36
63
|
"publishConfig": {
|
|
37
64
|
"access": "public"
|
|
38
|
-
}
|
|
39
|
-
|
|
65
|
+
},
|
|
66
|
+
"stableVersion": "0.4.1"
|
|
67
|
+
}
|