@open-mercato/cache 0.9.1-develop.7574.1.837288eec1 → 0.10.1-develop.7576.1.21d5b50435

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.
@@ -1,2 +1,2 @@
1
- [build:cache] found 12 entry points
1
+ [build:cache] found 11 entry points
2
2
  [build:cache] built successfully
package/AGENTS.md CHANGED
@@ -21,16 +21,6 @@ The memory strategy is bounded so a process-shared instance (`OM_BOOTSTRAP_CACHE
21
21
  - **Amortized expired-entry sweep** — expired entries are reclaimed by a budgeted sweep that runs every 256 writes (no per-instance timer, so the per-request default stays leak-free). Each pass scans a bounded slice from the LRU head, so it stays `O(budget)` rather than scanning the whole store. Expired entries beyond the budget are still reclaimed on access, by LRU eviction, or via an explicit `cleanup()`.
22
22
  - **Observability** — `stats()` on a memory-backed service surfaces `evictions`, `sweeps`, and `lastSweepReclaimed` (process-global counters) alongside the tenant-scoped `size`/`expired`, so operators can tell whether the bound is actively protecting the process. Other backends omit these fields.
23
23
 
24
- ## Value Compression
25
-
26
- The redis and sqlite strategies can compress serialized entries (`packages/cache/src/codec.ts`); memory and jsonfile never do.
27
-
28
- - **`CACHE_COMPRESSION`** — `off` (default) | `zstd` | `gzip`. zstd runs at level 1, gzip at level 6. A `zstd` setting falls back to `gzip` with one warning on a runtime without zstd (the `node:zlib` zstd API is marked experimental).
29
- - **`CACHE_COMPRESSION_MIN_BYTES`** — entries smaller than this (default `1024`) stay plain JSON; a compressed form is kept only when it is smaller.
30
- - **Reads detect the format by magic bytes**, whatever the current setting, so legacy JSON, zstd and gzip entries coexist and switching the setting never strands an entry. Decompression is capped at 64 MiB; a corrupt frame is dropped as a miss.
31
- - **When to enable:** Redis on another host, or memory-bound Redis — fewer bytes over the network and in memory. Redis on localhost or SQLite saves memory/disk only, at a small read cost.
32
- - **Rollout:** deploy with compression `off`, enable it only once every instance runs a version with the codec, and set it back to `off` before rolling back to an older version (older versions read compressed entries as misses and delete them).
33
-
34
24
  ## Always
35
25
 
36
26
  1. **MUST resolve via DI** — always use `container.resolve('cache')`, never instantiate cache directly. The DI token is `cache` (registered in `packages/core/src/bootstrap.ts`); there is no `cacheService` registration.
package/ENV.md CHANGED
@@ -75,32 +75,6 @@ CACHE_SQLITE_PATH=./.mercato/cache/cache.db
75
75
  CACHE_JSON_FILE_PATH=./.mercato/cache/cache.json
76
76
  ```
77
77
 
78
- ### `CACHE_COMPRESSION`
79
-
80
- **Description:** Compresses cache values stored by the `redis` and `sqlite` strategies. Reads always detect the stored format, so entries written under any setting stay readable. Enable it only once every instance runs a version that supports it, and switch it back to `off` before rolling back.
81
-
82
- **Values:** `off` | `zstd` | `gzip`
83
-
84
- **Default:** `off`
85
-
86
- **Example:**
87
- ```bash
88
- CACHE_COMPRESSION=zstd
89
- ```
90
-
91
- ### `CACHE_COMPRESSION_MIN_BYTES`
92
-
93
- **Description:** Smallest serialized entry, in bytes, that gets compressed. Smaller entries stay plain JSON.
94
-
95
- **Type:** Number (bytes)
96
-
97
- **Default:** `1024`
98
-
99
- **Example:**
100
- ```bash
101
- CACHE_COMPRESSION_MIN_BYTES=1024
102
- ```
103
-
104
78
  ## Example Configurations
105
79
 
106
80
  ### Development (In-Memory)
package/dist/service.js CHANGED
@@ -6,7 +6,6 @@ import { getCurrentCacheTenant } from "./tenantContext.js";
6
6
  import { createHash } from "node:crypto";
7
7
  import { CacheDependencyUnavailableError } from "./errors.js";
8
8
  import { matchCacheKeyPattern } from "./patterns.js";
9
- import { parseCacheCompression, parseCacheCompressionMinBytes } from "./codec.js";
10
9
  function normalizeTenantKey(raw) {
11
10
  const value = typeof raw === "string" ? raw.trim() : "";
12
11
  if (!value) return "global";
@@ -192,11 +191,7 @@ function createConfiguredTenantAwareStrategy(options) {
192
191
  const envMaxEntries = process.env.CACHE_MEMORY_MAX_ENTRIES;
193
192
  const parsedEnvMaxEntries = envMaxEntries ? Number.parseInt(envMaxEntries, 10) : void 0;
194
193
  const maxEntries = options?.maxEntries ?? (typeof parsedEnvMaxEntries === "number" && Number.isFinite(parsedEnvMaxEntries) ? parsedEnvMaxEntries : void 0);
195
- const codecOptions = {
196
- compression: options?.compression ?? parseCacheCompression(process.env.CACHE_COMPRESSION),
197
- compressionMinBytes: options?.compressionMinBytes ?? parseCacheCompressionMinBytes(process.env.CACHE_COMPRESSION_MIN_BYTES)
198
- };
199
- const baseStrategy = createStrategyForType(strategyType, options, defaultTtl, maxEntries, codecOptions);
194
+ const baseStrategy = createStrategyForType(strategyType, options, defaultTtl, maxEntries);
200
195
  const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl, maxEntries);
201
196
  const reportsBoundedCounters = strategyType === "memory";
202
197
  return createTenantAwareWrapper(resilientStrategy, reportsBoundedCounters);
@@ -253,12 +248,12 @@ class CacheService {
253
248
  }
254
249
  }
255
250
  }
256
- function createStrategyForType(strategyType, options, defaultTtl, maxEntries, codecOptions) {
251
+ function createStrategyForType(strategyType, options, defaultTtl, maxEntries) {
257
252
  switch (strategyType) {
258
253
  case "redis":
259
- return createRedisStrategy(options?.redisUrl, { defaultTtl, ...codecOptions });
254
+ return createRedisStrategy(options?.redisUrl, { defaultTtl });
260
255
  case "sqlite":
261
- return createSqliteStrategy(options?.sqlitePath, { defaultTtl, ...codecOptions });
256
+ return createSqliteStrategy(options?.sqlitePath, { defaultTtl });
262
257
  case "jsonfile":
263
258
  return createJsonFileStrategy(options?.jsonFilePath, { defaultTtl });
264
259
  case "memory":
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/service.ts"],
4
- "sourcesContent": ["import type { CacheStrategy, CacheServiceOptions, CacheGetOptions, CacheSetOptions, CacheValue } from './types'\nimport { createMemoryStrategy } from './strategies/memory'\nimport { createRedisStrategy } from './strategies/redis'\nimport { createSqliteStrategy } from './strategies/sqlite'\nimport { createJsonFileStrategy } from './strategies/jsonfile'\nimport { getCurrentCacheTenant } from './tenantContext'\nimport { createHash } from 'node:crypto'\nimport { CacheDependencyUnavailableError } from './errors'\nimport { matchCacheKeyPattern } from './patterns'\nimport { parseCacheCompression, parseCacheCompressionMinBytes } from './codec'\nimport type { CacheCodecOptions } from './codec'\n\nfunction normalizeTenantKey(raw: string | null | undefined): string {\n const value = typeof raw === 'string' ? raw.trim() : ''\n if (!value) return 'global'\n return value.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\ntype TenantPrefixes = {\n keyPrefix: string\n tagPrefix: string\n scopeTag: string\n}\n\ntype CacheMetadata = {\n key: string\n expiresAt: number | null\n}\n\nfunction isCacheMetadata(value: CacheValue | null): value is CacheMetadata {\n if (typeof value !== 'object' || value === null) {\n return false\n }\n const record = value as Record<string, unknown>\n const hasValidKey = typeof record.key === 'string'\n const hasValidExpiresAt =\n !('expiresAt' in record)\n || record.expiresAt === null\n || typeof record.expiresAt === 'number'\n\n return hasValidKey && hasValidExpiresAt\n}\n\ntype CacheStrategyName = NonNullable<CacheServiceOptions['strategy']>\nconst KNOWN_STRATEGIES: CacheStrategyName[] = ['memory', 'redis', 'sqlite', 'jsonfile']\nconst TENANT_META_KEY_PATTERN = 'tenant:*:key:meta:*'\nconst TENANT_META_KEY_MARKER = ':key:meta:'\n\ntype TenantAwareCacheStrategy = CacheStrategy & {\n deleteByPatternsAcrossScopes(patterns: string[]): Promise<number>\n}\n\nfunction isCacheStrategyName(value: string | undefined): value is CacheStrategyName {\n if (!value) return false\n return KNOWN_STRATEGIES.includes(value as CacheStrategyName)\n}\n\nfunction resolveConfiguredStrategyType(options?: CacheServiceOptions): CacheStrategyName {\n const envStrategy = isCacheStrategyName(process.env.CACHE_STRATEGY)\n ? process.env.CACHE_STRATEGY\n : undefined\n return options?.strategy ?? envStrategy ?? 'memory'\n}\n\nfunction resolveTenantPrefixes(): TenantPrefixes {\n const tenant = normalizeTenantKey(getCurrentCacheTenant())\n const base = `tenant:${tenant}:`\n return {\n keyPrefix: `${base}key:`,\n tagPrefix: `${base}tag:`,\n scopeTag: `${base}tag:__scope__`,\n }\n}\n\nfunction hashIdentifier(input: string): string {\n return createHash('sha256').update(input).digest('hex')\n}\n\nfunction storageKey(originalKey: string, prefixes: TenantPrefixes): string {\n return `${prefixes.keyPrefix}k:${hashIdentifier(originalKey)}`\n}\n\nfunction metaKey(originalKey: string, prefixes: TenantPrefixes): string {\n return `${prefixes.keyPrefix}meta:${hashIdentifier(originalKey)}`\n}\n\nfunction hashedTag(tag: string, prefixes: TenantPrefixes): string {\n return `${prefixes.tagPrefix}t:${hashIdentifier(tag)}`\n}\n\nfunction buildTagSet(tags: string[] | undefined, prefixes: TenantPrefixes, includeScope: boolean): string[] {\n const scoped = new Set<string>()\n if (includeScope) scoped.add(prefixes.scopeTag)\n if (Array.isArray(tags)) {\n for (const tag of tags) {\n if (typeof tag === 'string' && tag.length > 0) scoped.add(hashedTag(tag, prefixes))\n }\n }\n return Array.from(scoped)\n}\n\nfunction createTenantAwareWrapper(base: CacheStrategy, reportsBoundedCounters: boolean): TenantAwareCacheStrategy {\n function normalizeDeletionCount(raw: number): number {\n if (!raw) return raw\n if (!Number.isFinite(raw)) return raw\n return Math.ceil(raw / 2)\n }\n\n const get = async (key: string, options?: CacheGetOptions) => {\n const prefixes = resolveTenantPrefixes()\n return base.get(storageKey(key, prefixes), options)\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions) => {\n const prefixes = resolveTenantPrefixes()\n const hashedTags = buildTagSet(options?.tags, prefixes, true)\n const ttl = options?.ttl ?? undefined\n const nextOptions: CacheSetOptions | undefined = options\n ? { ...options, tags: hashedTags }\n : { tags: hashedTags }\n await base.set(storageKey(key, prefixes), value, nextOptions)\n const metaPayload: CacheMetadata = { key, expiresAt: ttl ? Date.now() + ttl : null }\n await base.set(metaKey(key, prefixes), metaPayload, {\n ttl,\n tags: hashedTags,\n })\n }\n\n const has = async (key: string) => {\n const prefixes = resolveTenantPrefixes()\n return base.has(storageKey(key, prefixes))\n }\n\n const del = async (key: string) => {\n const prefixes = resolveTenantPrefixes()\n const primary = await base.delete(storageKey(key, prefixes))\n await base.delete(metaKey(key, prefixes))\n return primary\n }\n\n const deleteByTags = async (tags: string[]) => {\n const prefixes = resolveTenantPrefixes()\n const scopedTags = buildTagSet(tags, prefixes, false)\n if (!scopedTags.length) return 0\n const removed = await base.deleteByTags(scopedTags)\n return normalizeDeletionCount(removed)\n }\n\n const clear = async () => {\n const prefixes = resolveTenantPrefixes()\n const removed = await base.deleteByTags([prefixes.scopeTag])\n return normalizeDeletionCount(removed)\n }\n\n const keys = async (pattern?: string) => {\n const prefixes = resolveTenantPrefixes()\n const metaPattern = `${prefixes.keyPrefix}meta:*`\n const metaKeys = await base.keys(metaPattern)\n const originals: string[] = []\n for (const metaKey of metaKeys) {\n const metaValue = await base.get(metaKey, { returnExpired: true })\n if (!metaValue) continue\n const metadata = typeof metaValue === 'string' ? null : (isCacheMetadata(metaValue) ? metaValue : null)\n const original = typeof metaValue === 'string' ? metaValue : metadata?.key\n if (!original) continue\n if (pattern && !matchCacheKeyPattern(original, pattern)) continue\n originals.push(original)\n }\n return originals\n }\n\n const deleteByPatternsAcrossScopes = async (patterns: string[]) => {\n const normalizedPatterns = Array.from(new Set(patterns.map((pattern) => pattern.trim()).filter(Boolean)))\n if (normalizedPatterns.length === 0) return 0\n\n const metaKeys = await base.keys(TENANT_META_KEY_PATTERN)\n let deleted = 0\n for (const metadataKey of metaKeys) {\n const metadataValue = await base.get(metadataKey, { returnExpired: true })\n if (!metadataValue) continue\n const metadata = typeof metadataValue === 'string'\n ? null\n : (isCacheMetadata(metadataValue) ? metadataValue : null)\n const originalKey = typeof metadataValue === 'string' ? metadataValue : metadata?.key\n if (!originalKey || !normalizedPatterns.some((pattern) => matchCacheKeyPattern(originalKey, pattern))) {\n continue\n }\n\n const markerIndex = metadataKey.lastIndexOf(TENANT_META_KEY_MARKER)\n if (markerIndex < 0) continue\n const primaryKey = `${metadataKey.slice(0, markerIndex)}:key:k:${metadataKey.slice(markerIndex + TENANT_META_KEY_MARKER.length)}`\n if (await base.delete(primaryKey)) deleted += 1\n await base.delete(metadataKey)\n }\n return deleted\n }\n\n const stats = async () => {\n const prefixes = resolveTenantPrefixes()\n const metaKeys = await base.keys(`${prefixes.keyPrefix}meta:*`)\n let size = 0\n let expired = 0\n const now = Date.now()\n for (const metaKey of metaKeys) {\n const metaValue = await base.get(metaKey, { returnExpired: true })\n if (!metaValue) continue\n const metadata = typeof metaValue === 'string' ? null : (isCacheMetadata(metaValue) ? metaValue : null)\n const original = typeof metaValue === 'string' ? metaValue : metadata?.key\n if (!original) continue\n size++\n const expiresAt = metadata?.expiresAt ?? null\n if (expiresAt !== null && expiresAt <= now) expired++\n }\n // size/expired stay tenant-scoped (derived from this tenant's meta keys).\n // Only memory-backed strategies track the process-global bound counters, so\n // skip the extra base.stats() round-trip for redis/sqlite/jsonfile and omit\n // the optional fields (the stats() contract marks them optional). Keyed off\n // the configured strategy: a backend degraded to the memory fallback\n // intentionally does not surface these diagnostics.\n if (!reportsBoundedCounters) {\n return { size, expired }\n }\n const { evictions, sweeps, lastSweepReclaimed } = await base.stats()\n return { size, expired, evictions, sweeps, lastSweepReclaimed }\n }\n\n const cleanup = base.cleanup\n ? async () => normalizeDeletionCount(await base.cleanup!())\n : undefined\n\n const close = base.close\n ? async () => base.close!()\n : undefined\n const healthcheck = base.healthcheck\n ? async () => base.healthcheck!()\n : undefined\n\n return {\n get,\n set,\n has,\n delete: del,\n deleteByTags,\n clear,\n keys,\n stats,\n healthcheck,\n cleanup,\n close,\n deleteByPatternsAcrossScopes,\n }\n}\n\nfunction createConfiguredTenantAwareStrategy(options?: CacheServiceOptions): TenantAwareCacheStrategy {\n const strategyType = resolveConfiguredStrategyType(options)\n\n const envTtl = process.env.CACHE_TTL\n const parsedEnvTtl = envTtl ? Number.parseInt(envTtl, 10) : undefined\n const defaultTtl = options?.defaultTtl ?? (typeof parsedEnvTtl === 'number' && Number.isFinite(parsedEnvTtl) ? parsedEnvTtl : undefined)\n\n const envMaxEntries = process.env.CACHE_MEMORY_MAX_ENTRIES\n const parsedEnvMaxEntries = envMaxEntries ? Number.parseInt(envMaxEntries, 10) : undefined\n const maxEntries = options?.maxEntries ?? (typeof parsedEnvMaxEntries === 'number' && Number.isFinite(parsedEnvMaxEntries) ? parsedEnvMaxEntries : undefined)\n\n const codecOptions: CacheCodecOptions = {\n compression: options?.compression ?? parseCacheCompression(process.env.CACHE_COMPRESSION),\n compressionMinBytes: options?.compressionMinBytes ?? parseCacheCompressionMinBytes(process.env.CACHE_COMPRESSION_MIN_BYTES),\n }\n\n const baseStrategy = createStrategyForType(strategyType, options, defaultTtl, maxEntries, codecOptions)\n const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl, maxEntries)\n const reportsBoundedCounters = strategyType === 'memory'\n\n return createTenantAwareWrapper(resilientStrategy, reportsBoundedCounters)\n}\n\n/**\n * Cache service that provides a unified interface to different cache strategies\n *\n * Configuration via environment variables:\n * - CACHE_STRATEGY: 'memory' | 'redis' | 'sqlite' | 'jsonfile' (default: 'memory')\n * - CACHE_TTL: Default TTL in milliseconds (optional)\n * - CACHE_REDIS_URL: Redis connection URL (for redis strategy)\n * - CACHE_SQLITE_PATH: SQLite database file path (for sqlite strategy)\n * - CACHE_JSON_FILE_PATH: JSON file path (for jsonfile strategy)\n * - CACHE_COMPRESSION: 'off' | 'zstd' | 'gzip' value compression for redis/sqlite (default: 'off')\n * - CACHE_COMPRESSION_MIN_BYTES: smallest serialized entry that gets compressed (default: 1024)\n *\n * @example\n * const cache = createCacheService({ strategy: 'memory', defaultTtl: 60000 })\n * await cache.set('user:123', { name: 'John' }, { tags: ['users', 'user:123'] })\n * const user = await cache.get('user:123')\n * await cache.deleteByTags(['users']) // Invalidate all user-related cache\n */\nexport function createCacheService(options?: CacheServiceOptions): CacheStrategy {\n return createConfiguredTenantAwareStrategy(options)\n}\n\n/**\n * Delete matching logical keys across every tenant scope in the configured\n * stock cache backend without bootstrapping application DI or querying the\n * tenant database. Intended for maintenance tasks that already operate on all\n * scopes, such as post-generation structural invalidation.\n */\nexport async function purgeConfiguredCachePatternsAcrossTenantScopes(\n patterns: string[],\n options?: CacheServiceOptions,\n): Promise<number> {\n // A newly-created memory strategy cannot reach the running app's process-local\n // cache, so constructing one here would only allocate and immediately discard\n // an empty store. Persistent stock backends remain cross-process maintainable.\n if (resolveConfiguredStrategyType(options) === 'memory') return 0\n\n const cache = createConfiguredTenantAwareStrategy(options)\n try {\n return await cache.deleteByPatternsAcrossScopes(patterns)\n } finally {\n await cache.close?.()\n }\n}\n\n/**\n * CacheService class wrapper for DI integration\n * Provides the same interface as the functional API but as a class\n */\nexport class CacheService implements CacheStrategy {\n private strategy: CacheStrategy\n\n constructor(options?: CacheServiceOptions) {\n this.strategy = createCacheService(options)\n }\n\n async get(key: string, options?: CacheGetOptions): Promise<CacheValue | null> {\n return this.strategy.get(key, options)\n }\n\n async set(key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> {\n return this.strategy.set(key, value, options)\n }\n\n async has(key: string): Promise<boolean> {\n return this.strategy.has(key)\n }\n\n async delete(key: string): Promise<boolean> {\n return this.strategy.delete(key)\n }\n\n async deleteByTags(tags: string[]): Promise<number> {\n return this.strategy.deleteByTags(tags)\n }\n\n async clear(): Promise<number> {\n return this.strategy.clear()\n }\n\n async keys(pattern?: string): Promise<string[]> {\n return this.strategy.keys(pattern)\n }\n\n async stats(): Promise<{\n size: number\n expired: number\n evictions?: number\n sweeps?: number\n lastSweepReclaimed?: number\n }> {\n return this.strategy.stats()\n }\n\n async cleanup(): Promise<number> {\n if (this.strategy.cleanup) {\n return this.strategy.cleanup()\n }\n return 0\n }\n\n async close(): Promise<void> {\n if (this.strategy.close) {\n return this.strategy.close()\n }\n }\n}\n\nfunction createStrategyForType(strategyType: CacheStrategyName, options?: CacheServiceOptions, defaultTtl?: number, maxEntries?: number, codecOptions?: CacheCodecOptions): CacheStrategy {\n switch (strategyType) {\n case 'redis':\n return createRedisStrategy(options?.redisUrl, { defaultTtl, ...codecOptions })\n case 'sqlite':\n return createSqliteStrategy(options?.sqlitePath, { defaultTtl, ...codecOptions })\n case 'jsonfile':\n return createJsonFileStrategy(options?.jsonFilePath, { defaultTtl })\n case 'memory':\n default:\n return createMemoryStrategy({ defaultTtl, maxEntries })\n }\n}\n\nfunction describeDependencyFailure(error: CacheDependencyUnavailableError): string {\n const originalError = error.originalError\n if (!(originalError instanceof Error)) {\n return `missing dependency: ${error.dependency}`\n }\n\n const message = originalError.message.trim()\n if (message.length === 0) {\n return `missing dependency: ${error.dependency}`\n }\n\n if (message.includes('compiled against a different Node.js version') || message.includes('NODE_MODULE_VERSION')) {\n return `${error.dependency} native module needs rebuild for the current Node.js version`\n }\n\n if (message.includes('Cannot find module')) {\n return `missing dependency: ${error.dependency}`\n }\n\n return `${error.dependency} failed to load`\n}\n\nfunction withDependencyFallback(strategy: CacheStrategy, strategyType: CacheStrategyName, defaultTtl?: number, maxEntries?: number): CacheStrategy {\n if (strategyType === 'memory') return strategy\n\n let activeStrategy = strategy\n let fallbackStrategy: CacheStrategy | null = null\n let warned = false\n\n const ensureFallback = (error: CacheDependencyUnavailableError) => {\n if (!fallbackStrategy) {\n fallbackStrategy = createMemoryStrategy({ defaultTtl, maxEntries })\n }\n if (!warned) {\n const dependencyMessage = error.dependency\n ? ` (${describeDependencyFailure(error)})`\n : ''\n console.warn(`[cache] ${error.strategy} strategy unavailable${dependencyMessage}. Falling back to memory strategy.`)\n warned = true\n }\n activeStrategy = fallbackStrategy\n }\n\n const wrapMethod = <K extends keyof CacheStrategy>(method: K): CacheStrategy[K] => {\n const handler = async (...args: Parameters<NonNullable<CacheStrategy[K]>>) => {\n const fn = activeStrategy[method] as ((...methodArgs: Parameters<NonNullable<CacheStrategy[K]>>) => ReturnType<NonNullable<CacheStrategy[K]>>) | undefined\n if (!fn) {\n return undefined as Awaited<ReturnType<NonNullable<CacheStrategy[K]>>>\n }\n\n try {\n return await fn(...args)\n } catch (error) {\n if (error instanceof CacheDependencyUnavailableError) {\n ensureFallback(error)\n const fallbackFn = activeStrategy[method] as ((...methodArgs: Parameters<NonNullable<CacheStrategy[K]>>) => ReturnType<NonNullable<CacheStrategy[K]>>) | undefined\n if (!fallbackFn) {\n return undefined as Awaited<ReturnType<NonNullable<CacheStrategy[K]>>>\n }\n return fallbackFn(...args)\n }\n throw error\n }\n }\n\n return handler as CacheStrategy[K]\n }\n\n return {\n get: wrapMethod('get'),\n set: wrapMethod('set'),\n has: wrapMethod('has'),\n delete: wrapMethod('delete'),\n deleteByTags: wrapMethod('deleteByTags'),\n clear: wrapMethod('clear'),\n keys: wrapMethod('keys'),\n stats: wrapMethod('stats'),\n healthcheck: typeof strategy.healthcheck === 'function'\n ? async () => strategy.healthcheck!()\n : undefined,\n cleanup: typeof strategy.cleanup === 'function' ? wrapMethod('cleanup') : undefined,\n close: typeof strategy.close === 'function' ? wrapMethod('close') : undefined,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { CacheStrategy, CacheServiceOptions, CacheGetOptions, CacheSetOptions, CacheValue } from './types'\nimport { createMemoryStrategy } from './strategies/memory'\nimport { createRedisStrategy } from './strategies/redis'\nimport { createSqliteStrategy } from './strategies/sqlite'\nimport { createJsonFileStrategy } from './strategies/jsonfile'\nimport { getCurrentCacheTenant } from './tenantContext'\nimport { createHash } from 'node:crypto'\nimport { CacheDependencyUnavailableError } from './errors'\nimport { matchCacheKeyPattern } from './patterns'\n\nfunction normalizeTenantKey(raw: string | null | undefined): string {\n const value = typeof raw === 'string' ? raw.trim() : ''\n if (!value) return 'global'\n return value.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\ntype TenantPrefixes = {\n keyPrefix: string\n tagPrefix: string\n scopeTag: string\n}\n\ntype CacheMetadata = {\n key: string\n expiresAt: number | null\n}\n\nfunction isCacheMetadata(value: CacheValue | null): value is CacheMetadata {\n if (typeof value !== 'object' || value === null) {\n return false\n }\n const record = value as Record<string, unknown>\n const hasValidKey = typeof record.key === 'string'\n const hasValidExpiresAt =\n !('expiresAt' in record)\n || record.expiresAt === null\n || typeof record.expiresAt === 'number'\n\n return hasValidKey && hasValidExpiresAt\n}\n\ntype CacheStrategyName = NonNullable<CacheServiceOptions['strategy']>\nconst KNOWN_STRATEGIES: CacheStrategyName[] = ['memory', 'redis', 'sqlite', 'jsonfile']\nconst TENANT_META_KEY_PATTERN = 'tenant:*:key:meta:*'\nconst TENANT_META_KEY_MARKER = ':key:meta:'\n\ntype TenantAwareCacheStrategy = CacheStrategy & {\n deleteByPatternsAcrossScopes(patterns: string[]): Promise<number>\n}\n\nfunction isCacheStrategyName(value: string | undefined): value is CacheStrategyName {\n if (!value) return false\n return KNOWN_STRATEGIES.includes(value as CacheStrategyName)\n}\n\nfunction resolveConfiguredStrategyType(options?: CacheServiceOptions): CacheStrategyName {\n const envStrategy = isCacheStrategyName(process.env.CACHE_STRATEGY)\n ? process.env.CACHE_STRATEGY\n : undefined\n return options?.strategy ?? envStrategy ?? 'memory'\n}\n\nfunction resolveTenantPrefixes(): TenantPrefixes {\n const tenant = normalizeTenantKey(getCurrentCacheTenant())\n const base = `tenant:${tenant}:`\n return {\n keyPrefix: `${base}key:`,\n tagPrefix: `${base}tag:`,\n scopeTag: `${base}tag:__scope__`,\n }\n}\n\nfunction hashIdentifier(input: string): string {\n return createHash('sha256').update(input).digest('hex')\n}\n\nfunction storageKey(originalKey: string, prefixes: TenantPrefixes): string {\n return `${prefixes.keyPrefix}k:${hashIdentifier(originalKey)}`\n}\n\nfunction metaKey(originalKey: string, prefixes: TenantPrefixes): string {\n return `${prefixes.keyPrefix}meta:${hashIdentifier(originalKey)}`\n}\n\nfunction hashedTag(tag: string, prefixes: TenantPrefixes): string {\n return `${prefixes.tagPrefix}t:${hashIdentifier(tag)}`\n}\n\nfunction buildTagSet(tags: string[] | undefined, prefixes: TenantPrefixes, includeScope: boolean): string[] {\n const scoped = new Set<string>()\n if (includeScope) scoped.add(prefixes.scopeTag)\n if (Array.isArray(tags)) {\n for (const tag of tags) {\n if (typeof tag === 'string' && tag.length > 0) scoped.add(hashedTag(tag, prefixes))\n }\n }\n return Array.from(scoped)\n}\n\nfunction createTenantAwareWrapper(base: CacheStrategy, reportsBoundedCounters: boolean): TenantAwareCacheStrategy {\n function normalizeDeletionCount(raw: number): number {\n if (!raw) return raw\n if (!Number.isFinite(raw)) return raw\n return Math.ceil(raw / 2)\n }\n\n const get = async (key: string, options?: CacheGetOptions) => {\n const prefixes = resolveTenantPrefixes()\n return base.get(storageKey(key, prefixes), options)\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions) => {\n const prefixes = resolveTenantPrefixes()\n const hashedTags = buildTagSet(options?.tags, prefixes, true)\n const ttl = options?.ttl ?? undefined\n const nextOptions: CacheSetOptions | undefined = options\n ? { ...options, tags: hashedTags }\n : { tags: hashedTags }\n await base.set(storageKey(key, prefixes), value, nextOptions)\n const metaPayload: CacheMetadata = { key, expiresAt: ttl ? Date.now() + ttl : null }\n await base.set(metaKey(key, prefixes), metaPayload, {\n ttl,\n tags: hashedTags,\n })\n }\n\n const has = async (key: string) => {\n const prefixes = resolveTenantPrefixes()\n return base.has(storageKey(key, prefixes))\n }\n\n const del = async (key: string) => {\n const prefixes = resolveTenantPrefixes()\n const primary = await base.delete(storageKey(key, prefixes))\n await base.delete(metaKey(key, prefixes))\n return primary\n }\n\n const deleteByTags = async (tags: string[]) => {\n const prefixes = resolveTenantPrefixes()\n const scopedTags = buildTagSet(tags, prefixes, false)\n if (!scopedTags.length) return 0\n const removed = await base.deleteByTags(scopedTags)\n return normalizeDeletionCount(removed)\n }\n\n const clear = async () => {\n const prefixes = resolveTenantPrefixes()\n const removed = await base.deleteByTags([prefixes.scopeTag])\n return normalizeDeletionCount(removed)\n }\n\n const keys = async (pattern?: string) => {\n const prefixes = resolveTenantPrefixes()\n const metaPattern = `${prefixes.keyPrefix}meta:*`\n const metaKeys = await base.keys(metaPattern)\n const originals: string[] = []\n for (const metaKey of metaKeys) {\n const metaValue = await base.get(metaKey, { returnExpired: true })\n if (!metaValue) continue\n const metadata = typeof metaValue === 'string' ? null : (isCacheMetadata(metaValue) ? metaValue : null)\n const original = typeof metaValue === 'string' ? metaValue : metadata?.key\n if (!original) continue\n if (pattern && !matchCacheKeyPattern(original, pattern)) continue\n originals.push(original)\n }\n return originals\n }\n\n const deleteByPatternsAcrossScopes = async (patterns: string[]) => {\n const normalizedPatterns = Array.from(new Set(patterns.map((pattern) => pattern.trim()).filter(Boolean)))\n if (normalizedPatterns.length === 0) return 0\n\n const metaKeys = await base.keys(TENANT_META_KEY_PATTERN)\n let deleted = 0\n for (const metadataKey of metaKeys) {\n const metadataValue = await base.get(metadataKey, { returnExpired: true })\n if (!metadataValue) continue\n const metadata = typeof metadataValue === 'string'\n ? null\n : (isCacheMetadata(metadataValue) ? metadataValue : null)\n const originalKey = typeof metadataValue === 'string' ? metadataValue : metadata?.key\n if (!originalKey || !normalizedPatterns.some((pattern) => matchCacheKeyPattern(originalKey, pattern))) {\n continue\n }\n\n const markerIndex = metadataKey.lastIndexOf(TENANT_META_KEY_MARKER)\n if (markerIndex < 0) continue\n const primaryKey = `${metadataKey.slice(0, markerIndex)}:key:k:${metadataKey.slice(markerIndex + TENANT_META_KEY_MARKER.length)}`\n if (await base.delete(primaryKey)) deleted += 1\n await base.delete(metadataKey)\n }\n return deleted\n }\n\n const stats = async () => {\n const prefixes = resolveTenantPrefixes()\n const metaKeys = await base.keys(`${prefixes.keyPrefix}meta:*`)\n let size = 0\n let expired = 0\n const now = Date.now()\n for (const metaKey of metaKeys) {\n const metaValue = await base.get(metaKey, { returnExpired: true })\n if (!metaValue) continue\n const metadata = typeof metaValue === 'string' ? null : (isCacheMetadata(metaValue) ? metaValue : null)\n const original = typeof metaValue === 'string' ? metaValue : metadata?.key\n if (!original) continue\n size++\n const expiresAt = metadata?.expiresAt ?? null\n if (expiresAt !== null && expiresAt <= now) expired++\n }\n // size/expired stay tenant-scoped (derived from this tenant's meta keys).\n // Only memory-backed strategies track the process-global bound counters, so\n // skip the extra base.stats() round-trip for redis/sqlite/jsonfile and omit\n // the optional fields (the stats() contract marks them optional). Keyed off\n // the configured strategy: a backend degraded to the memory fallback\n // intentionally does not surface these diagnostics.\n if (!reportsBoundedCounters) {\n return { size, expired }\n }\n const { evictions, sweeps, lastSweepReclaimed } = await base.stats()\n return { size, expired, evictions, sweeps, lastSweepReclaimed }\n }\n\n const cleanup = base.cleanup\n ? async () => normalizeDeletionCount(await base.cleanup!())\n : undefined\n\n const close = base.close\n ? async () => base.close!()\n : undefined\n const healthcheck = base.healthcheck\n ? async () => base.healthcheck!()\n : undefined\n\n return {\n get,\n set,\n has,\n delete: del,\n deleteByTags,\n clear,\n keys,\n stats,\n healthcheck,\n cleanup,\n close,\n deleteByPatternsAcrossScopes,\n }\n}\n\nfunction createConfiguredTenantAwareStrategy(options?: CacheServiceOptions): TenantAwareCacheStrategy {\n const strategyType = resolveConfiguredStrategyType(options)\n\n const envTtl = process.env.CACHE_TTL\n const parsedEnvTtl = envTtl ? Number.parseInt(envTtl, 10) : undefined\n const defaultTtl = options?.defaultTtl ?? (typeof parsedEnvTtl === 'number' && Number.isFinite(parsedEnvTtl) ? parsedEnvTtl : undefined)\n\n const envMaxEntries = process.env.CACHE_MEMORY_MAX_ENTRIES\n const parsedEnvMaxEntries = envMaxEntries ? Number.parseInt(envMaxEntries, 10) : undefined\n const maxEntries = options?.maxEntries ?? (typeof parsedEnvMaxEntries === 'number' && Number.isFinite(parsedEnvMaxEntries) ? parsedEnvMaxEntries : undefined)\n\n const baseStrategy = createStrategyForType(strategyType, options, defaultTtl, maxEntries)\n const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl, maxEntries)\n const reportsBoundedCounters = strategyType === 'memory'\n\n return createTenantAwareWrapper(resilientStrategy, reportsBoundedCounters)\n}\n\n/**\n * Cache service that provides a unified interface to different cache strategies\n *\n * Configuration via environment variables:\n * - CACHE_STRATEGY: 'memory' | 'redis' | 'sqlite' | 'jsonfile' (default: 'memory')\n * - CACHE_TTL: Default TTL in milliseconds (optional)\n * - CACHE_REDIS_URL: Redis connection URL (for redis strategy)\n * - CACHE_SQLITE_PATH: SQLite database file path (for sqlite strategy)\n * - CACHE_JSON_FILE_PATH: JSON file path (for jsonfile strategy)\n *\n * @example\n * const cache = createCacheService({ strategy: 'memory', defaultTtl: 60000 })\n * await cache.set('user:123', { name: 'John' }, { tags: ['users', 'user:123'] })\n * const user = await cache.get('user:123')\n * await cache.deleteByTags(['users']) // Invalidate all user-related cache\n */\nexport function createCacheService(options?: CacheServiceOptions): CacheStrategy {\n return createConfiguredTenantAwareStrategy(options)\n}\n\n/**\n * Delete matching logical keys across every tenant scope in the configured\n * stock cache backend without bootstrapping application DI or querying the\n * tenant database. Intended for maintenance tasks that already operate on all\n * scopes, such as post-generation structural invalidation.\n */\nexport async function purgeConfiguredCachePatternsAcrossTenantScopes(\n patterns: string[],\n options?: CacheServiceOptions,\n): Promise<number> {\n // A newly-created memory strategy cannot reach the running app's process-local\n // cache, so constructing one here would only allocate and immediately discard\n // an empty store. Persistent stock backends remain cross-process maintainable.\n if (resolveConfiguredStrategyType(options) === 'memory') return 0\n\n const cache = createConfiguredTenantAwareStrategy(options)\n try {\n return await cache.deleteByPatternsAcrossScopes(patterns)\n } finally {\n await cache.close?.()\n }\n}\n\n/**\n * CacheService class wrapper for DI integration\n * Provides the same interface as the functional API but as a class\n */\nexport class CacheService implements CacheStrategy {\n private strategy: CacheStrategy\n\n constructor(options?: CacheServiceOptions) {\n this.strategy = createCacheService(options)\n }\n\n async get(key: string, options?: CacheGetOptions): Promise<CacheValue | null> {\n return this.strategy.get(key, options)\n }\n\n async set(key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> {\n return this.strategy.set(key, value, options)\n }\n\n async has(key: string): Promise<boolean> {\n return this.strategy.has(key)\n }\n\n async delete(key: string): Promise<boolean> {\n return this.strategy.delete(key)\n }\n\n async deleteByTags(tags: string[]): Promise<number> {\n return this.strategy.deleteByTags(tags)\n }\n\n async clear(): Promise<number> {\n return this.strategy.clear()\n }\n\n async keys(pattern?: string): Promise<string[]> {\n return this.strategy.keys(pattern)\n }\n\n async stats(): Promise<{\n size: number\n expired: number\n evictions?: number\n sweeps?: number\n lastSweepReclaimed?: number\n }> {\n return this.strategy.stats()\n }\n\n async cleanup(): Promise<number> {\n if (this.strategy.cleanup) {\n return this.strategy.cleanup()\n }\n return 0\n }\n\n async close(): Promise<void> {\n if (this.strategy.close) {\n return this.strategy.close()\n }\n }\n}\n\nfunction createStrategyForType(strategyType: CacheStrategyName, options?: CacheServiceOptions, defaultTtl?: number, maxEntries?: number): CacheStrategy {\n switch (strategyType) {\n case 'redis':\n return createRedisStrategy(options?.redisUrl, { defaultTtl })\n case 'sqlite':\n return createSqliteStrategy(options?.sqlitePath, { defaultTtl })\n case 'jsonfile':\n return createJsonFileStrategy(options?.jsonFilePath, { defaultTtl })\n case 'memory':\n default:\n return createMemoryStrategy({ defaultTtl, maxEntries })\n }\n}\n\nfunction describeDependencyFailure(error: CacheDependencyUnavailableError): string {\n const originalError = error.originalError\n if (!(originalError instanceof Error)) {\n return `missing dependency: ${error.dependency}`\n }\n\n const message = originalError.message.trim()\n if (message.length === 0) {\n return `missing dependency: ${error.dependency}`\n }\n\n if (message.includes('compiled against a different Node.js version') || message.includes('NODE_MODULE_VERSION')) {\n return `${error.dependency} native module needs rebuild for the current Node.js version`\n }\n\n if (message.includes('Cannot find module')) {\n return `missing dependency: ${error.dependency}`\n }\n\n return `${error.dependency} failed to load`\n}\n\nfunction withDependencyFallback(strategy: CacheStrategy, strategyType: CacheStrategyName, defaultTtl?: number, maxEntries?: number): CacheStrategy {\n if (strategyType === 'memory') return strategy\n\n let activeStrategy = strategy\n let fallbackStrategy: CacheStrategy | null = null\n let warned = false\n\n const ensureFallback = (error: CacheDependencyUnavailableError) => {\n if (!fallbackStrategy) {\n fallbackStrategy = createMemoryStrategy({ defaultTtl, maxEntries })\n }\n if (!warned) {\n const dependencyMessage = error.dependency\n ? ` (${describeDependencyFailure(error)})`\n : ''\n console.warn(`[cache] ${error.strategy} strategy unavailable${dependencyMessage}. Falling back to memory strategy.`)\n warned = true\n }\n activeStrategy = fallbackStrategy\n }\n\n const wrapMethod = <K extends keyof CacheStrategy>(method: K): CacheStrategy[K] => {\n const handler = async (...args: Parameters<NonNullable<CacheStrategy[K]>>) => {\n const fn = activeStrategy[method] as ((...methodArgs: Parameters<NonNullable<CacheStrategy[K]>>) => ReturnType<NonNullable<CacheStrategy[K]>>) | undefined\n if (!fn) {\n return undefined as Awaited<ReturnType<NonNullable<CacheStrategy[K]>>>\n }\n\n try {\n return await fn(...args)\n } catch (error) {\n if (error instanceof CacheDependencyUnavailableError) {\n ensureFallback(error)\n const fallbackFn = activeStrategy[method] as ((...methodArgs: Parameters<NonNullable<CacheStrategy[K]>>) => ReturnType<NonNullable<CacheStrategy[K]>>) | undefined\n if (!fallbackFn) {\n return undefined as Awaited<ReturnType<NonNullable<CacheStrategy[K]>>>\n }\n return fallbackFn(...args)\n }\n throw error\n }\n }\n\n return handler as CacheStrategy[K]\n }\n\n return {\n get: wrapMethod('get'),\n set: wrapMethod('set'),\n has: wrapMethod('has'),\n delete: wrapMethod('delete'),\n deleteByTags: wrapMethod('deleteByTags'),\n clear: wrapMethod('clear'),\n keys: wrapMethod('keys'),\n stats: wrapMethod('stats'),\n healthcheck: typeof strategy.healthcheck === 'function'\n ? async () => strategy.healthcheck!()\n : undefined,\n cleanup: typeof strategy.cleanup === 'function' ? wrapMethod('cleanup') : undefined,\n close: typeof strategy.close === 'function' ? wrapMethod('close') : undefined,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["metaKey"]
7
7
  }
@@ -1,7 +1,6 @@
1
1
  import { CacheDependencyUnavailableError } from "../errors.js";
2
2
  import { getRedisUrlOrThrow, REDIS_WIRE_PROTOCOL } from "@open-mercato/shared/lib/redis/connection";
3
3
  import { matchCacheKeyPattern } from "../patterns.js";
4
- import { decodeCacheText, encodeCacheText, resolveCacheCodecConfig } from "../codec.js";
5
4
  const REAP_ORPHANED_TAG_MEMBERS = `
6
5
  local removed = 0
7
6
  for i = 2, #KEYS do
@@ -115,7 +114,6 @@ async function releaseRedisEntry(key, entry) {
115
114
  }
116
115
  function createRedisStrategy(redisUrl, options) {
117
116
  const defaultTtl = options?.defaultTtl;
118
- const codec = resolveCacheCodecConfig(options);
119
117
  const keyPrefix = "cache:";
120
118
  const tagPrefix = "tag:";
121
119
  const connectionUrl = redisUrl || getRedisUrlOrThrow("CACHE");
@@ -139,10 +137,10 @@ function createRedisStrategy(redisUrl, options) {
139
137
  const get = async (key, options2) => {
140
138
  const client = await getRedisClient();
141
139
  const cacheKey = getCacheKey(key);
142
- const data = await client.getBuffer(cacheKey);
140
+ const data = await client.get(cacheKey);
143
141
  if (!data) return null;
144
142
  try {
145
- const entry = JSON.parse(decodeCacheText(data));
143
+ const entry = JSON.parse(data);
146
144
  if (isExpired(entry)) {
147
145
  if (options2?.returnExpired) {
148
146
  return entry.value;
@@ -159,10 +157,10 @@ function createRedisStrategy(redisUrl, options) {
159
157
  const set = async (key, value, options2) => {
160
158
  const client = await getRedisClient();
161
159
  const cacheKey = getCacheKey(key);
162
- const oldData = await client.getBuffer(cacheKey);
160
+ const oldData = await client.get(cacheKey);
163
161
  if (oldData) {
164
162
  try {
165
- const oldEntry = JSON.parse(decodeCacheText(oldData));
163
+ const oldEntry = JSON.parse(oldData);
166
164
  const pipeline2 = client.pipeline();
167
165
  for (const tag of oldEntry.tags) {
168
166
  pipeline2.srem(getTagKey(tag), key);
@@ -182,7 +180,7 @@ function createRedisStrategy(redisUrl, options) {
182
180
  createdAt: Date.now()
183
181
  };
184
182
  const pipeline = client.pipeline();
185
- const serialized = encodeCacheText(JSON.stringify(entry), codec);
183
+ const serialized = JSON.stringify(entry);
186
184
  if (ttl) {
187
185
  pipeline.setex(cacheKey, Math.ceil(ttl / 1e3), serialized);
188
186
  } else {
@@ -198,10 +196,10 @@ function createRedisStrategy(redisUrl, options) {
198
196
  const cacheKey = getCacheKey(key);
199
197
  const exists = await client.exists(cacheKey);
200
198
  if (!exists) return false;
201
- const data = await client.getBuffer(cacheKey);
199
+ const data = await client.get(cacheKey);
202
200
  if (!data) return false;
203
201
  try {
204
- const entry = JSON.parse(decodeCacheText(data));
202
+ const entry = JSON.parse(data);
205
203
  if (isExpired(entry)) {
206
204
  await deleteKey(key);
207
205
  return false;
@@ -214,10 +212,10 @@ function createRedisStrategy(redisUrl, options) {
214
212
  const deleteKey = async (key) => {
215
213
  const client = await getRedisClient();
216
214
  const cacheKey = getCacheKey(key);
217
- const data = await client.getBuffer(cacheKey);
215
+ const data = await client.get(cacheKey);
218
216
  if (!data) return false;
219
217
  try {
220
- const entry = JSON.parse(decodeCacheText(data));
218
+ const entry = JSON.parse(data);
221
219
  const pipeline = client.pipeline();
222
220
  for (const tag of entry.tags) {
223
221
  pipeline.srem(getTagKey(tag), key);
@@ -290,10 +288,10 @@ function createRedisStrategy(redisUrl, options) {
290
288
  const cacheKeys = await client.keys(`${keyPrefix}*`);
291
289
  let expired = 0;
292
290
  for (const cacheKey of cacheKeys) {
293
- const data = await client.getBuffer(cacheKey);
291
+ const data = await client.get(cacheKey);
294
292
  if (data) {
295
293
  try {
296
- const entry = JSON.parse(decodeCacheText(data));
294
+ const entry = JSON.parse(data);
297
295
  if (isExpired(entry)) {
298
296
  expired++;
299
297
  }
@@ -308,10 +306,10 @@ function createRedisStrategy(redisUrl, options) {
308
306
  const cacheKeys = await client.keys(`${keyPrefix}*`);
309
307
  let removed = 0;
310
308
  for (const cacheKey of cacheKeys) {
311
- const data = await client.getBuffer(cacheKey);
309
+ const data = await client.get(cacheKey);
312
310
  if (data) {
313
311
  try {
314
- const entry = JSON.parse(decodeCacheText(data));
312
+ const entry = JSON.parse(data);
315
313
  if (isExpired(entry)) {
316
314
  const key = cacheKey.substring(keyPrefix.length);
317
315
  await deleteKey(key);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/strategies/redis.ts"],
4
- "sourcesContent": ["import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport { CacheDependencyUnavailableError } from '../errors'\nimport { getRedisUrlOrThrow, REDIS_WIRE_PROTOCOL } from '@open-mercato/shared/lib/redis/connection'\nimport type { RedisProtocolVersion } from '@open-mercato/shared/lib/redis/connection'\nimport { matchCacheKeyPattern } from '../patterns'\nimport { decodeCacheText, encodeCacheText, resolveCacheCodecConfig } from '../codec'\nimport type { CacheCodecOptions } from '../codec'\n\ntype RedisPipeline = {\n set(key: string, value: string | Buffer): RedisPipeline\n setex(key: string, ttlSeconds: number, value: string | Buffer): RedisPipeline\n sadd(key: string, value: string): RedisPipeline\n srem(key: string, member: string): RedisPipeline\n del(key: string): RedisPipeline\n eval(script: string, numKeys: number, ...args: (string | number)[]): RedisPipeline\n exec(): Promise<unknown>\n}\n\ntype RedisClient = {\n ping(): Promise<string>\n get(key: string): Promise<string | null>\n getBuffer(key: string): Promise<Buffer | null>\n set(key: string, value: string | Buffer): Promise<unknown>\n setex(key: string, ttlSeconds: number, value: string | Buffer): 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, options?: { protocol?: RedisProtocolVersion }) => RedisClient\ntype PossibleRedisModule = unknown\n\ntype RequireFn = (id: string) => unknown\n\n/**\n * Reaps orphaned members from ONE tag set atomically.\n *\n * `KEYS[1]` is the tag key and `KEYS[2..]` are the candidate value keys, with\n * `ARGV[1..]` the member names aligned to them. Every key the script touches is\n * declared in `KEYS` so it stays valid against a slot-aware deployment.\n *\n * The `EXISTS` check and the `SREM` run in one atomic step, which is the whole\n * point: a member may only be dropped while its value key is genuinely absent.\n * A concurrent `set()` either lands first (`EXISTS` is 1, so the member is left\n * alone) or lands after (its own `sadd` re-adds the member). Either ordering\n * leaves a live key indexed.\n *\n * Deliberately single-tag. Pairing every orphan with every requested tag would\n * make the work quadratic in the tag count \u2014 and because a script blocks the\n * server for its whole run, that lands as a stall rather than as throughput.\n * Callers reap per tag using the membership each `smembers` actually returned.\n */\nconst REAP_ORPHANED_TAG_MEMBERS = `\nlocal removed = 0\nfor i = 2, #KEYS do\n if redis.call('EXISTS', KEYS[i]) == 0 then\n removed = removed + redis.call('SREM', KEYS[1], ARGV[i - 1])\n end\nend\nreturn removed\n`\n\n/** Bounds a single script's blocking time on the server, and the request size. */\nconst REAP_CHUNK_SIZE = 256\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, { protocol: REDIS_WIRE_PROTOCOL })\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 } & CacheCodecOptions): CacheStrategy {\n const defaultTtl = options?.defaultTtl\n const codec = resolveCacheCodecConfig(options)\n const keyPrefix = 'cache:'\n const tagPrefix = 'tag:'\n const connectionUrl = redisUrl || getRedisUrlOrThrow('CACHE')\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 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.getBuffer(cacheKey)\n\n if (!data) return null\n\n try {\n const entry: CacheEntry = JSON.parse(decodeCacheText(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.getBuffer(cacheKey)\n if (oldData) {\n try {\n const oldEntry: CacheEntry = JSON.parse(decodeCacheText(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 = encodeCacheText(JSON.stringify(entry), codec)\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.getBuffer(cacheKey)\n if (!data) return false\n\n try {\n const entry: CacheEntry = JSON.parse(decodeCacheText(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.getBuffer(cacheKey)\n if (!data) return false\n\n try {\n const entry: CacheEntry = JSON.parse(decodeCacheText(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 // Keep which tag each key came from. Reaping needs it to stay linear in the\n // memberships that actually exist rather than in tags x keys.\n const membersByTagKey = new Map<string, 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 membersByTagKey.set(tagKey, keys)\n for (const key of keys) {\n keysToDelete.add(key)\n }\n }\n\n // Delete all collected keys\n let deleted = 0\n const orphans = new Set<string>()\n for (const key of keysToDelete) {\n const success = await deleteKey(key)\n if (success) deleted++\n else orphans.add(key)\n }\n\n // A key whose value has already expired leaves its name behind in every tag\n // set it was added to: `set()` only prunes tags when it overwrites a live\n // key, `deleteKey` only prunes when it finds one, and `cleanup()` scans\n // value keys rather than tag sets. Nothing else reaps them, so the sets grow\n // without bound whenever TTL'd entries also rotate their key names \u2014 and\n // every later invalidation pays a GET per orphan. Reap what we just walked.\n //\n // `deleteKey` reported these missing some time ago \u2014 the walk above is one\n // round trip per member \u2014 so a concurrent request may have rebuilt any of\n // them since. Re-check each value key atomically with the removal rather\n // than trusting that stale read: dropping a live key from the index would\n // silently exempt it from every future invalidation of this tag.\n //\n // Reap per tag, over that tag's own members, so the work stays linear in the\n // memberships `smembers` actually returned. Each script is capped at\n // REAP_CHUNK_SIZE members, so no single atomic call can stall the server for\n // longer than a fixed bound however many tags were requested. The calls are\n // pipelined into one round trip, but stay separate scripts so Redis can\n // serve other clients between them.\n if (orphans.size > 0) {\n const pipeline = client.pipeline()\n let queued = 0\n for (const [tagKey, members] of membersByTagKey) {\n const tagOrphans = members.filter((member) => orphans.has(member))\n for (let offset = 0; offset < tagOrphans.length; offset += REAP_CHUNK_SIZE) {\n const chunk = tagOrphans.slice(offset, offset + REAP_CHUNK_SIZE)\n const keys = [tagKey, ...chunk.map(getCacheKey)]\n pipeline.eval(REAP_ORPHANED_TAG_MEMBERS, keys.length, ...keys, ...chunk)\n queued++\n }\n }\n if (queued > 0) await pipeline.exec()\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) => matchCacheKeyPattern(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.getBuffer(cacheKey)\n if (data) {\n try {\n const entry: CacheEntry = JSON.parse(decodeCacheText(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.getBuffer(cacheKey)\n if (data) {\n try {\n const entry: CacheEntry = JSON.parse(decodeCacheText(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 const healthcheck = async (): Promise<void> => {\n const client = await getRedisClient()\n await client.ping()\n }\n\n return {\n get,\n set,\n has,\n delete: deleteKey,\n deleteByTags,\n clear,\n keys,\n stats,\n healthcheck,\n cleanup,\n close,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport { CacheDependencyUnavailableError } from '../errors'\nimport { getRedisUrlOrThrow, REDIS_WIRE_PROTOCOL } from '@open-mercato/shared/lib/redis/connection'\nimport type { RedisProtocolVersion } from '@open-mercato/shared/lib/redis/connection'\nimport { matchCacheKeyPattern } from '../patterns'\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 eval(script: string, numKeys: number, ...args: (string | number)[]): RedisPipeline\n exec(): Promise<unknown>\n}\n\ntype RedisClient = {\n ping(): Promise<string>\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, options?: { protocol?: RedisProtocolVersion }) => RedisClient\ntype PossibleRedisModule = unknown\n\ntype RequireFn = (id: string) => unknown\n\n/**\n * Reaps orphaned members from ONE tag set atomically.\n *\n * `KEYS[1]` is the tag key and `KEYS[2..]` are the candidate value keys, with\n * `ARGV[1..]` the member names aligned to them. Every key the script touches is\n * declared in `KEYS` so it stays valid against a slot-aware deployment.\n *\n * The `EXISTS` check and the `SREM` run in one atomic step, which is the whole\n * point: a member may only be dropped while its value key is genuinely absent.\n * A concurrent `set()` either lands first (`EXISTS` is 1, so the member is left\n * alone) or lands after (its own `sadd` re-adds the member). Either ordering\n * leaves a live key indexed.\n *\n * Deliberately single-tag. Pairing every orphan with every requested tag would\n * make the work quadratic in the tag count \u2014 and because a script blocks the\n * server for its whole run, that lands as a stall rather than as throughput.\n * Callers reap per tag using the membership each `smembers` actually returned.\n */\nconst REAP_ORPHANED_TAG_MEMBERS = `\nlocal removed = 0\nfor i = 2, #KEYS do\n if redis.call('EXISTS', KEYS[i]) == 0 then\n removed = removed + redis.call('SREM', KEYS[1], ARGV[i - 1])\n end\nend\nreturn removed\n`\n\n/** Bounds a single script's blocking time on the server, and the request size. */\nconst REAP_CHUNK_SIZE = 256\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, { protocol: REDIS_WIRE_PROTOCOL })\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 = redisUrl || getRedisUrlOrThrow('CACHE')\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 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 // Keep which tag each key came from. Reaping needs it to stay linear in the\n // memberships that actually exist rather than in tags x keys.\n const membersByTagKey = new Map<string, 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 membersByTagKey.set(tagKey, keys)\n for (const key of keys) {\n keysToDelete.add(key)\n }\n }\n\n // Delete all collected keys\n let deleted = 0\n const orphans = new Set<string>()\n for (const key of keysToDelete) {\n const success = await deleteKey(key)\n if (success) deleted++\n else orphans.add(key)\n }\n\n // A key whose value has already expired leaves its name behind in every tag\n // set it was added to: `set()` only prunes tags when it overwrites a live\n // key, `deleteKey` only prunes when it finds one, and `cleanup()` scans\n // value keys rather than tag sets. Nothing else reaps them, so the sets grow\n // without bound whenever TTL'd entries also rotate their key names \u2014 and\n // every later invalidation pays a GET per orphan. Reap what we just walked.\n //\n // `deleteKey` reported these missing some time ago \u2014 the walk above is one\n // round trip per member \u2014 so a concurrent request may have rebuilt any of\n // them since. Re-check each value key atomically with the removal rather\n // than trusting that stale read: dropping a live key from the index would\n // silently exempt it from every future invalidation of this tag.\n //\n // Reap per tag, over that tag's own members, so the work stays linear in the\n // memberships `smembers` actually returned. Each script is capped at\n // REAP_CHUNK_SIZE members, so no single atomic call can stall the server for\n // longer than a fixed bound however many tags were requested. The calls are\n // pipelined into one round trip, but stay separate scripts so Redis can\n // serve other clients between them.\n if (orphans.size > 0) {\n const pipeline = client.pipeline()\n let queued = 0\n for (const [tagKey, members] of membersByTagKey) {\n const tagOrphans = members.filter((member) => orphans.has(member))\n for (let offset = 0; offset < tagOrphans.length; offset += REAP_CHUNK_SIZE) {\n const chunk = tagOrphans.slice(offset, offset + REAP_CHUNK_SIZE)\n const keys = [tagKey, ...chunk.map(getCacheKey)]\n pipeline.eval(REAP_ORPHANED_TAG_MEMBERS, keys.length, ...keys, ...chunk)\n queued++\n }\n }\n if (queued > 0) await pipeline.exec()\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) => matchCacheKeyPattern(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 const healthcheck = async (): Promise<void> => {\n const client = await getRedisClient()\n await client.ping()\n }\n\n return {\n get,\n set,\n has,\n delete: deleteKey,\n deleteByTags,\n clear,\n keys,\n stats,\n healthcheck,\n cleanup,\n close,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["options", "pipeline", "keys"]
7
7
  }
@@ -4,12 +4,10 @@ import path from "node:path";
4
4
  import { CacheDependencyUnavailableError } from "../errors.js";
5
5
  import { DEFAULT_SQLITE_CACHE_PATH } from "../defaults.js";
6
6
  import { matchCacheKeyPattern } from "../patterns.js";
7
- import { decodeCacheText, encodeCacheText, resolveCacheCodecConfig } from "../codec.js";
8
7
  const sqliteRequire = createRequire(path.join(process.cwd(), "package.json"));
9
8
  function createSqliteStrategy(dbPath, options) {
10
9
  let db = null;
11
10
  const defaultTtl = options?.defaultTtl;
12
- const codec = resolveCacheCodecConfig(options);
13
11
  const filePath = dbPath || process.env.CACHE_SQLITE_PATH || DEFAULT_SQLITE_CACHE_PATH;
14
12
  async function getDb() {
15
13
  if (db) return db;
@@ -60,7 +58,7 @@ function createSqliteStrategy(dbPath, options) {
60
58
  const row = stmt.get(key);
61
59
  if (!row) return null;
62
60
  try {
63
- const value = JSON.parse(decodeCacheText(row.value));
61
+ const value = JSON.parse(row.value);
64
62
  const expiresAt = row.expires_at;
65
63
  if (isExpired(expiresAt)) {
66
64
  if (options2?.returnExpired) {
@@ -81,7 +79,7 @@ function createSqliteStrategy(dbPath, options) {
81
79
  const tags = options2?.tags || [];
82
80
  const expiresAt = ttl ? Date.now() + ttl : null;
83
81
  const createdAt = Date.now();
84
- const serialized = encodeCacheText(JSON.stringify(value), codec);
82
+ const serialized = JSON.stringify(value);
85
83
  database.transaction(() => {
86
84
  database.prepare("DELETE FROM cache_tags WHERE key = ?").run(key);
87
85
  database.prepare(`
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/strategies/sqlite.ts"],
4
- "sourcesContent": ["import type { CacheStrategy, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport fs from 'node:fs'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport { CacheDependencyUnavailableError } from '../errors'\nimport { DEFAULT_SQLITE_CACHE_PATH } from '../defaults'\nimport { matchCacheKeyPattern } from '../patterns'\nimport { decodeCacheText, encodeCacheText, resolveCacheCodecConfig } from '../codec'\nimport type { CacheCodecOptions } from '../codec'\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 }\ntype SqliteStrategyOptions = { defaultTtl?: number; databaseConstructor?: SqliteConstructor } & CacheCodecOptions\n\nconst sqliteRequire = createRequire(path.join(process.cwd(), 'package.json'))\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?: SqliteStrategyOptions): CacheStrategy {\n let db: SqliteDatabase | null = null\n const defaultTtl = options?.defaultTtl\n const codec = resolveCacheCodecConfig(options)\n const filePath = dbPath || process.env.CACHE_SQLITE_PATH || DEFAULT_SQLITE_CACHE_PATH\n\n async function getDb(): Promise<SqliteDatabase> {\n if (db) return db\n\n try {\n const imported = (options?.databaseConstructor ?? sqliteRequire('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 db.exec(`\n PRAGMA journal_mode = WAL;\n PRAGMA synchronous = NORMAL;\n PRAGMA busy_timeout = 5000;\n PRAGMA foreign_keys = ON;\n `)\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 type EntryRow = { value: string | Buffer; 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(decodeCacheText(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 = encodeCacheText(JSON.stringify(value), codec)\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) => matchCacheKeyPattern(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": "AACA,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,OAAO,UAAU;AACjB,SAAS,uCAAuC;AAChD,SAAS,iCAAiC;AAC1C,SAAS,4BAA4B;AACrC,SAAS,iBAAiB,iBAAiB,+BAA+B;AAsB1E,MAAM,gBAAgB,cAAc,KAAK,KAAK,QAAQ,IAAI,GAAG,cAAc,CAAC;AAUrE,SAAS,qBAAqB,QAAiB,SAAgD;AACpG,MAAI,KAA4B;AAChC,QAAM,aAAa,SAAS;AAC5B,QAAM,QAAQ,wBAAwB,OAAO;AAC7C,QAAM,WAAW,UAAU,QAAQ,IAAI,qBAAqB;AAE5D,iBAAe,QAAiC;AAC9C,QAAI,GAAI,QAAO;AAEf,QAAI;AACF,YAAM,WAAY,SAAS,uBAAuB,cAAc,gBAAgB;AAChF,YAAM,WAAW,OAAO,aAAa,aAAa,WAAW,SAAS;AAGtE,YAAM,MAAM,KAAK,QAAQ,QAAQ;AACjC,UAAI,CAAC,GAAG,WAAW,GAAG,GAAG;AACvB,WAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,MACvC;AAEA,WAAK,IAAI,SAAS,QAAQ;AAE1B,SAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,OAKP;AAGD,SAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAiBP;AAED,aAAO;AAAA,IACT,SAAS,OAAO;AACd,YAAM,IAAI,gCAAgC,UAAU,kBAAkB,KAAK;AAAA,IAC7E;AAAA,EACF;AAEA,WAAS,UAAU,WAAmC;AACpD,QAAI,cAAc,KAAM,QAAO;AAC/B,WAAO,KAAK,IAAI,IAAI;AAAA,EACtB;AAOA,QAAM,MAAM,OAAO,KAAaA,aAA0D;AACxF,UAAM,WAAW,MAAM,MAAM;AAE7B,UAAM,OAAO,SAAS,QAAQ,2DAA2D;AACzF,UAAM,MAAM,KAAK,IAAI,GAAG;AAExB,QAAI,CAAC,IAAK,QAAO;AAEjB,QAAI;AACF,YAAM,QAAQ,KAAK,MAAM,gBAAgB,IAAI,KAAK,CAAC;AACnD,YAAM,YAAY,IAAI;AAEtB,UAAI,UAAU,SAAS,GAAG;AACxB,YAAIA,UAAS,eAAe;AAC1B,iBAAO;AAAA,QACT;AAEA,cAAM,UAAU,GAAG;AACnB,eAAO;AAAA,MACT;AAEA,aAAO;AAAA,IACT,QAAQ;AAEN,YAAM,UAAU,GAAG;AACnB,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,MAAM,OAAO,KAAa,OAAmBA,aAA6C;AAC9F,UAAM,WAAW,MAAM,MAAM;AAC7B,UAAM,MAAMA,UAAS,OAAO;AAC5B,UAAM,OAAOA,UAAS,QAAQ,CAAC;AAC/B,UAAM,YAAY,MAAM,KAAK,IAAI,IAAI,MAAM;AAC3C,UAAM,YAAY,KAAK,IAAI;AAE3B,UAAM,aAAa,gBAAgB,KAAK,UAAU,KAAK,GAAG,KAAK;AAE/D,aAAS,YAAY,MAAM;AAEzB,eAAS,QAAQ,sCAAsC,EAAE,IAAI,GAAG;AAGhE,eAAS,QAAQ;AAAA;AAAA;AAAA,OAGhB,EAAE,IAAI,KAAK,YAAY,WAAW,SAAS;AAG5C,UAAI,KAAK,SAAS,GAAG;AACnB,cAAM,YAAY,SAAS,QAAQ,iDAAiD;AACpF,mBAAW,OAAO,MAAM;AACtB,oBAAU,IAAI,KAAK,GAAG;AAAA,QACxB;AAAA,MACF;AAAA,IACF,CAAC,EAAE;AAAA,EACL;AAEA,QAAM,MAAM,OAAO,QAAkC;AACnD,UAAM,WAAW,MAAM,MAAM;AAE7B,UAAM,OAAO,SAAS,QAAQ,oDAAoD;AAClF,UAAM,MAAM,KAAK,IAAI,GAAG;AAExB,QAAI,CAAC,IAAK,QAAO;AAEjB,QAAI,UAAU,IAAI,UAAU,GAAG;AAC7B,YAAM,UAAU,GAAG;AACnB,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,OAAO,QAAkC;AACzD,UAAM,WAAW,MAAM,MAAM;AAE7B,UAAM,SAAS,SAAS,YAAY,MAAM;AACxC,eAAS,QAAQ,sCAAsC,EAAE,IAAI,GAAG;AAChE,YAAM,OAAO,SAAS,QAAQ,yCAAyC,EAAE,IAAI,GAAG;AAChF,aAAO,KAAK,UAAU;AAAA,IACxB,CAAC,EAAE;AAEH,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,OAAO,SAAoC;AAC9D,UAAM,WAAW,MAAM,MAAM;AAG7B,UAAM,eAAe,KAAK,IAAI,MAAM,GAAG,EAAE,KAAK,GAAG;AACjD,UAAM,OAAO,SAAS,QAAQ;AAAA,0DACwB,YAAY;AAAA,KACjE;AACD,UAAM,OAAO,KAAK,IAAI,GAAG,IAAI;AAE7B,QAAI,UAAU;AACd,eAAW,OAAO,MAAM;AACtB,YAAM,UAAU,MAAM,UAAU,IAAI,GAAG;AACvC,UAAI,QAAS;AAAA,IACf;AAEA,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,YAA6B;AACzC,UAAM,WAAW,MAAM,MAAM;AAE7B,UAAM,SAAS,SAAS,YAAY,MAAM;AACxC,YAAM,YAAY,SAAS,QAAQ,6CAA6C;AAChF,YAAM,QAAS,UAAU,IAAI,EAAe;AAE5C,eAAS,QAAQ,wBAAwB,EAAE,IAAI;AAC/C,eAAS,QAAQ,2BAA2B,EAAE,IAAI;AAElD,aAAO;AAAA,IACT,CAAC,EAAE;AAEH,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,OAAO,YAAwC;AAC1D,UAAM,WAAW,MAAM,MAAM;AAE7B,UAAM,OAAO,SAAS,QAAQ,+BAA+B;AAC7D,UAAM,OAAO,KAAK,IAAI;AAEtB,UAAM,UAAU,KAAK,IAAI,CAAC,QAAQ,IAAI,GAAG;AAEzC,QAAI,CAAC,QAAS,QAAO;AAErB,WAAO,QAAQ,OAAO,CAAC,QAAgB,qBAAqB,KAAK,OAAO,CAAC;AAAA,EAC3E;AAEA,QAAM,QAAQ,YAAwD;AACpE,UAAM,WAAW,MAAM,MAAM;AAE7B,UAAM,WAAW,SAAS,QAAQ,6CAA6C;AAC/E,UAAM,OAAQ,SAAS,IAAI,EAAe;AAE1C,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,cAAc,SAAS,QAAQ,6FAA6F;AAClI,UAAM,UAAW,YAAY,IAAI,GAAG,EAAe;AAEnD,WAAO,EAAE,MAAM,QAAQ;AAAA,EACzB;AAEA,QAAM,UAAU,YAA6B;AAC3C,UAAM,WAAW,MAAM,MAAM;AAC7B,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,SAAS,SAAS,YAAY,MAAM;AAExC,YAAM,OAAO,SAAS,QAAQ,+EAA+E;AAC7G,YAAM,OAAO,KAAK,IAAI,GAAG;AAGzB,iBAAW,OAAO,MAAM;AACtB,iBAAS,QAAQ,sCAAsC,EAAE,IAAI,IAAI,GAAG;AAAA,MACtE;AAGA,YAAM,OAAO,SAAS,QAAQ,2EAA2E,EAAE,IAAI,GAAG;AAClH,aAAO,KAAK;AAAA,IACd,CAAC,EAAE;AAEH,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,YAA2B;AACvC,QAAI,IAAI;AACN,SAAG,MAAM;AACT,WAAK;AAAA,IACP;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { CacheStrategy, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport fs from 'node:fs'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport { CacheDependencyUnavailableError } from '../errors'\nimport { DEFAULT_SQLITE_CACHE_PATH } from '../defaults'\nimport { matchCacheKeyPattern } from '../patterns'\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 }\ntype SqliteStrategyOptions = { defaultTtl?: number; databaseConstructor?: SqliteConstructor }\n\nconst sqliteRequire = createRequire(path.join(process.cwd(), 'package.json'))\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?: SqliteStrategyOptions): CacheStrategy {\n let db: SqliteDatabase | null = null\n const defaultTtl = options?.defaultTtl\n const filePath = dbPath || process.env.CACHE_SQLITE_PATH || DEFAULT_SQLITE_CACHE_PATH\n\n async function getDb(): Promise<SqliteDatabase> {\n if (db) return db\n\n try {\n const imported = (options?.databaseConstructor ?? sqliteRequire('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 db.exec(`\n PRAGMA journal_mode = WAL;\n PRAGMA synchronous = NORMAL;\n PRAGMA busy_timeout = 5000;\n PRAGMA foreign_keys = ON;\n `)\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 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) => matchCacheKeyPattern(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
6
  "names": ["options"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cache",
3
- "version": "0.9.1-develop.7574.1.837288eec1",
3
+ "version": "0.10.1-develop.7576.1.21d5b50435",
4
4
  "license": "MIT",
5
5
  "description": "Multi-strategy cache service with tag-based invalidation support",
6
6
  "type": "module",
@@ -72,5 +72,5 @@
72
72
  "url": "https://github.com/open-mercato/open-mercato",
73
73
  "directory": "packages/cache"
74
74
  },
75
- "stableVersion": "0.9.0"
75
+ "stableVersion": "0.10.0"
76
76
  }