@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.
@@ -1,162 +0,0 @@
1
- /** @jest-environment node */
2
- import zlib from 'node:zlib'
3
- import {
4
- DEFAULT_COMPRESSION_MIN_BYTES,
5
- MAX_DECOMPRESSED_BYTES,
6
- decodeCacheText,
7
- encodeCacheText,
8
- parseCacheCompression,
9
- parseCacheCompressionMinBytes,
10
- resolveCacheCodecConfig,
11
- } from '../codec'
12
- import type { CacheCodecConfig } from '../codec'
13
-
14
- const ZSTD_MAGIC = [0x28, 0xb5, 0x2f, 0xfd]
15
- const GZIP_MAGIC = [0x1f, 0x8b]
16
-
17
- function listPayload(rows: number): string {
18
- return JSON.stringify({
19
- items: Array.from({ length: rows }, (_, index) => ({
20
- id: `00000000-0000-4000-8000-${String(index).padStart(12, '0')}`,
21
- displayName: `Contact ${index}`,
22
- primaryEmail: `contact.${index}@example.com`,
23
- status: index % 2 ? 'active' : 'lead',
24
- })),
25
- total: rows,
26
- })
27
- }
28
-
29
- function startsWith(raw: string | Buffer, magic: number[]): boolean {
30
- return Buffer.isBuffer(raw) && magic.every((byte, index) => raw[index] === byte)
31
- }
32
-
33
- describe.each([
34
- ['zstd', ZSTD_MAGIC],
35
- ['gzip', GZIP_MAGIC],
36
- ] as const)('cache codec (%s)', (compression, magic) => {
37
- const config: CacheCodecConfig = { compression, minBytes: DEFAULT_COMPRESSION_MIN_BYTES }
38
-
39
- it('round-trips a large entry through a compressed frame', () => {
40
- const json = listPayload(100)
41
- const encoded = encodeCacheText(json, config)
42
-
43
- expect(startsWith(encoded, magic)).toBe(true)
44
- expect((encoded as Buffer).length).toBeLessThan(Buffer.byteLength(json) / 4)
45
- expect(decodeCacheText(encoded)).toBe(json)
46
- })
47
-
48
- it('keeps entries below the threshold as plain JSON', () => {
49
- const json = listPayload(2)
50
- expect(Buffer.byteLength(json)).toBeLessThan(DEFAULT_COMPRESSION_MIN_BYTES)
51
-
52
- expect(encodeCacheText(json, config)).toBe(json)
53
- })
54
-
55
- it('stores the JSON when compression would not shrink it', () => {
56
- const json = JSON.stringify({ id: 'a1' })
57
-
58
- expect(encodeCacheText(json, { compression, minBytes: 1 })).toBe(json)
59
- })
60
-
61
- it('reads entries written by the other codec and by the off setting', () => {
62
- const json = listPayload(100)
63
- const other: CacheCodecConfig = { compression: compression === 'zstd' ? 'gzip' : 'zstd', minBytes: 1024 }
64
-
65
- expect(decodeCacheText(encodeCacheText(json, other))).toBe(json)
66
- expect(decodeCacheText(encodeCacheText(json, { compression: 'off', minBytes: 1024 }))).toBe(json)
67
- })
68
-
69
- it('rejects a corrupt frame', () => {
70
- const encoded = encodeCacheText(listPayload(100), config) as Buffer
71
- const corrupt = Buffer.concat([encoded.subarray(0, magic.length), Buffer.alloc(32, 0xff)])
72
-
73
- expect(() => decodeCacheText(corrupt)).toThrow()
74
- })
75
- })
76
-
77
- describe('cache codec format detection', () => {
78
- it('reads legacy JSON stored as a string or as UTF-8 bytes', () => {
79
- const json = listPayload(100)
80
-
81
- expect(decodeCacheText(json)).toBe(json)
82
- expect(decodeCacheText(Buffer.from(json, 'utf8'))).toBe(json)
83
- })
84
-
85
- it('never compresses with compression off', () => {
86
- const json = listPayload(100)
87
- expect(encodeCacheText(json, { compression: 'off', minBytes: 1 })).toBe(json)
88
- })
89
-
90
- it('skips compression above the decode cap so a legitimate entry can always be read back', () => {
91
- const encodeSpy = jest.spyOn(zlib, 'gzipSync')
92
- const json = 'x'.repeat(MAX_DECOMPRESSED_BYTES + 1)
93
-
94
- expect(encodeCacheText(json, { compression: 'gzip', minBytes: 1 })).toBe(json)
95
- expect(encodeSpy).not.toHaveBeenCalled()
96
- encodeSpy.mockRestore()
97
- })
98
-
99
- it('caps the decompressed size of a hostile frame', () => {
100
- const bomb = zlib.zstdCompressSync(Buffer.alloc(MAX_DECOMPRESSED_BYTES + 1024, 0x20))
101
-
102
- expect(() => decodeCacheText(bomb)).toThrow()
103
- })
104
- })
105
-
106
- describe('cache codec configuration', () => {
107
- afterEach(() => {
108
- jest.restoreAllMocks()
109
- })
110
-
111
- it('parses known compression values case-insensitively', () => {
112
- expect(parseCacheCompression('zstd')).toBe('zstd')
113
- expect(parseCacheCompression(' GZIP ')).toBe('gzip')
114
- expect(parseCacheCompression('off')).toBe('off')
115
- expect(parseCacheCompression(undefined)).toBe('off')
116
- expect(parseCacheCompression('')).toBe('off')
117
- })
118
-
119
- it('treats an unknown value as off and warns once', () => {
120
- const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
121
-
122
- expect(parseCacheCompression('brotli-codec-test')).toBe('off')
123
- expect(parseCacheCompression('brotli-codec-test')).toBe('off')
124
-
125
- expect(warn).toHaveBeenCalledTimes(1)
126
- })
127
-
128
- it('accepts only a positive min-bytes value', () => {
129
- expect(parseCacheCompressionMinBytes('2048')).toBe(2048)
130
- expect(parseCacheCompressionMinBytes('0')).toBeUndefined()
131
- expect(parseCacheCompressionMinBytes('-5')).toBeUndefined()
132
- expect(parseCacheCompressionMinBytes('abc')).toBeUndefined()
133
- expect(parseCacheCompressionMinBytes(undefined)).toBeUndefined()
134
- })
135
-
136
- it('defaults to off with a 1 KB threshold', () => {
137
- expect(resolveCacheCodecConfig()).toEqual({ compression: 'off', minBytes: DEFAULT_COMPRESSION_MIN_BYTES })
138
- expect(resolveCacheCodecConfig({ compression: 'zstd', compressionMinBytes: 0 })).toEqual({
139
- compression: 'zstd',
140
- minBytes: DEFAULT_COMPRESSION_MIN_BYTES,
141
- })
142
- })
143
-
144
- it('falls back to gzip when the runtime has no zstd', () => {
145
- jest.isolateModules(() => {
146
- jest.doMock('node:zlib', () => {
147
- const actual = jest.requireActual<typeof import('node:zlib')>('node:zlib')
148
- return { ...actual, zstdCompressSync: undefined, zstdDecompressSync: undefined }
149
- })
150
- const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
151
- const isolated = require('../codec') as typeof import('../codec')
152
-
153
- const config = isolated.resolveCacheCodecConfig({ compression: 'zstd' })
154
- const encoded = isolated.encodeCacheText(listPayload(100), config)
155
-
156
- expect(config.compression).toBe('gzip')
157
- expect(startsWith(encoded, GZIP_MAGIC)).toBe(true)
158
- expect(warn).toHaveBeenCalledWith(expect.stringContaining('zstd is not available'))
159
- })
160
- jest.dontMock('node:zlib')
161
- })
162
- })
@@ -1,41 +0,0 @@
1
- /** @jest-environment node */
2
- import { randomUUID } from 'node:crypto'
3
- import { createRedisStrategy } from '../strategies/redis'
4
-
5
- const redisUrl = process.env.CACHE_REDIS_URL
6
- const describeWithRedis = redisUrl ? describe : describe.skip
7
-
8
- describeWithRedis('redis strategy value compression against a live Redis', () => {
9
- const largeValue = {
10
- items: Array.from({ length: 100 }, (_, index) => ({ id: `row-${index}`, name: `Contact ${index}`, status: 'active' })),
11
- }
12
- const runId = randomUUID()
13
-
14
- it.each(['zstd', 'gzip'] as const)('round-trips %s frames and legacy JSON over the binary-safe path', async (compression) => {
15
- const legacy = createRedisStrategy(redisUrl, { compression: 'off' })
16
- const compressed = createRedisStrategy(redisUrl, { compression })
17
- const legacyKey = `compression-live:${runId}:${compression}:legacy`
18
- const compressedKey = `compression-live:${runId}:${compression}:compressed`
19
- const tag = `compression-live:${runId}:${compression}`
20
-
21
- try {
22
- await legacy.set(legacyKey, largeValue, { ttl: 60_000, tags: [tag] })
23
- await compressed.set(compressedKey, largeValue, { ttl: 60_000, tags: [tag] })
24
-
25
- expect(await compressed.get(legacyKey)).toEqual(largeValue)
26
- expect(await compressed.get(compressedKey)).toEqual(largeValue)
27
- expect(await legacy.get(compressedKey)).toEqual(largeValue)
28
- expect(await compressed.has(compressedKey)).toBe(true)
29
-
30
- await compressed.set(compressedKey, largeValue, { ttl: 60_000, tags: [`${tag}:next`] })
31
- expect(await compressed.deleteByTags([tag])).toBe(1)
32
- expect(await compressed.deleteByTags([`${tag}:next`])).toBe(1)
33
- expect(await compressed.get(compressedKey)).toBeNull()
34
- } finally {
35
- await legacy.delete(legacyKey)
36
- await compressed.delete(compressedKey)
37
- await legacy.close?.()
38
- await compressed.close?.()
39
- }
40
- })
41
- })
package/src/codec.ts DELETED
@@ -1,94 +0,0 @@
1
- import zlib from 'node:zlib'
2
-
3
- export type CacheCompression = 'off' | 'zstd' | 'gzip'
4
-
5
- export type CacheCodecConfig = {
6
- compression: CacheCompression
7
- minBytes: number
8
- }
9
-
10
- export type CacheCodecOptions = {
11
- compression?: CacheCompression
12
- compressionMinBytes?: number
13
- }
14
-
15
- export const DEFAULT_COMPRESSION_MIN_BYTES = 1024
16
-
17
- /** Upper bound for one decompressed entry; larger JSON is never compressed, so no legitimate entry can hit it. */
18
- export const MAX_DECOMPRESSED_BYTES = 64 * 1024 * 1024
19
-
20
- const ZSTD_LEVEL = 1
21
- const COMPRESSIONS: CacheCompression[] = ['off', 'zstd', 'gzip']
22
-
23
- const warnedMessages = new Set<string>()
24
-
25
- function warnOnce(message: string): void {
26
- if (warnedMessages.has(message)) return
27
- warnedMessages.add(message)
28
- console.warn(message)
29
- }
30
-
31
- export function isZstdAvailable(): boolean {
32
- return typeof zlib.zstdCompressSync === 'function' && typeof zlib.zstdDecompressSync === 'function'
33
- }
34
-
35
- export function parseCacheCompression(raw: string | undefined): CacheCompression {
36
- const value = raw?.trim().toLowerCase()
37
- if (!value) return 'off'
38
- if (COMPRESSIONS.includes(value as CacheCompression)) return value as CacheCompression
39
- warnOnce(`[cache] Unknown CACHE_COMPRESSION value "${raw}". Compression stays off.`)
40
- return 'off'
41
- }
42
-
43
- export function parseCacheCompressionMinBytes(raw: string | undefined): number | undefined {
44
- if (!raw) return undefined
45
- const parsed = Number.parseInt(raw, 10)
46
- return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined
47
- }
48
-
49
- export function resolveCacheCodecConfig(options?: CacheCodecOptions): CacheCodecConfig {
50
- const minBytes = options?.compressionMinBytes && options.compressionMinBytes > 0
51
- ? options.compressionMinBytes
52
- : DEFAULT_COMPRESSION_MIN_BYTES
53
- let compression = options?.compression ?? 'off'
54
- if (compression === 'zstd' && !isZstdAvailable()) {
55
- warnOnce('[cache] zstd is not available in this Node.js runtime. Falling back to gzip cache compression.')
56
- compression = 'gzip'
57
- }
58
- return { compression, minBytes }
59
- }
60
-
61
- function compress(input: Buffer, compression: Exclude<CacheCompression, 'off'>): Buffer {
62
- if (compression === 'zstd') {
63
- return zlib.zstdCompressSync(input, { params: { [zlib.constants.ZSTD_c_compressionLevel]: ZSTD_LEVEL } })
64
- }
65
- return zlib.gzipSync(input)
66
- }
67
-
68
- export function encodeCacheText(json: string, config: CacheCodecConfig): string | Buffer {
69
- if (config.compression === 'off') return json
70
- const size = Buffer.byteLength(json)
71
- if (size < config.minBytes || size > MAX_DECOMPRESSED_BYTES) return json
72
- const compressed = compress(Buffer.from(json, 'utf8'), config.compression)
73
- return compressed.length < size ? compressed : json
74
- }
75
-
76
- function isZstdFrame(raw: Buffer): boolean {
77
- return raw.length >= 4 && raw[0] === 0x28 && raw[1] === 0xb5 && raw[2] === 0x2f && raw[3] === 0xfd
78
- }
79
-
80
- function isGzipStream(raw: Buffer): boolean {
81
- return raw.length >= 2 && raw[0] === 0x1f && raw[1] === 0x8b
82
- }
83
-
84
- /** Detects the format from the stored bytes, so entries stay readable whatever CACHE_COMPRESSION is set to now. */
85
- export function decodeCacheText(raw: string | Buffer): string {
86
- if (typeof raw === 'string') return raw
87
- if (isZstdFrame(raw)) {
88
- return zlib.zstdDecompressSync(raw, { maxOutputLength: MAX_DECOMPRESSED_BYTES }).toString('utf8')
89
- }
90
- if (isGzipStream(raw)) {
91
- return zlib.gunzipSync(raw, { maxOutputLength: MAX_DECOMPRESSED_BYTES }).toString('utf8')
92
- }
93
- return raw.toString('utf8')
94
- }