@open-mercato/cache 0.9.1-develop.7573.1.d0ccc51d5b → 0.9.1-develop.7575.1.8480826a9b

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.
@@ -5,7 +5,7 @@ import { createRedisStrategy } from '../strategies/redis'
5
5
  type PipelineOp = () => void
6
6
 
7
7
  class FakeRedis {
8
- values = new Map<string, string | Buffer>()
8
+ values = new Map<string, string>()
9
9
  sets = new Map<string, Set<string>>()
10
10
  /** Records the shape of each reap script call so tests can assert its cost. */
11
11
  evalCalls: { numKeys: number; memberCount: number }[] = []
@@ -15,23 +15,15 @@ class FakeRedis {
15
15
  }
16
16
 
17
17
  async get(key: string): Promise<string | null> {
18
- const value = this.values.get(key)
19
- if (value === undefined) return null
20
- return Buffer.isBuffer(value) ? value.toString('utf8') : value
18
+ return this.values.get(key) ?? null
21
19
  }
22
20
 
23
- async getBuffer(key: string): Promise<Buffer | null> {
24
- const value = this.values.get(key)
25
- if (value === undefined) return null
26
- return Buffer.isBuffer(value) ? value : Buffer.from(value, 'utf8')
27
- }
28
-
29
- async set(key: string, value: string | Buffer): Promise<unknown> {
21
+ async set(key: string, value: string): Promise<unknown> {
30
22
  this.values.set(key, value)
31
23
  return 'OK'
32
24
  }
33
25
 
34
- async setex(key: string, _ttlSeconds: number, value: string | Buffer): Promise<unknown> {
26
+ async setex(key: string, _ttlSeconds: number, value: string): Promise<unknown> {
35
27
  this.values.set(key, value)
36
28
  return 'OK'
37
29
  }
@@ -83,11 +75,11 @@ class FakeRedis {
83
75
  pipeline() {
84
76
  const ops: PipelineOp[] = []
85
77
  const chain = {
86
- set: (key: string, value: string | Buffer) => {
78
+ set: (key: string, value: string) => {
87
79
  ops.push(() => void this.values.set(key, value))
88
80
  return chain
89
81
  },
90
- setex: (key: string, _ttlSeconds: number, value: string | Buffer) => {
82
+ setex: (key: string, _ttlSeconds: number, value: string) => {
91
83
  ops.push(() => void this.values.set(key, value))
92
84
  return chain
93
85
  },
@@ -210,9 +202,9 @@ describe('redis strategy tag index', () => {
210
202
  // deleteByTags walks members one round trip at a time, so an entry it read
211
203
  // as missing can be rebuilt before the reap runs. Land that write in the
212
204
  // window by hooking the GET that reports the key absent.
213
- const readValue = currentRedis.getBuffer.bind(currentRedis)
205
+ const readValue = currentRedis.get.bind(currentRedis)
214
206
  let rebuilt = false
215
- currentRedis.getBuffer = async (key: string) => {
207
+ currentRedis.get = async (key: string) => {
216
208
  const result = await readValue(key)
217
209
  if (!rebuilt && key === 'cache:nav:user-1' && result === null) {
218
210
  rebuilt = true
@@ -222,7 +214,7 @@ describe('redis strategy tag index', () => {
222
214
  }
223
215
 
224
216
  await strategy.deleteByTags(['rbac:tenant:t1'])
225
- currentRedis.getBuffer = readValue
217
+ currentRedis.get = readValue
226
218
 
227
219
  // The rebuilt entry must stay in the index, or it silently outlives every
228
220
  // future invalidation of this tag and serves stale data until its TTL.
@@ -284,95 +276,3 @@ describe('redis strategy tag index', () => {
284
276
  expect(await currentRedis.smembers('tag:tag-b')).toEqual(['key-1'])
285
277
  })
286
278
  })
287
-
288
- describe('redis strategy value compression', () => {
289
- let urlCounter = 0
290
- const largeValue = {
291
- items: Array.from({ length: 100 }, (_, index) => ({ id: `row-${index}`, name: `Contact ${index}`, status: 'active' })),
292
- }
293
-
294
- function createStrategy(compression: 'off' | 'zstd' | 'gzip'): CacheStrategy {
295
- urlCounter += 1
296
- return createRedisStrategy(`redis://cache-compression-test-${urlCounter}`, { compression })
297
- }
298
-
299
- beforeEach(() => {
300
- currentRedis = new FakeRedis()
301
- })
302
-
303
- it.each([
304
- ['zstd', [0x28, 0xb5, 0x2f, 0xfd]],
305
- ['gzip', [0x1f, 0x8b]],
306
- ] as const)('stores large entries as %s frames and reads them back', async (compression, magic) => {
307
- const strategy = createStrategy(compression)
308
-
309
- await strategy.set('list:page:1', largeValue, { ttl: 60_000, tags: ['customers'] })
310
-
311
- const stored = currentRedis.values.get('cache:list:page:1')
312
- expect(Buffer.isBuffer(stored)).toBe(true)
313
- expect(magic.every((byte, index) => (stored as Buffer)[index] === byte)).toBe(true)
314
- expect(await strategy.get('list:page:1')).toEqual(largeValue)
315
- expect(await strategy.has('list:page:1')).toBe(true)
316
- await strategy.close?.()
317
- })
318
-
319
- it('keeps small entries as plain JSON strings', async () => {
320
- const strategy = createStrategy('zstd')
321
-
322
- await strategy.set('small', { id: 1 })
323
-
324
- expect(typeof currentRedis.values.get('cache:small')).toBe('string')
325
- expect(await strategy.get('small')).toEqual({ id: 1 })
326
- await strategy.close?.()
327
- })
328
-
329
- it('reads legacy and compressed entries side by side in either direction', async () => {
330
- const legacy = createStrategy('off')
331
- await legacy.set('legacy', largeValue)
332
- const compressed = createStrategy('zstd')
333
- await compressed.set('compressed', largeValue)
334
-
335
- expect(typeof currentRedis.values.get('cache:legacy')).toBe('string')
336
- expect(await compressed.get('legacy')).toEqual(largeValue)
337
- expect(await legacy.get('compressed')).toEqual(largeValue)
338
- await legacy.close?.()
339
- await compressed.close?.()
340
- })
341
-
342
- it('moves tag memberships when a compressed entry is overwritten', async () => {
343
- const strategy = createStrategy('zstd')
344
-
345
- await strategy.set('list', largeValue, { tags: ['old-tag'] })
346
- await strategy.set('list', largeValue, { tags: ['new-tag'] })
347
-
348
- expect(await currentRedis.smembers('tag:old-tag')).toEqual([])
349
- expect(await currentRedis.smembers('tag:new-tag')).toEqual(['list'])
350
- expect(await strategy.deleteByTags(['new-tag'])).toBe(1)
351
- expect(currentRedis.values.has('cache:list')).toBe(false)
352
- expect(await currentRedis.smembers('tag:new-tag')).toEqual([])
353
- await strategy.close?.()
354
- })
355
-
356
- it('counts and cleans up expired compressed entries', async () => {
357
- const strategy = createStrategy('gzip')
358
- await strategy.set('fresh', largeValue, { ttl: 60_000, tags: ['t'] })
359
- await strategy.set('stale', largeValue, { ttl: 1, tags: ['t'] })
360
- await new Promise((resolve) => setTimeout(resolve, 5))
361
-
362
- expect(await strategy.stats()).toEqual({ size: 2, expired: 1 })
363
- expect(await strategy.cleanup?.()).toBe(1)
364
- expect(currentRedis.values.has('cache:fresh')).toBe(true)
365
- expect(currentRedis.values.has('cache:stale')).toBe(false)
366
- expect(await currentRedis.smembers('tag:t')).toEqual(['fresh'])
367
- await strategy.close?.()
368
- })
369
-
370
- it('drops a corrupt frame as a miss', async () => {
371
- const strategy = createStrategy('zstd')
372
- currentRedis.values.set('cache:broken', Buffer.from([0x28, 0xb5, 0x2f, 0xfd, 0xff, 0xff, 0xff]))
373
-
374
- expect(await strategy.get('broken')).toBeNull()
375
- expect(currentRedis.values.has('cache:broken')).toBe(false)
376
- await strategy.close?.()
377
- })
378
- })
@@ -392,57 +392,3 @@ describe('Cache Service stats() counter gating', () => {
392
392
  expect(stats.lastSweepReclaimed).toBe(3)
393
393
  })
394
394
  })
395
-
396
- describe('Cache Service compression resolution (CACHE_COMPRESSION)', () => {
397
- afterEach(() => {
398
- delete process.env.CACHE_COMPRESSION
399
- delete process.env.CACHE_COMPRESSION_MIN_BYTES
400
- jest.restoreAllMocks()
401
- })
402
-
403
- const captureSqliteOptions = () => {
404
- const spy = jest.spyOn(sqliteStrategyModule, 'createSqliteStrategy')
405
- spy.mockImplementation((_path, options) => {
406
- return memoryStrategyModule.createMemoryStrategy({ defaultTtl: options?.defaultTtl })
407
- })
408
- return spy
409
- }
410
-
411
- it('passes the env compression settings to persistent strategies', () => {
412
- process.env.CACHE_COMPRESSION = 'zstd'
413
- process.env.CACHE_COMPRESSION_MIN_BYTES = '2048'
414
- const spy = captureSqliteOptions()
415
-
416
- createCacheService({ strategy: 'sqlite' })
417
-
418
- expect(spy.mock.calls[0][1]).toMatchObject({ compression: 'zstd', compressionMinBytes: 2048 })
419
- })
420
-
421
- it('lets explicit options override the env', () => {
422
- process.env.CACHE_COMPRESSION = 'zstd'
423
- const spy = captureSqliteOptions()
424
-
425
- createCacheService({ strategy: 'sqlite', compression: 'gzip', compressionMinBytes: 512 })
426
-
427
- expect(spy.mock.calls[0][1]).toMatchObject({ compression: 'gzip', compressionMinBytes: 512 })
428
- })
429
-
430
- it('keeps compression off for an unknown env value and warns', () => {
431
- process.env.CACHE_COMPRESSION = 'lz4-service-test'
432
- const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
433
- const spy = captureSqliteOptions()
434
-
435
- createCacheService({ strategy: 'sqlite' })
436
-
437
- expect(spy.mock.calls[0][1]).toMatchObject({ compression: 'off' })
438
- expect(warn).toHaveBeenCalledWith(expect.stringContaining('lz4-service-test'))
439
- })
440
-
441
- it('defaults to off with no env and no options', () => {
442
- const spy = captureSqliteOptions()
443
-
444
- createCacheService({ strategy: 'sqlite' })
445
-
446
- expect(spy.mock.calls[0][1]).toMatchObject({ compression: 'off', compressionMinBytes: undefined })
447
- })
448
- })
@@ -3,7 +3,7 @@ import os from 'node:os'
3
3
  import path from 'node:path'
4
4
  import { createSqliteStrategy } from '../strategies/sqlite'
5
5
 
6
- type CacheEntry = { value: string | Buffer; expires_at: number | null; created_at: number }
6
+ type CacheEntry = { value: string; expires_at: number | null; created_at: number }
7
7
 
8
8
  class MockSqliteDatabase {
9
9
  static instances: MockSqliteDatabase[] = []
@@ -90,7 +90,7 @@ class MockSqliteDatabase {
90
90
  if (sql.startsWith('INSERT OR REPLACE INTO cache_entries')) {
91
91
  const [key, value, expiresAt, createdAt] = args
92
92
  this.entries.set(String(key), {
93
- value: Buffer.isBuffer(value) ? value : String(value),
93
+ value: String(value),
94
94
  expires_at: expiresAt === null ? null : Number(expiresAt),
95
95
  created_at: Number(createdAt),
96
96
  })
@@ -161,54 +161,3 @@ describe('SQLite Cache Strategy', () => {
161
161
  await cache.close?.()
162
162
  })
163
163
  })
164
-
165
- describe('SQLite Cache Strategy value compression (better-sqlite3)', () => {
166
- const largeValue = {
167
- items: Array.from({ length: 100 }, (_, index) => ({ id: `row-${index}`, name: `Contact ${index}`, status: 'active' })),
168
- }
169
- let tempDir: string
170
- let dbPath: string
171
-
172
- beforeEach(() => {
173
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'open-mercato-cache-compression-'))
174
- dbPath = path.join(tempDir, 'cache.db')
175
- })
176
-
177
- afterEach(() => {
178
- fs.rmSync(tempDir, { recursive: true, force: true })
179
- })
180
-
181
- function storedTypes(): Record<string, string> {
182
- const Database = require('better-sqlite3') as typeof import('better-sqlite3')
183
- const db = new Database(dbPath, { readonly: true })
184
- try {
185
- const rows = db.prepare('SELECT key, typeof(value) AS type FROM cache_entries').all() as { key: string; type: string }[]
186
- return Object.fromEntries(rows.map((row) => [row.key, row.type]))
187
- } finally {
188
- db.close()
189
- }
190
- }
191
-
192
- it.each(['zstd', 'gzip'] as const)('stores large %s entries as BLOBs next to legacy TEXT rows', async (compression) => {
193
- const legacy = createSqliteStrategy(dbPath, { compression: 'off' })
194
- await legacy.set('legacy', largeValue, { tags: ['customers'] })
195
- await legacy.close?.()
196
-
197
- const cache = createSqliteStrategy(dbPath, { compression })
198
- await cache.set('compressed', largeValue, { tags: ['customers'] })
199
- await cache.set('small', { id: 1 }, { tags: ['customers'] })
200
-
201
- expect(await cache.get('legacy')).toEqual(largeValue)
202
- expect(await cache.get('compressed')).toEqual(largeValue)
203
- expect(await cache.get('small')).toEqual({ id: 1 })
204
- await cache.close?.()
205
-
206
- expect(storedTypes()).toEqual({ legacy: 'text', compressed: 'blob', small: 'text' })
207
-
208
- const off = createSqliteStrategy(dbPath, { compression: 'off' })
209
- expect(await off.get('compressed')).toEqual(largeValue)
210
- expect(await off.deleteByTags(['customers'])).toBe(3)
211
- expect(await off.get('compressed')).toBeNull()
212
- await off.close?.()
213
- })
214
- })
package/src/service.ts CHANGED
@@ -7,8 +7,6 @@ import { getCurrentCacheTenant } from './tenantContext'
7
7
  import { createHash } from 'node:crypto'
8
8
  import { CacheDependencyUnavailableError } from './errors'
9
9
  import { matchCacheKeyPattern } from './patterns'
10
- import { parseCacheCompression, parseCacheCompressionMinBytes } from './codec'
11
- import type { CacheCodecOptions } from './codec'
12
10
 
13
11
  function normalizeTenantKey(raw: string | null | undefined): string {
14
12
  const value = typeof raw === 'string' ? raw.trim() : ''
@@ -262,12 +260,7 @@ function createConfiguredTenantAwareStrategy(options?: CacheServiceOptions): Ten
262
260
  const parsedEnvMaxEntries = envMaxEntries ? Number.parseInt(envMaxEntries, 10) : undefined
263
261
  const maxEntries = options?.maxEntries ?? (typeof parsedEnvMaxEntries === 'number' && Number.isFinite(parsedEnvMaxEntries) ? parsedEnvMaxEntries : undefined)
264
262
 
265
- const codecOptions: CacheCodecOptions = {
266
- compression: options?.compression ?? parseCacheCompression(process.env.CACHE_COMPRESSION),
267
- compressionMinBytes: options?.compressionMinBytes ?? parseCacheCompressionMinBytes(process.env.CACHE_COMPRESSION_MIN_BYTES),
268
- }
269
-
270
- const baseStrategy = createStrategyForType(strategyType, options, defaultTtl, maxEntries, codecOptions)
263
+ const baseStrategy = createStrategyForType(strategyType, options, defaultTtl, maxEntries)
271
264
  const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl, maxEntries)
272
265
  const reportsBoundedCounters = strategyType === 'memory'
273
266
 
@@ -283,8 +276,6 @@ function createConfiguredTenantAwareStrategy(options?: CacheServiceOptions): Ten
283
276
  * - CACHE_REDIS_URL: Redis connection URL (for redis strategy)
284
277
  * - CACHE_SQLITE_PATH: SQLite database file path (for sqlite strategy)
285
278
  * - CACHE_JSON_FILE_PATH: JSON file path (for jsonfile strategy)
286
- * - CACHE_COMPRESSION: 'off' | 'zstd' | 'gzip' value compression for redis/sqlite (default: 'off')
287
- * - CACHE_COMPRESSION_MIN_BYTES: smallest serialized entry that gets compressed (default: 1024)
288
279
  *
289
280
  * @example
290
281
  * const cache = createCacheService({ strategy: 'memory', defaultTtl: 60000 })
@@ -382,12 +373,12 @@ export class CacheService implements CacheStrategy {
382
373
  }
383
374
  }
384
375
 
385
- function createStrategyForType(strategyType: CacheStrategyName, options?: CacheServiceOptions, defaultTtl?: number, maxEntries?: number, codecOptions?: CacheCodecOptions): CacheStrategy {
376
+ function createStrategyForType(strategyType: CacheStrategyName, options?: CacheServiceOptions, defaultTtl?: number, maxEntries?: number): CacheStrategy {
386
377
  switch (strategyType) {
387
378
  case 'redis':
388
- return createRedisStrategy(options?.redisUrl, { defaultTtl, ...codecOptions })
379
+ return createRedisStrategy(options?.redisUrl, { defaultTtl })
389
380
  case 'sqlite':
390
- return createSqliteStrategy(options?.sqlitePath, { defaultTtl, ...codecOptions })
381
+ return createSqliteStrategy(options?.sqlitePath, { defaultTtl })
391
382
  case 'jsonfile':
392
383
  return createJsonFileStrategy(options?.jsonFilePath, { defaultTtl })
393
384
  case 'memory':
@@ -3,12 +3,10 @@ import { CacheDependencyUnavailableError } from '../errors'
3
3
  import { getRedisUrlOrThrow, REDIS_WIRE_PROTOCOL } from '@open-mercato/shared/lib/redis/connection'
4
4
  import type { RedisProtocolVersion } from '@open-mercato/shared/lib/redis/connection'
5
5
  import { matchCacheKeyPattern } from '../patterns'
6
- import { decodeCacheText, encodeCacheText, resolveCacheCodecConfig } from '../codec'
7
- import type { CacheCodecOptions } from '../codec'
8
6
 
9
7
  type RedisPipeline = {
10
- set(key: string, value: string | Buffer): RedisPipeline
11
- setex(key: string, ttlSeconds: number, value: string | Buffer): RedisPipeline
8
+ set(key: string, value: string): RedisPipeline
9
+ setex(key: string, ttlSeconds: number, value: string): RedisPipeline
12
10
  sadd(key: string, value: string): RedisPipeline
13
11
  srem(key: string, member: string): RedisPipeline
14
12
  del(key: string): RedisPipeline
@@ -19,9 +17,8 @@ type RedisPipeline = {
19
17
  type RedisClient = {
20
18
  ping(): Promise<string>
21
19
  get(key: string): Promise<string | null>
22
- getBuffer(key: string): Promise<Buffer | null>
23
- set(key: string, value: string | Buffer): Promise<unknown>
24
- setex(key: string, ttlSeconds: number, value: string | Buffer): Promise<unknown>
20
+ set(key: string, value: string): Promise<unknown>
21
+ setex(key: string, ttlSeconds: number, value: string): Promise<unknown>
25
22
  del(key: string): Promise<unknown>
26
23
  exists(key: string): Promise<number>
27
24
  keys(pattern: string): Promise<string[]>
@@ -189,9 +186,8 @@ async function releaseRedisEntry(key: string, entry: RedisRegistryEntry): Promis
189
186
  }
190
187
  }
191
188
 
192
- export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?: number } & CacheCodecOptions): CacheStrategy {
189
+ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?: number }): CacheStrategy {
193
190
  const defaultTtl = options?.defaultTtl
194
- const codec = resolveCacheCodecConfig(options)
195
191
  const keyPrefix = 'cache:'
196
192
  const tagPrefix = 'tag:'
197
193
  const connectionUrl = redisUrl || getRedisUrlOrThrow('CACHE')
@@ -221,12 +217,12 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
221
217
  const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {
222
218
  const client = await getRedisClient()
223
219
  const cacheKey = getCacheKey(key)
224
- const data = await client.getBuffer(cacheKey)
220
+ const data = await client.get(cacheKey)
225
221
 
226
222
  if (!data) return null
227
223
 
228
224
  try {
229
- const entry: CacheEntry = JSON.parse(decodeCacheText(data))
225
+ const entry: CacheEntry = JSON.parse(data)
230
226
 
231
227
  if (isExpired(entry)) {
232
228
  if (options?.returnExpired) {
@@ -250,10 +246,10 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
250
246
  const cacheKey = getCacheKey(key)
251
247
 
252
248
  // Remove old entry from tag index if it exists
253
- const oldData = await client.getBuffer(cacheKey)
249
+ const oldData = await client.get(cacheKey)
254
250
  if (oldData) {
255
251
  try {
256
- const oldEntry: CacheEntry = JSON.parse(decodeCacheText(oldData))
252
+ const oldEntry: CacheEntry = JSON.parse(oldData)
257
253
  // Remove from old tags
258
254
  const pipeline = client.pipeline()
259
255
  for (const tag of oldEntry.tags) {
@@ -280,7 +276,7 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
280
276
  const pipeline = client.pipeline()
281
277
 
282
278
  // Store the entry
283
- const serialized = encodeCacheText(JSON.stringify(entry), codec)
279
+ const serialized = JSON.stringify(entry)
284
280
  if (ttl) {
285
281
  pipeline.setex(cacheKey, Math.ceil(ttl / 1000), serialized)
286
282
  } else {
@@ -303,11 +299,11 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
303
299
  if (!exists) return false
304
300
 
305
301
  // Check if expired
306
- const data = await client.getBuffer(cacheKey)
302
+ const data = await client.get(cacheKey)
307
303
  if (!data) return false
308
304
 
309
305
  try {
310
- const entry: CacheEntry = JSON.parse(decodeCacheText(data))
306
+ const entry: CacheEntry = JSON.parse(data)
311
307
  if (isExpired(entry)) {
312
308
  await deleteKey(key)
313
309
  return false
@@ -323,11 +319,11 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
323
319
  const cacheKey = getCacheKey(key)
324
320
 
325
321
  // Get entry to remove from tag index
326
- const data = await client.getBuffer(cacheKey)
322
+ const data = await client.get(cacheKey)
327
323
  if (!data) return false
328
324
 
329
325
  try {
330
- const entry: CacheEntry = JSON.parse(decodeCacheText(data))
326
+ const entry: CacheEntry = JSON.parse(data)
331
327
  const pipeline = client.pipeline()
332
328
 
333
329
  // Remove from tag index
@@ -451,10 +447,10 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
451
447
 
452
448
  let expired = 0
453
449
  for (const cacheKey of cacheKeys) {
454
- const data = await client.getBuffer(cacheKey)
450
+ const data = await client.get(cacheKey)
455
451
  if (data) {
456
452
  try {
457
- const entry: CacheEntry = JSON.parse(decodeCacheText(data))
453
+ const entry: CacheEntry = JSON.parse(data)
458
454
  if (isExpired(entry)) {
459
455
  expired++
460
456
  }
@@ -473,10 +469,10 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
473
469
 
474
470
  let removed = 0
475
471
  for (const cacheKey of cacheKeys) {
476
- const data = await client.getBuffer(cacheKey)
472
+ const data = await client.get(cacheKey)
477
473
  if (data) {
478
474
  try {
479
- const entry: CacheEntry = JSON.parse(decodeCacheText(data))
475
+ const entry: CacheEntry = JSON.parse(data)
480
476
  if (isExpired(entry)) {
481
477
  const key = cacheKey.substring(keyPrefix.length)
482
478
  await deleteKey(key)
@@ -5,8 +5,6 @@ import path from 'node:path'
5
5
  import { CacheDependencyUnavailableError } from '../errors'
6
6
  import { DEFAULT_SQLITE_CACHE_PATH } from '../defaults'
7
7
  import { matchCacheKeyPattern } from '../patterns'
8
- import { decodeCacheText, encodeCacheText, resolveCacheCodecConfig } from '../codec'
9
- import type { CacheCodecOptions } from '../codec'
10
8
 
11
9
  type SqliteStatement<TResult = unknown> = {
12
10
  get(...args: unknown[]): TResult | undefined
@@ -25,7 +23,7 @@ type SqliteDatabase = {
25
23
 
26
24
  type SqliteConstructor = new (file: string) => SqliteDatabase
27
25
  type SqliteModule = SqliteConstructor | { default: SqliteConstructor }
28
- type SqliteStrategyOptions = { defaultTtl?: number; databaseConstructor?: SqliteConstructor } & CacheCodecOptions
26
+ type SqliteStrategyOptions = { defaultTtl?: number; databaseConstructor?: SqliteConstructor }
29
27
 
30
28
  const sqliteRequire = createRequire(path.join(process.cwd(), 'package.json'))
31
29
 
@@ -40,7 +38,6 @@ const sqliteRequire = createRequire(path.join(process.cwd(), 'package.json'))
40
38
  export function createSqliteStrategy(dbPath?: string, options?: SqliteStrategyOptions): CacheStrategy {
41
39
  let db: SqliteDatabase | null = null
42
40
  const defaultTtl = options?.defaultTtl
43
- const codec = resolveCacheCodecConfig(options)
44
41
  const filePath = dbPath || process.env.CACHE_SQLITE_PATH || DEFAULT_SQLITE_CACHE_PATH
45
42
 
46
43
  async function getDb(): Promise<SqliteDatabase> {
@@ -96,7 +93,7 @@ export function createSqliteStrategy(dbPath?: string, options?: SqliteStrategyOp
96
93
  return Date.now() > expiresAt
97
94
  }
98
95
 
99
- type EntryRow = { value: string | Buffer; expires_at: number | null }
96
+ type EntryRow = { value: string; expires_at: number | null }
100
97
  type ExpiresRow = { expires_at: number | null }
101
98
  type CountRow = { count: number }
102
99
  type KeyRow = { key: string }
@@ -110,7 +107,7 @@ export function createSqliteStrategy(dbPath?: string, options?: SqliteStrategyOp
110
107
  if (!row) return null
111
108
 
112
109
  try {
113
- const value = JSON.parse(decodeCacheText(row.value)) as CacheValue
110
+ const value = JSON.parse(row.value) as CacheValue
114
111
  const expiresAt = row.expires_at
115
112
 
116
113
  if (isExpired(expiresAt)) {
@@ -137,7 +134,7 @@ export function createSqliteStrategy(dbPath?: string, options?: SqliteStrategyOp
137
134
  const expiresAt = ttl ? Date.now() + ttl : null
138
135
  const createdAt = Date.now()
139
136
 
140
- const serialized = encodeCacheText(JSON.stringify(value), codec)
137
+ const serialized = JSON.stringify(value)
141
138
 
142
139
  database.transaction(() => {
143
140
  // Delete old tags
package/src/types.ts CHANGED
@@ -120,15 +120,4 @@ export type CacheServiceOptions = {
120
120
  * disables the cap.
121
121
  */
122
122
  maxEntries?: number
123
- /**
124
- * Value compression for the redis and sqlite strategies. Falls back to
125
- * `CACHE_COMPRESSION`, then `off`. Reads detect the stored format, so
126
- * entries written under any setting stay readable.
127
- */
128
- compression?: 'off' | 'zstd' | 'gzip'
129
- /**
130
- * Smallest serialized entry, in bytes, that gets compressed. Falls back to
131
- * `CACHE_COMPRESSION_MIN_BYTES`, then 1024.
132
- */
133
- compressionMinBytes?: number
134
123
  }
package/dist/codec.js DELETED
@@ -1,75 +0,0 @@
1
- import zlib from "node:zlib";
2
- const DEFAULT_COMPRESSION_MIN_BYTES = 1024;
3
- const MAX_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
4
- const ZSTD_LEVEL = 1;
5
- const COMPRESSIONS = ["off", "zstd", "gzip"];
6
- const warnedMessages = /* @__PURE__ */ new Set();
7
- function warnOnce(message) {
8
- if (warnedMessages.has(message)) return;
9
- warnedMessages.add(message);
10
- console.warn(message);
11
- }
12
- function isZstdAvailable() {
13
- return typeof zlib.zstdCompressSync === "function" && typeof zlib.zstdDecompressSync === "function";
14
- }
15
- function parseCacheCompression(raw) {
16
- const value = raw?.trim().toLowerCase();
17
- if (!value) return "off";
18
- if (COMPRESSIONS.includes(value)) return value;
19
- warnOnce(`[cache] Unknown CACHE_COMPRESSION value "${raw}". Compression stays off.`);
20
- return "off";
21
- }
22
- function parseCacheCompressionMinBytes(raw) {
23
- if (!raw) return void 0;
24
- const parsed = Number.parseInt(raw, 10);
25
- return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
26
- }
27
- function resolveCacheCodecConfig(options) {
28
- const minBytes = options?.compressionMinBytes && options.compressionMinBytes > 0 ? options.compressionMinBytes : DEFAULT_COMPRESSION_MIN_BYTES;
29
- let compression = options?.compression ?? "off";
30
- if (compression === "zstd" && !isZstdAvailable()) {
31
- warnOnce("[cache] zstd is not available in this Node.js runtime. Falling back to gzip cache compression.");
32
- compression = "gzip";
33
- }
34
- return { compression, minBytes };
35
- }
36
- function compress(input, compression) {
37
- if (compression === "zstd") {
38
- return zlib.zstdCompressSync(input, { params: { [zlib.constants.ZSTD_c_compressionLevel]: ZSTD_LEVEL } });
39
- }
40
- return zlib.gzipSync(input);
41
- }
42
- function encodeCacheText(json, config) {
43
- if (config.compression === "off") return json;
44
- const size = Buffer.byteLength(json);
45
- if (size < config.minBytes || size > MAX_DECOMPRESSED_BYTES) return json;
46
- const compressed = compress(Buffer.from(json, "utf8"), config.compression);
47
- return compressed.length < size ? compressed : json;
48
- }
49
- function isZstdFrame(raw) {
50
- return raw.length >= 4 && raw[0] === 40 && raw[1] === 181 && raw[2] === 47 && raw[3] === 253;
51
- }
52
- function isGzipStream(raw) {
53
- return raw.length >= 2 && raw[0] === 31 && raw[1] === 139;
54
- }
55
- function decodeCacheText(raw) {
56
- if (typeof raw === "string") return raw;
57
- if (isZstdFrame(raw)) {
58
- return zlib.zstdDecompressSync(raw, { maxOutputLength: MAX_DECOMPRESSED_BYTES }).toString("utf8");
59
- }
60
- if (isGzipStream(raw)) {
61
- return zlib.gunzipSync(raw, { maxOutputLength: MAX_DECOMPRESSED_BYTES }).toString("utf8");
62
- }
63
- return raw.toString("utf8");
64
- }
65
- export {
66
- DEFAULT_COMPRESSION_MIN_BYTES,
67
- MAX_DECOMPRESSED_BYTES,
68
- decodeCacheText,
69
- encodeCacheText,
70
- isZstdAvailable,
71
- parseCacheCompression,
72
- parseCacheCompressionMinBytes,
73
- resolveCacheCodecConfig
74
- };
75
- //# sourceMappingURL=codec.js.map
package/dist/codec.js.map DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/codec.ts"],
4
- "sourcesContent": ["import zlib from 'node:zlib'\n\nexport type CacheCompression = 'off' | 'zstd' | 'gzip'\n\nexport type CacheCodecConfig = {\n compression: CacheCompression\n minBytes: number\n}\n\nexport type CacheCodecOptions = {\n compression?: CacheCompression\n compressionMinBytes?: number\n}\n\nexport const DEFAULT_COMPRESSION_MIN_BYTES = 1024\n\n/** Upper bound for one decompressed entry; larger JSON is never compressed, so no legitimate entry can hit it. */\nexport const MAX_DECOMPRESSED_BYTES = 64 * 1024 * 1024\n\nconst ZSTD_LEVEL = 1\nconst COMPRESSIONS: CacheCompression[] = ['off', 'zstd', 'gzip']\n\nconst warnedMessages = new Set<string>()\n\nfunction warnOnce(message: string): void {\n if (warnedMessages.has(message)) return\n warnedMessages.add(message)\n console.warn(message)\n}\n\nexport function isZstdAvailable(): boolean {\n return typeof zlib.zstdCompressSync === 'function' && typeof zlib.zstdDecompressSync === 'function'\n}\n\nexport function parseCacheCompression(raw: string | undefined): CacheCompression {\n const value = raw?.trim().toLowerCase()\n if (!value) return 'off'\n if (COMPRESSIONS.includes(value as CacheCompression)) return value as CacheCompression\n warnOnce(`[cache] Unknown CACHE_COMPRESSION value \"${raw}\". Compression stays off.`)\n return 'off'\n}\n\nexport function parseCacheCompressionMinBytes(raw: string | undefined): number | undefined {\n if (!raw) return undefined\n const parsed = Number.parseInt(raw, 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined\n}\n\nexport function resolveCacheCodecConfig(options?: CacheCodecOptions): CacheCodecConfig {\n const minBytes = options?.compressionMinBytes && options.compressionMinBytes > 0\n ? options.compressionMinBytes\n : DEFAULT_COMPRESSION_MIN_BYTES\n let compression = options?.compression ?? 'off'\n if (compression === 'zstd' && !isZstdAvailable()) {\n warnOnce('[cache] zstd is not available in this Node.js runtime. Falling back to gzip cache compression.')\n compression = 'gzip'\n }\n return { compression, minBytes }\n}\n\nfunction compress(input: Buffer, compression: Exclude<CacheCompression, 'off'>): Buffer {\n if (compression === 'zstd') {\n return zlib.zstdCompressSync(input, { params: { [zlib.constants.ZSTD_c_compressionLevel]: ZSTD_LEVEL } })\n }\n return zlib.gzipSync(input)\n}\n\nexport function encodeCacheText(json: string, config: CacheCodecConfig): string | Buffer {\n if (config.compression === 'off') return json\n const size = Buffer.byteLength(json)\n if (size < config.minBytes || size > MAX_DECOMPRESSED_BYTES) return json\n const compressed = compress(Buffer.from(json, 'utf8'), config.compression)\n return compressed.length < size ? compressed : json\n}\n\nfunction isZstdFrame(raw: Buffer): boolean {\n return raw.length >= 4 && raw[0] === 0x28 && raw[1] === 0xb5 && raw[2] === 0x2f && raw[3] === 0xfd\n}\n\nfunction isGzipStream(raw: Buffer): boolean {\n return raw.length >= 2 && raw[0] === 0x1f && raw[1] === 0x8b\n}\n\n/** Detects the format from the stored bytes, so entries stay readable whatever CACHE_COMPRESSION is set to now. */\nexport function decodeCacheText(raw: string | Buffer): string {\n if (typeof raw === 'string') return raw\n if (isZstdFrame(raw)) {\n return zlib.zstdDecompressSync(raw, { maxOutputLength: MAX_DECOMPRESSED_BYTES }).toString('utf8')\n }\n if (isGzipStream(raw)) {\n return zlib.gunzipSync(raw, { maxOutputLength: MAX_DECOMPRESSED_BYTES }).toString('utf8')\n }\n return raw.toString('utf8')\n}\n"],
5
- "mappings": "AAAA,OAAO,UAAU;AAcV,MAAM,gCAAgC;AAGtC,MAAM,yBAAyB,KAAK,OAAO;AAElD,MAAM,aAAa;AACnB,MAAM,eAAmC,CAAC,OAAO,QAAQ,MAAM;AAE/D,MAAM,iBAAiB,oBAAI,IAAY;AAEvC,SAAS,SAAS,SAAuB;AACvC,MAAI,eAAe,IAAI,OAAO,EAAG;AACjC,iBAAe,IAAI,OAAO;AAC1B,UAAQ,KAAK,OAAO;AACtB;AAEO,SAAS,kBAA2B;AACzC,SAAO,OAAO,KAAK,qBAAqB,cAAc,OAAO,KAAK,uBAAuB;AAC3F;AAEO,SAAS,sBAAsB,KAA2C;AAC/E,QAAM,QAAQ,KAAK,KAAK,EAAE,YAAY;AACtC,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,aAAa,SAAS,KAAyB,EAAG,QAAO;AAC7D,WAAS,4CAA4C,GAAG,2BAA2B;AACnF,SAAO;AACT;AAEO,SAAS,8BAA8B,KAA6C;AACzF,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,SAAS,OAAO,SAAS,KAAK,EAAE;AACtC,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAEO,SAAS,wBAAwB,SAA+C;AACrF,QAAM,WAAW,SAAS,uBAAuB,QAAQ,sBAAsB,IAC3E,QAAQ,sBACR;AACJ,MAAI,cAAc,SAAS,eAAe;AAC1C,MAAI,gBAAgB,UAAU,CAAC,gBAAgB,GAAG;AAChD,aAAS,gGAAgG;AACzG,kBAAc;AAAA,EAChB;AACA,SAAO,EAAE,aAAa,SAAS;AACjC;AAEA,SAAS,SAAS,OAAe,aAAuD;AACtF,MAAI,gBAAgB,QAAQ;AAC1B,WAAO,KAAK,iBAAiB,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,UAAU,uBAAuB,GAAG,WAAW,EAAE,CAAC;AAAA,EAC1G;AACA,SAAO,KAAK,SAAS,KAAK;AAC5B;AAEO,SAAS,gBAAgB,MAAc,QAA2C;AACvF,MAAI,OAAO,gBAAgB,MAAO,QAAO;AACzC,QAAM,OAAO,OAAO,WAAW,IAAI;AACnC,MAAI,OAAO,OAAO,YAAY,OAAO,uBAAwB,QAAO;AACpE,QAAM,aAAa,SAAS,OAAO,KAAK,MAAM,MAAM,GAAG,OAAO,WAAW;AACzE,SAAO,WAAW,SAAS,OAAO,aAAa;AACjD;AAEA,SAAS,YAAY,KAAsB;AACzC,SAAO,IAAI,UAAU,KAAK,IAAI,CAAC,MAAM,MAAQ,IAAI,CAAC,MAAM,OAAQ,IAAI,CAAC,MAAM,MAAQ,IAAI,CAAC,MAAM;AAChG;AAEA,SAAS,aAAa,KAAsB;AAC1C,SAAO,IAAI,UAAU,KAAK,IAAI,CAAC,MAAM,MAAQ,IAAI,CAAC,MAAM;AAC1D;AAGO,SAAS,gBAAgB,KAA8B;AAC5D,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,YAAY,GAAG,GAAG;AACpB,WAAO,KAAK,mBAAmB,KAAK,EAAE,iBAAiB,uBAAuB,CAAC,EAAE,SAAS,MAAM;AAAA,EAClG;AACA,MAAI,aAAa,GAAG,GAAG;AACrB,WAAO,KAAK,WAAW,KAAK,EAAE,iBAAiB,uBAAuB,CAAC,EAAE,SAAS,MAAM;AAAA,EAC1F;AACA,SAAO,IAAI,SAAS,MAAM;AAC5B;",
6
- "names": []
7
- }