layercache 1.0.2 → 1.2.0

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dw/layercache)](https://www.npmjs.com/package/layercache)
7
7
  [![license](https://img.shields.io/npm/l/layercache)](LICENSE)
8
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-first-blue)](https://www.typescriptlang.org/)
9
- [![test coverage](https://img.shields.io/badge/tests-49%20passing-brightgreen)](https://github.com/flyingsquirrel0419/layercache)
9
+ [![test coverage](https://img.shields.io/badge/tests-132%20passing-brightgreen)](https://github.com/flyingsquirrel0419/layercache)
10
10
 
11
11
  ```
12
12
  L1 hit ~0.01 ms ← served from memory, zero network
@@ -58,13 +58,17 @@ On a hit, the value is returned from the fastest layer that has it, and automati
58
58
  - **Event hooks** — `EventEmitter`-based events for hits, misses, stale serves, errors, and more
59
59
  - **Graceful degradation** — skip a failing layer for a configurable retry window
60
60
  - **Circuit breaker** — per-key or global; opens after N failures, recovers after cooldown
61
- - **Compression** — transparent gzip/brotli in `RedisLayer` with a byte threshold
62
- - **Metrics & stats** — per-layer hit/miss counters, circuit-breaker trips, degraded operations; HTTP stats handler
61
+ - **Compression** — transparent async gzip/brotli in `RedisLayer` (non-blocking) with a byte threshold
62
+ - **Metrics & stats** — per-layer hit/miss counters, **per-layer latency tracking**, circuit-breaker trips, degraded operations; HTTP stats handler
63
63
  - **Persistence** — `exportState` / `importState` for in-process snapshots; `persistToFile` / `restoreFromFile` for disk
64
64
  - **Admin CLI** — `layercache stats | keys | invalidate` against any Redis URL
65
- - **Framework integrations** — Fastify plugin, tRPC middleware, GraphQL resolver wrapper
65
+ - **Framework integrations** — Express middleware, Fastify plugin, tRPC middleware, GraphQL resolver wrapper
66
66
  - **MessagePack serializer** — drop-in replacement for lower Redis memory usage
67
- - **NestJS module** — `CacheStackModule.forRoot(...)` with `@InjectCacheStack()`
67
+ - **NestJS module** — `CacheStackModule.forRoot(...)` and `forRootAsync(...)` with `@InjectCacheStack()`
68
+ - **`getOrThrow()`** — throws `CacheMissError` instead of returning `null`, for strict use cases
69
+ - **`inspect()`** — debug a key: see which layers hold it, remaining TTLs, tags, and staleness state
70
+ - **Conditional caching** — `shouldCache` predicate to skip caching specific fetcher results
71
+ - **Nested namespaces** — `namespace('a').namespace('b')` for composable key prefixes
68
72
  - **Custom layers** — implement the 5-method `CacheLayer` interface to plug in Memcached, DynamoDB, or anything else
69
73
  - **ESM + CJS** — works with both module systems, Node.js ≥ 18
70
74
 
@@ -250,6 +254,55 @@ const posts = cache.namespace('posts')
250
254
 
251
255
  await users.set('123', userData) // stored as "users:123"
252
256
  await users.clear() // only deletes "users:*"
257
+
258
+ // Nested namespaces
259
+ const tenant = cache.namespace('tenant:abc')
260
+ const posts = tenant.namespace('posts')
261
+ await posts.set('1', postData) // stored as "tenant:abc:posts:1"
262
+ ```
263
+
264
+ ### `cache.getOrThrow<T>(key, fetcher?, options?): Promise<T>`
265
+
266
+ Like `get()`, but throws `CacheMissError` instead of returning `null`. Useful when you know the value must exist (e.g. after a warm-up).
267
+
268
+ ```ts
269
+ import { CacheMissError } from 'layercache'
270
+
271
+ try {
272
+ const config = await cache.getOrThrow<Config>('app:config')
273
+ } catch (err) {
274
+ if (err instanceof CacheMissError) {
275
+ console.error(`Missing key: ${err.key}`)
276
+ }
277
+ }
278
+ ```
279
+
280
+ ### `cache.inspect(key): Promise<CacheInspectResult | null>`
281
+
282
+ Returns detailed metadata about a cache key for debugging. Returns `null` if the key is not in any layer.
283
+
284
+ ```ts
285
+ const info = await cache.inspect('user:123')
286
+ // {
287
+ // key: 'user:123',
288
+ // foundInLayers: ['memory', 'redis'],
289
+ // freshTtlSeconds: 45,
290
+ // staleTtlSeconds: 75,
291
+ // errorTtlSeconds: 345,
292
+ // isStale: false,
293
+ // tags: ['user', 'user:123']
294
+ // }
295
+ ```
296
+
297
+ ### Conditional caching with `shouldCache`
298
+
299
+ Skip caching specific results without affecting the return value:
300
+
301
+ ```ts
302
+ const data = await cache.get('api:response', fetchFromApi, {
303
+ shouldCache: (value) => (value as any).status === 200
304
+ })
305
+ // If fetchFromApi returns { status: 500 }, the value is returned but NOT cached
253
306
  ```
254
307
 
255
308
  ---
@@ -591,6 +644,26 @@ cache.on('error', ({ event, context }) => logger.error(event, context))
591
644
 
592
645
  ## Framework integrations
593
646
 
647
+ ### Express
648
+
649
+ ```ts
650
+ import { CacheStack, MemoryLayer, createExpressCacheMiddleware } from 'layercache'
651
+
652
+ const cache = new CacheStack([new MemoryLayer({ ttl: 60 })])
653
+
654
+ // Automatically caches GET responses as JSON
655
+ app.get('/api/users', createExpressCacheMiddleware(cache, { ttl: 30 }), (req, res) => {
656
+ res.json(await db.getUsers())
657
+ })
658
+
659
+ // Custom key resolver + tag support
660
+ app.get('/api/user/:id', createExpressCacheMiddleware(cache, {
661
+ keyResolver: (req) => `user:${req.url}`,
662
+ tags: ['users'],
663
+ ttl: 60
664
+ }), handler)
665
+ ```
666
+
594
667
  ### tRPC
595
668
 
596
669
  ```ts
@@ -699,6 +772,25 @@ import { CacheStackModule } from '@cachestack/nestjs'
699
772
  export class AppModule {}
700
773
  ```
701
774
 
775
+ Async configuration (resolve dependencies from DI):
776
+
777
+ ```ts
778
+ @Module({
779
+ imports: [
780
+ CacheStackModule.forRootAsync({
781
+ inject: [ConfigService],
782
+ useFactory: (config: ConfigService) => ({
783
+ layers: [
784
+ new MemoryLayer({ ttl: 20 }),
785
+ new RedisLayer({ client: new Redis(config.get('REDIS_URL')), ttl: 300 })
786
+ ]
787
+ })
788
+ })
789
+ ]
790
+ })
791
+ export class AppModule {}
792
+ ```
793
+
702
794
  ```ts
703
795
  // your.service.ts
704
796
  import { InjectCacheStack } from '@cachestack/nestjs'
@@ -1,5 +1,5 @@
1
- import Redis from 'ioredis-mock'
2
1
  import { performance } from 'node:perf_hooks'
2
+ import Redis from 'ioredis-mock'
3
3
  import { CacheStack, MemoryLayer, RedisLayer } from '../src'
4
4
 
5
5
  async function main(): Promise<void> {
@@ -3,10 +3,7 @@ import { CacheStack, MemoryLayer, RedisLayer } from '../src'
3
3
 
4
4
  async function main(): Promise<void> {
5
5
  const redis = new Redis()
6
- const cache = new CacheStack([
7
- new MemoryLayer({ ttl: 60 }),
8
- new RedisLayer({ client: redis, ttl: 300 })
9
- ])
6
+ const cache = new CacheStack([new MemoryLayer({ ttl: 60 }), new RedisLayer({ client: redis, ttl: 300 })])
10
7
 
11
8
  let executions = 0
12
9
 
@@ -1,9 +1,38 @@
1
1
  // src/invalidation/PatternMatcher.ts
2
- var PatternMatcher = class {
2
+ var PatternMatcher = class _PatternMatcher {
3
+ /**
4
+ * Tests whether a glob-style pattern matches a value.
5
+ * Supports `*` (any sequence of characters) and `?` (any single character).
6
+ * Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
7
+ */
3
8
  static matches(pattern, value) {
4
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
5
- const regex = new RegExp(`^${escaped.replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
6
- return regex.test(value);
9
+ return _PatternMatcher.matchLinear(pattern, value);
10
+ }
11
+ /**
12
+ * Linear-time glob matching using dynamic programming.
13
+ * Avoids catastrophic backtracking that RegExp-based glob matching can cause.
14
+ */
15
+ static matchLinear(pattern, value) {
16
+ const m = pattern.length;
17
+ const n = value.length;
18
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
19
+ dp[0][0] = true;
20
+ for (let i = 1; i <= m; i++) {
21
+ if (pattern[i - 1] === "*") {
22
+ dp[i][0] = dp[i - 1]?.[0];
23
+ }
24
+ }
25
+ for (let i = 1; i <= m; i++) {
26
+ for (let j = 1; j <= n; j++) {
27
+ const pc = pattern[i - 1];
28
+ if (pc === "*") {
29
+ dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
30
+ } else if (pc === "?" || pc === value[j - 1]) {
31
+ dp[i][j] = dp[i - 1]?.[j - 1];
32
+ }
33
+ }
34
+ }
35
+ return dp[m]?.[n];
7
36
  }
8
37
  };
9
38
 
@@ -51,6 +80,9 @@ var RedisTagIndex = class {
51
80
  async keysForTag(tag) {
52
81
  return this.client.smembers(this.tagKeysKey(tag));
53
82
  }
83
+ async tagsForKey(key) {
84
+ return this.client.smembers(this.keyTagsKey(key));
85
+ }
54
86
  async matchPattern(pattern) {
55
87
  const matches = [];
56
88
  let cursor = "0";
package/dist/cli.cjs CHANGED
@@ -37,11 +37,40 @@ module.exports = __toCommonJS(cli_exports);
37
37
  var import_ioredis = __toESM(require("ioredis"), 1);
38
38
 
39
39
  // src/invalidation/PatternMatcher.ts
40
- var PatternMatcher = class {
40
+ var PatternMatcher = class _PatternMatcher {
41
+ /**
42
+ * Tests whether a glob-style pattern matches a value.
43
+ * Supports `*` (any sequence of characters) and `?` (any single character).
44
+ * Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
45
+ */
41
46
  static matches(pattern, value) {
42
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
43
- const regex = new RegExp(`^${escaped.replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
44
- return regex.test(value);
47
+ return _PatternMatcher.matchLinear(pattern, value);
48
+ }
49
+ /**
50
+ * Linear-time glob matching using dynamic programming.
51
+ * Avoids catastrophic backtracking that RegExp-based glob matching can cause.
52
+ */
53
+ static matchLinear(pattern, value) {
54
+ const m = pattern.length;
55
+ const n = value.length;
56
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
57
+ dp[0][0] = true;
58
+ for (let i = 1; i <= m; i++) {
59
+ if (pattern[i - 1] === "*") {
60
+ dp[i][0] = dp[i - 1]?.[0];
61
+ }
62
+ }
63
+ for (let i = 1; i <= m; i++) {
64
+ for (let j = 1; j <= n; j++) {
65
+ const pc = pattern[i - 1];
66
+ if (pc === "*") {
67
+ dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
68
+ } else if (pc === "?" || pc === value[j - 1]) {
69
+ dp[i][j] = dp[i - 1]?.[j - 1];
70
+ }
71
+ }
72
+ }
73
+ return dp[m]?.[n];
45
74
  }
46
75
  };
47
76
 
@@ -89,6 +118,9 @@ var RedisTagIndex = class {
89
118
  async keysForTag(tag) {
90
119
  return this.client.smembers(this.tagKeysKey(tag));
91
120
  }
121
+ async tagsForKey(key) {
122
+ return this.client.smembers(this.keyTagsKey(key));
123
+ }
92
124
  async matchPattern(pattern) {
93
125
  const matches = [];
94
126
  let cursor = "0";
@@ -136,6 +168,7 @@ var RedisTagIndex = class {
136
168
  };
137
169
 
138
170
  // src/cli.ts
171
+ var CONNECT_TIMEOUT_MS = 5e3;
139
172
  async function main(argv = process.argv.slice(2)) {
140
173
  const args = parseArgs(argv);
141
174
  if (!args.command || !args.redisUrl) {
@@ -143,8 +176,25 @@ async function main(argv = process.argv.slice(2)) {
143
176
  process.exitCode = 1;
144
177
  return;
145
178
  }
146
- const redis = new import_ioredis.default(args.redisUrl);
179
+ const redisUrl = validateRedisUrl(args.redisUrl);
180
+ if (!redisUrl) {
181
+ process.stderr.write(
182
+ `Error: invalid Redis URL "${args.redisUrl}". Expected format: redis://[user:password@]host[:port][/db]
183
+ `
184
+ );
185
+ process.exitCode = 1;
186
+ return;
187
+ }
188
+ const redis = new import_ioredis.default(redisUrl, {
189
+ connectTimeout: CONNECT_TIMEOUT_MS,
190
+ lazyConnect: true,
191
+ enableReadyCheck: false
192
+ });
147
193
  try {
194
+ await redis.connect().catch((error) => {
195
+ const message = error instanceof Error ? error.message : String(error);
196
+ throw new Error(`Failed to connect to Redis at "${redisUrl}": ${message}`);
197
+ });
148
198
  if (args.command === "stats") {
149
199
  const keys = await scanKeys(redis, args.pattern ?? "*");
150
200
  process.stdout.write(`${JSON.stringify({ totalKeys: keys.length, pattern: args.pattern ?? "*" }, null, 2)}
@@ -153,8 +203,10 @@ async function main(argv = process.argv.slice(2)) {
153
203
  }
154
204
  if (args.command === "keys") {
155
205
  const keys = await scanKeys(redis, args.pattern ?? "*");
156
- process.stdout.write(`${keys.join("\n")}
206
+ if (keys.length > 0) {
207
+ process.stdout.write(`${keys.join("\n")}
157
208
  `);
209
+ }
158
210
  return;
159
211
  }
160
212
  if (args.command === "invalidate") {
@@ -162,7 +214,7 @@ async function main(argv = process.argv.slice(2)) {
162
214
  const tagIndex = new RedisTagIndex({ client: redis, prefix: args.tagIndexPrefix ?? "layercache:tag-index" });
163
215
  const keys2 = await tagIndex.keysForTag(args.tag);
164
216
  if (keys2.length > 0) {
165
- await redis.del(...keys2);
217
+ await batchDelete(redis, keys2);
166
218
  }
167
219
  process.stdout.write(`${JSON.stringify({ deletedKeys: keys2.length, tag: args.tag }, null, 2)}
168
220
  `);
@@ -170,7 +222,7 @@ async function main(argv = process.argv.slice(2)) {
170
222
  }
171
223
  const keys = await scanKeys(redis, args.pattern ?? "*");
172
224
  if (keys.length > 0) {
173
- await redis.del(...keys);
225
+ await batchDelete(redis, keys);
174
226
  }
175
227
  process.stdout.write(`${JSON.stringify({ deletedKeys: keys.length, pattern: args.pattern ?? "*" }, null, 2)}
176
228
  `);
@@ -178,10 +230,29 @@ async function main(argv = process.argv.slice(2)) {
178
230
  }
179
231
  printUsage();
180
232
  process.exitCode = 1;
233
+ } catch (error) {
234
+ const message = error instanceof Error ? error.message : String(error);
235
+ process.stderr.write(`Error: ${message}
236
+ `);
237
+ process.exitCode = 1;
181
238
  } finally {
182
239
  redis.disconnect();
183
240
  }
184
241
  }
242
+ function validateRedisUrl(url) {
243
+ try {
244
+ const parsed = new URL(url);
245
+ if (parsed.protocol !== "redis:" && parsed.protocol !== "rediss:") {
246
+ return null;
247
+ }
248
+ return url;
249
+ } catch {
250
+ if (/^[A-Za-z0-9._-]+(:\d+)?$/.test(url)) {
251
+ return url;
252
+ }
253
+ return null;
254
+ }
255
+ }
185
256
  function parseArgs(argv) {
186
257
  const [command, ...rest] = argv;
187
258
  const parsed = { command };
@@ -204,6 +275,13 @@ function parseArgs(argv) {
204
275
  }
205
276
  return parsed;
206
277
  }
278
+ var BATCH_DELETE_SIZE = 500;
279
+ async function batchDelete(redis, keys) {
280
+ for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
281
+ const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
282
+ await redis.del(...batch);
283
+ }
284
+ }
207
285
  async function scanKeys(redis, pattern) {
208
286
  const keys = [];
209
287
  let cursor = "0";
@@ -216,7 +294,7 @@ async function scanKeys(redis, pattern) {
216
294
  }
217
295
  function printUsage() {
218
296
  process.stdout.write(
219
- "Usage:\n layercache stats --redis <url> [--pattern <glob>]\n layercache keys --redis <url> [--pattern <glob>]\n layercache invalidate --redis <url> [--pattern <glob> | --tag <tag>] [--tag-index-prefix <prefix>]\n"
297
+ "Usage:\n layercache stats --redis <url> [--pattern <glob>]\n layercache keys --redis <url> [--pattern <glob>]\n layercache invalidate --redis <url> [--pattern <glob> | --tag <tag>] [--tag-index-prefix <prefix>]\n\nOptions:\n --redis <url> Redis connection URL (e.g. redis://localhost:6379)\n --pattern <glob> Glob pattern to filter keys (default: *)\n --tag <tag> Invalidate by tag name\n --tag-index-prefix <prefix> Redis key prefix for tag index (default: layercache:tag-index)\n"
220
298
  );
221
299
  }
222
300
  if (process.argv[1]?.includes("cli.")) {
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  RedisTagIndex
4
- } from "./chunk-IILH5XTS.js";
4
+ } from "./chunk-BWM4MU2X.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import Redis from "ioredis";
8
+ var CONNECT_TIMEOUT_MS = 5e3;
8
9
  async function main(argv = process.argv.slice(2)) {
9
10
  const args = parseArgs(argv);
10
11
  if (!args.command || !args.redisUrl) {
@@ -12,8 +13,25 @@ async function main(argv = process.argv.slice(2)) {
12
13
  process.exitCode = 1;
13
14
  return;
14
15
  }
15
- const redis = new Redis(args.redisUrl);
16
+ const redisUrl = validateRedisUrl(args.redisUrl);
17
+ if (!redisUrl) {
18
+ process.stderr.write(
19
+ `Error: invalid Redis URL "${args.redisUrl}". Expected format: redis://[user:password@]host[:port][/db]
20
+ `
21
+ );
22
+ process.exitCode = 1;
23
+ return;
24
+ }
25
+ const redis = new Redis(redisUrl, {
26
+ connectTimeout: CONNECT_TIMEOUT_MS,
27
+ lazyConnect: true,
28
+ enableReadyCheck: false
29
+ });
16
30
  try {
31
+ await redis.connect().catch((error) => {
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ throw new Error(`Failed to connect to Redis at "${redisUrl}": ${message}`);
34
+ });
17
35
  if (args.command === "stats") {
18
36
  const keys = await scanKeys(redis, args.pattern ?? "*");
19
37
  process.stdout.write(`${JSON.stringify({ totalKeys: keys.length, pattern: args.pattern ?? "*" }, null, 2)}
@@ -22,8 +40,10 @@ async function main(argv = process.argv.slice(2)) {
22
40
  }
23
41
  if (args.command === "keys") {
24
42
  const keys = await scanKeys(redis, args.pattern ?? "*");
25
- process.stdout.write(`${keys.join("\n")}
43
+ if (keys.length > 0) {
44
+ process.stdout.write(`${keys.join("\n")}
26
45
  `);
46
+ }
27
47
  return;
28
48
  }
29
49
  if (args.command === "invalidate") {
@@ -31,7 +51,7 @@ async function main(argv = process.argv.slice(2)) {
31
51
  const tagIndex = new RedisTagIndex({ client: redis, prefix: args.tagIndexPrefix ?? "layercache:tag-index" });
32
52
  const keys2 = await tagIndex.keysForTag(args.tag);
33
53
  if (keys2.length > 0) {
34
- await redis.del(...keys2);
54
+ await batchDelete(redis, keys2);
35
55
  }
36
56
  process.stdout.write(`${JSON.stringify({ deletedKeys: keys2.length, tag: args.tag }, null, 2)}
37
57
  `);
@@ -39,7 +59,7 @@ async function main(argv = process.argv.slice(2)) {
39
59
  }
40
60
  const keys = await scanKeys(redis, args.pattern ?? "*");
41
61
  if (keys.length > 0) {
42
- await redis.del(...keys);
62
+ await batchDelete(redis, keys);
43
63
  }
44
64
  process.stdout.write(`${JSON.stringify({ deletedKeys: keys.length, pattern: args.pattern ?? "*" }, null, 2)}
45
65
  `);
@@ -47,10 +67,29 @@ async function main(argv = process.argv.slice(2)) {
47
67
  }
48
68
  printUsage();
49
69
  process.exitCode = 1;
70
+ } catch (error) {
71
+ const message = error instanceof Error ? error.message : String(error);
72
+ process.stderr.write(`Error: ${message}
73
+ `);
74
+ process.exitCode = 1;
50
75
  } finally {
51
76
  redis.disconnect();
52
77
  }
53
78
  }
79
+ function validateRedisUrl(url) {
80
+ try {
81
+ const parsed = new URL(url);
82
+ if (parsed.protocol !== "redis:" && parsed.protocol !== "rediss:") {
83
+ return null;
84
+ }
85
+ return url;
86
+ } catch {
87
+ if (/^[A-Za-z0-9._-]+(:\d+)?$/.test(url)) {
88
+ return url;
89
+ }
90
+ return null;
91
+ }
92
+ }
54
93
  function parseArgs(argv) {
55
94
  const [command, ...rest] = argv;
56
95
  const parsed = { command };
@@ -73,6 +112,13 @@ function parseArgs(argv) {
73
112
  }
74
113
  return parsed;
75
114
  }
115
+ var BATCH_DELETE_SIZE = 500;
116
+ async function batchDelete(redis, keys) {
117
+ for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
118
+ const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
119
+ await redis.del(...batch);
120
+ }
121
+ }
76
122
  async function scanKeys(redis, pattern) {
77
123
  const keys = [];
78
124
  let cursor = "0";
@@ -85,7 +131,7 @@ async function scanKeys(redis, pattern) {
85
131
  }
86
132
  function printUsage() {
87
133
  process.stdout.write(
88
- "Usage:\n layercache stats --redis <url> [--pattern <glob>]\n layercache keys --redis <url> [--pattern <glob>]\n layercache invalidate --redis <url> [--pattern <glob> | --tag <tag>] [--tag-index-prefix <prefix>]\n"
134
+ "Usage:\n layercache stats --redis <url> [--pattern <glob>]\n layercache keys --redis <url> [--pattern <glob>]\n layercache invalidate --redis <url> [--pattern <glob> | --tag <tag>] [--tag-index-prefix <prefix>]\n\nOptions:\n --redis <url> Redis connection URL (e.g. redis://localhost:6379)\n --pattern <glob> Glob pattern to filter keys (default: *)\n --tag <tag> Invalidate by tag name\n --tag-index-prefix <prefix> Redis key prefix for tag index (default: layercache:tag-index)\n"
89
135
  );
90
136
  }
91
137
  if (process.argv[1]?.includes("cli.")) {