layercache 1.0.2 → 1.1.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.
@@ -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
 
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
 
@@ -136,6 +165,7 @@ var RedisTagIndex = class {
136
165
  };
137
166
 
138
167
  // src/cli.ts
168
+ var CONNECT_TIMEOUT_MS = 5e3;
139
169
  async function main(argv = process.argv.slice(2)) {
140
170
  const args = parseArgs(argv);
141
171
  if (!args.command || !args.redisUrl) {
@@ -143,8 +173,25 @@ async function main(argv = process.argv.slice(2)) {
143
173
  process.exitCode = 1;
144
174
  return;
145
175
  }
146
- const redis = new import_ioredis.default(args.redisUrl);
176
+ const redisUrl = validateRedisUrl(args.redisUrl);
177
+ if (!redisUrl) {
178
+ process.stderr.write(
179
+ `Error: invalid Redis URL "${args.redisUrl}". Expected format: redis://[user:password@]host[:port][/db]
180
+ `
181
+ );
182
+ process.exitCode = 1;
183
+ return;
184
+ }
185
+ const redis = new import_ioredis.default(redisUrl, {
186
+ connectTimeout: CONNECT_TIMEOUT_MS,
187
+ lazyConnect: true,
188
+ enableReadyCheck: false
189
+ });
147
190
  try {
191
+ await redis.connect().catch((error) => {
192
+ const message = error instanceof Error ? error.message : String(error);
193
+ throw new Error(`Failed to connect to Redis at "${redisUrl}": ${message}`);
194
+ });
148
195
  if (args.command === "stats") {
149
196
  const keys = await scanKeys(redis, args.pattern ?? "*");
150
197
  process.stdout.write(`${JSON.stringify({ totalKeys: keys.length, pattern: args.pattern ?? "*" }, null, 2)}
@@ -153,8 +200,10 @@ async function main(argv = process.argv.slice(2)) {
153
200
  }
154
201
  if (args.command === "keys") {
155
202
  const keys = await scanKeys(redis, args.pattern ?? "*");
156
- process.stdout.write(`${keys.join("\n")}
203
+ if (keys.length > 0) {
204
+ process.stdout.write(`${keys.join("\n")}
157
205
  `);
206
+ }
158
207
  return;
159
208
  }
160
209
  if (args.command === "invalidate") {
@@ -178,10 +227,29 @@ async function main(argv = process.argv.slice(2)) {
178
227
  }
179
228
  printUsage();
180
229
  process.exitCode = 1;
230
+ } catch (error) {
231
+ const message = error instanceof Error ? error.message : String(error);
232
+ process.stderr.write(`Error: ${message}
233
+ `);
234
+ process.exitCode = 1;
181
235
  } finally {
182
236
  redis.disconnect();
183
237
  }
184
238
  }
239
+ function validateRedisUrl(url) {
240
+ try {
241
+ const parsed = new URL(url);
242
+ if (parsed.protocol !== "redis:" && parsed.protocol !== "rediss:") {
243
+ return null;
244
+ }
245
+ return url;
246
+ } catch {
247
+ if (/^[A-Za-z0-9._-]+(:\d+)?$/.test(url)) {
248
+ return url;
249
+ }
250
+ return null;
251
+ }
252
+ }
185
253
  function parseArgs(argv) {
186
254
  const [command, ...rest] = argv;
187
255
  const parsed = { command };
@@ -216,7 +284,7 @@ async function scanKeys(redis, pattern) {
216
284
  }
217
285
  function printUsage() {
218
286
  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"
287
+ "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
288
  );
221
289
  }
222
290
  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-QUB5VZFZ.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") {
@@ -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 };
@@ -85,7 +124,7 @@ async function scanKeys(redis, pattern) {
85
124
  }
86
125
  function printUsage() {
87
126
  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"
127
+ "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
128
  );
90
129
  }
91
130
  if (process.argv[1]?.includes("cli.")) {