@open-mercato/cache 0.9.1-develop.7517.1.3d0995a8ca → 0.9.1-develop.7518.1.343d9198f1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +10 -0
- package/ENV.md +26 -0
- package/dist/codec.js +75 -0
- package/dist/codec.js.map +7 -0
- package/dist/service.js +9 -4
- package/dist/service.js.map +2 -2
- package/dist/strategies/redis.js +15 -13
- package/dist/strategies/redis.js.map +2 -2
- package/dist/strategies/sqlite.js +4 -2
- package/dist/strategies/sqlite.js.map +2 -2
- package/package.json +1 -1
- package/src/__tests__/codec.test.ts +162 -0
- package/src/__tests__/redis.compression.live.test.ts +41 -0
- package/src/__tests__/redis.strategy.test.ts +109 -9
- package/src/__tests__/service.test.ts +54 -0
- package/src/__tests__/sqlite.strategy.test.ts +53 -2
- package/src/codec.ts +94 -0
- package/src/service.ts +13 -4
- package/src/strategies/redis.ts +22 -18
- package/src/strategies/sqlite.ts +7 -4
- package/src/types.ts +11 -0
package/src/codec.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -7,6 +7,8 @@ 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'
|
|
10
12
|
|
|
11
13
|
function normalizeTenantKey(raw: string | null | undefined): string {
|
|
12
14
|
const value = typeof raw === 'string' ? raw.trim() : ''
|
|
@@ -260,7 +262,12 @@ function createConfiguredTenantAwareStrategy(options?: CacheServiceOptions): Ten
|
|
|
260
262
|
const parsedEnvMaxEntries = envMaxEntries ? Number.parseInt(envMaxEntries, 10) : undefined
|
|
261
263
|
const maxEntries = options?.maxEntries ?? (typeof parsedEnvMaxEntries === 'number' && Number.isFinite(parsedEnvMaxEntries) ? parsedEnvMaxEntries : undefined)
|
|
262
264
|
|
|
263
|
-
const
|
|
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)
|
|
264
271
|
const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl, maxEntries)
|
|
265
272
|
const reportsBoundedCounters = strategyType === 'memory'
|
|
266
273
|
|
|
@@ -276,6 +283,8 @@ function createConfiguredTenantAwareStrategy(options?: CacheServiceOptions): Ten
|
|
|
276
283
|
* - CACHE_REDIS_URL: Redis connection URL (for redis strategy)
|
|
277
284
|
* - CACHE_SQLITE_PATH: SQLite database file path (for sqlite strategy)
|
|
278
285
|
* - 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)
|
|
279
288
|
*
|
|
280
289
|
* @example
|
|
281
290
|
* const cache = createCacheService({ strategy: 'memory', defaultTtl: 60000 })
|
|
@@ -373,12 +382,12 @@ export class CacheService implements CacheStrategy {
|
|
|
373
382
|
}
|
|
374
383
|
}
|
|
375
384
|
|
|
376
|
-
function createStrategyForType(strategyType: CacheStrategyName, options?: CacheServiceOptions, defaultTtl?: number, maxEntries?: number): CacheStrategy {
|
|
385
|
+
function createStrategyForType(strategyType: CacheStrategyName, options?: CacheServiceOptions, defaultTtl?: number, maxEntries?: number, codecOptions?: CacheCodecOptions): CacheStrategy {
|
|
377
386
|
switch (strategyType) {
|
|
378
387
|
case 'redis':
|
|
379
|
-
return createRedisStrategy(options?.redisUrl, { defaultTtl })
|
|
388
|
+
return createRedisStrategy(options?.redisUrl, { defaultTtl, ...codecOptions })
|
|
380
389
|
case 'sqlite':
|
|
381
|
-
return createSqliteStrategy(options?.sqlitePath, { defaultTtl })
|
|
390
|
+
return createSqliteStrategy(options?.sqlitePath, { defaultTtl, ...codecOptions })
|
|
382
391
|
case 'jsonfile':
|
|
383
392
|
return createJsonFileStrategy(options?.jsonFilePath, { defaultTtl })
|
|
384
393
|
case 'memory':
|
package/src/strategies/redis.ts
CHANGED
|
@@ -3,10 +3,12 @@ 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'
|
|
6
8
|
|
|
7
9
|
type RedisPipeline = {
|
|
8
|
-
set(key: string, value: string): RedisPipeline
|
|
9
|
-
setex(key: string, ttlSeconds: number, value: string): RedisPipeline
|
|
10
|
+
set(key: string, value: string | Buffer): RedisPipeline
|
|
11
|
+
setex(key: string, ttlSeconds: number, value: string | Buffer): RedisPipeline
|
|
10
12
|
sadd(key: string, value: string): RedisPipeline
|
|
11
13
|
srem(key: string, member: string): RedisPipeline
|
|
12
14
|
del(key: string): RedisPipeline
|
|
@@ -17,8 +19,9 @@ type RedisPipeline = {
|
|
|
17
19
|
type RedisClient = {
|
|
18
20
|
ping(): Promise<string>
|
|
19
21
|
get(key: string): Promise<string | null>
|
|
20
|
-
|
|
21
|
-
|
|
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>
|
|
22
25
|
del(key: string): Promise<unknown>
|
|
23
26
|
exists(key: string): Promise<number>
|
|
24
27
|
keys(pattern: string): Promise<string[]>
|
|
@@ -186,8 +189,9 @@ async function releaseRedisEntry(key: string, entry: RedisRegistryEntry): Promis
|
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
191
|
|
|
189
|
-
export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?: number }): CacheStrategy {
|
|
192
|
+
export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?: number } & CacheCodecOptions): CacheStrategy {
|
|
190
193
|
const defaultTtl = options?.defaultTtl
|
|
194
|
+
const codec = resolveCacheCodecConfig(options)
|
|
191
195
|
const keyPrefix = 'cache:'
|
|
192
196
|
const tagPrefix = 'tag:'
|
|
193
197
|
const connectionUrl = redisUrl || getRedisUrlOrThrow('CACHE')
|
|
@@ -217,12 +221,12 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
|
|
|
217
221
|
const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {
|
|
218
222
|
const client = await getRedisClient()
|
|
219
223
|
const cacheKey = getCacheKey(key)
|
|
220
|
-
const data = await client.
|
|
224
|
+
const data = await client.getBuffer(cacheKey)
|
|
221
225
|
|
|
222
226
|
if (!data) return null
|
|
223
227
|
|
|
224
228
|
try {
|
|
225
|
-
const entry: CacheEntry = JSON.parse(data)
|
|
229
|
+
const entry: CacheEntry = JSON.parse(decodeCacheText(data))
|
|
226
230
|
|
|
227
231
|
if (isExpired(entry)) {
|
|
228
232
|
if (options?.returnExpired) {
|
|
@@ -246,10 +250,10 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
|
|
|
246
250
|
const cacheKey = getCacheKey(key)
|
|
247
251
|
|
|
248
252
|
// Remove old entry from tag index if it exists
|
|
249
|
-
const oldData = await client.
|
|
253
|
+
const oldData = await client.getBuffer(cacheKey)
|
|
250
254
|
if (oldData) {
|
|
251
255
|
try {
|
|
252
|
-
const oldEntry: CacheEntry = JSON.parse(oldData)
|
|
256
|
+
const oldEntry: CacheEntry = JSON.parse(decodeCacheText(oldData))
|
|
253
257
|
// Remove from old tags
|
|
254
258
|
const pipeline = client.pipeline()
|
|
255
259
|
for (const tag of oldEntry.tags) {
|
|
@@ -276,7 +280,7 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
|
|
|
276
280
|
const pipeline = client.pipeline()
|
|
277
281
|
|
|
278
282
|
// Store the entry
|
|
279
|
-
const serialized = JSON.stringify(entry)
|
|
283
|
+
const serialized = encodeCacheText(JSON.stringify(entry), codec)
|
|
280
284
|
if (ttl) {
|
|
281
285
|
pipeline.setex(cacheKey, Math.ceil(ttl / 1000), serialized)
|
|
282
286
|
} else {
|
|
@@ -299,11 +303,11 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
|
|
|
299
303
|
if (!exists) return false
|
|
300
304
|
|
|
301
305
|
// Check if expired
|
|
302
|
-
const data = await client.
|
|
306
|
+
const data = await client.getBuffer(cacheKey)
|
|
303
307
|
if (!data) return false
|
|
304
308
|
|
|
305
309
|
try {
|
|
306
|
-
const entry: CacheEntry = JSON.parse(data)
|
|
310
|
+
const entry: CacheEntry = JSON.parse(decodeCacheText(data))
|
|
307
311
|
if (isExpired(entry)) {
|
|
308
312
|
await deleteKey(key)
|
|
309
313
|
return false
|
|
@@ -319,11 +323,11 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
|
|
|
319
323
|
const cacheKey = getCacheKey(key)
|
|
320
324
|
|
|
321
325
|
// Get entry to remove from tag index
|
|
322
|
-
const data = await client.
|
|
326
|
+
const data = await client.getBuffer(cacheKey)
|
|
323
327
|
if (!data) return false
|
|
324
328
|
|
|
325
329
|
try {
|
|
326
|
-
const entry: CacheEntry = JSON.parse(data)
|
|
330
|
+
const entry: CacheEntry = JSON.parse(decodeCacheText(data))
|
|
327
331
|
const pipeline = client.pipeline()
|
|
328
332
|
|
|
329
333
|
// Remove from tag index
|
|
@@ -447,10 +451,10 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
|
|
|
447
451
|
|
|
448
452
|
let expired = 0
|
|
449
453
|
for (const cacheKey of cacheKeys) {
|
|
450
|
-
const data = await client.
|
|
454
|
+
const data = await client.getBuffer(cacheKey)
|
|
451
455
|
if (data) {
|
|
452
456
|
try {
|
|
453
|
-
const entry: CacheEntry = JSON.parse(data)
|
|
457
|
+
const entry: CacheEntry = JSON.parse(decodeCacheText(data))
|
|
454
458
|
if (isExpired(entry)) {
|
|
455
459
|
expired++
|
|
456
460
|
}
|
|
@@ -469,10 +473,10 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
|
|
|
469
473
|
|
|
470
474
|
let removed = 0
|
|
471
475
|
for (const cacheKey of cacheKeys) {
|
|
472
|
-
const data = await client.
|
|
476
|
+
const data = await client.getBuffer(cacheKey)
|
|
473
477
|
if (data) {
|
|
474
478
|
try {
|
|
475
|
-
const entry: CacheEntry = JSON.parse(data)
|
|
479
|
+
const entry: CacheEntry = JSON.parse(decodeCacheText(data))
|
|
476
480
|
if (isExpired(entry)) {
|
|
477
481
|
const key = cacheKey.substring(keyPrefix.length)
|
|
478
482
|
await deleteKey(key)
|
package/src/strategies/sqlite.ts
CHANGED
|
@@ -5,6 +5,8 @@ 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'
|
|
8
10
|
|
|
9
11
|
type SqliteStatement<TResult = unknown> = {
|
|
10
12
|
get(...args: unknown[]): TResult | undefined
|
|
@@ -23,7 +25,7 @@ type SqliteDatabase = {
|
|
|
23
25
|
|
|
24
26
|
type SqliteConstructor = new (file: string) => SqliteDatabase
|
|
25
27
|
type SqliteModule = SqliteConstructor | { default: SqliteConstructor }
|
|
26
|
-
type SqliteStrategyOptions = { defaultTtl?: number; databaseConstructor?: SqliteConstructor }
|
|
28
|
+
type SqliteStrategyOptions = { defaultTtl?: number; databaseConstructor?: SqliteConstructor } & CacheCodecOptions
|
|
27
29
|
|
|
28
30
|
const sqliteRequire = createRequire(path.join(process.cwd(), 'package.json'))
|
|
29
31
|
|
|
@@ -38,6 +40,7 @@ const sqliteRequire = createRequire(path.join(process.cwd(), 'package.json'))
|
|
|
38
40
|
export function createSqliteStrategy(dbPath?: string, options?: SqliteStrategyOptions): CacheStrategy {
|
|
39
41
|
let db: SqliteDatabase | null = null
|
|
40
42
|
const defaultTtl = options?.defaultTtl
|
|
43
|
+
const codec = resolveCacheCodecConfig(options)
|
|
41
44
|
const filePath = dbPath || process.env.CACHE_SQLITE_PATH || DEFAULT_SQLITE_CACHE_PATH
|
|
42
45
|
|
|
43
46
|
async function getDb(): Promise<SqliteDatabase> {
|
|
@@ -93,7 +96,7 @@ export function createSqliteStrategy(dbPath?: string, options?: SqliteStrategyOp
|
|
|
93
96
|
return Date.now() > expiresAt
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
type EntryRow = { value: string; expires_at: number | null }
|
|
99
|
+
type EntryRow = { value: string | Buffer; expires_at: number | null }
|
|
97
100
|
type ExpiresRow = { expires_at: number | null }
|
|
98
101
|
type CountRow = { count: number }
|
|
99
102
|
type KeyRow = { key: string }
|
|
@@ -107,7 +110,7 @@ export function createSqliteStrategy(dbPath?: string, options?: SqliteStrategyOp
|
|
|
107
110
|
if (!row) return null
|
|
108
111
|
|
|
109
112
|
try {
|
|
110
|
-
const value = JSON.parse(row.value) as CacheValue
|
|
113
|
+
const value = JSON.parse(decodeCacheText(row.value)) as CacheValue
|
|
111
114
|
const expiresAt = row.expires_at
|
|
112
115
|
|
|
113
116
|
if (isExpired(expiresAt)) {
|
|
@@ -134,7 +137,7 @@ export function createSqliteStrategy(dbPath?: string, options?: SqliteStrategyOp
|
|
|
134
137
|
const expiresAt = ttl ? Date.now() + ttl : null
|
|
135
138
|
const createdAt = Date.now()
|
|
136
139
|
|
|
137
|
-
const serialized = JSON.stringify(value)
|
|
140
|
+
const serialized = encodeCacheText(JSON.stringify(value), codec)
|
|
138
141
|
|
|
139
142
|
database.transaction(() => {
|
|
140
143
|
// Delete old tags
|
package/src/types.ts
CHANGED
|
@@ -120,4 +120,15 @@ 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
|
|
123
134
|
}
|