layercache 1.2.0 → 1.2.1

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/dist/cli.cjs CHANGED
@@ -36,41 +36,80 @@ __export(cli_exports, {
36
36
  module.exports = __toCommonJS(cli_exports);
37
37
  var import_ioredis = __toESM(require("ioredis"), 1);
38
38
 
39
+ // src/internal/StoredValue.ts
40
+ function isStoredValueEnvelope(value) {
41
+ return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
42
+ }
43
+ function resolveStoredValue(stored, now = Date.now()) {
44
+ if (!isStoredValueEnvelope(stored)) {
45
+ return { state: "fresh", value: stored, stored };
46
+ }
47
+ if (stored.freshUntil === null || stored.freshUntil > now) {
48
+ return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
49
+ }
50
+ if (stored.staleUntil !== null && stored.staleUntil > now) {
51
+ return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
52
+ }
53
+ if (stored.errorUntil !== null && stored.errorUntil > now) {
54
+ return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
55
+ }
56
+ return { state: "expired", value: null, stored, envelope: stored };
57
+ }
58
+ function unwrapStoredValue(stored) {
59
+ if (!isStoredValueEnvelope(stored)) {
60
+ return stored;
61
+ }
62
+ if (stored.kind === "empty") {
63
+ return null;
64
+ }
65
+ return stored.value ?? null;
66
+ }
67
+
39
68
  // src/invalidation/PatternMatcher.ts
40
69
  var PatternMatcher = class _PatternMatcher {
41
70
  /**
42
71
  * Tests whether a glob-style pattern matches a value.
43
72
  * Supports `*` (any sequence of characters) and `?` (any single character).
44
- * Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
73
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
74
+ * quadratic memory usage on long patterns/keys.
45
75
  */
46
76
  static matches(pattern, value) {
47
77
  return _PatternMatcher.matchLinear(pattern, value);
48
78
  }
49
79
  /**
50
- * Linear-time glob matching using dynamic programming.
51
- * Avoids catastrophic backtracking that RegExp-based glob matching can cause.
80
+ * Linear-time glob matching with O(1) extra memory.
52
81
  */
53
82
  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];
83
+ let patternIndex = 0;
84
+ let valueIndex = 0;
85
+ let starIndex = -1;
86
+ let backtrackValueIndex = 0;
87
+ while (valueIndex < value.length) {
88
+ const patternChar = pattern[patternIndex];
89
+ const valueChar = value[valueIndex];
90
+ if (patternChar === "*" && patternIndex < pattern.length) {
91
+ starIndex = patternIndex;
92
+ patternIndex += 1;
93
+ backtrackValueIndex = valueIndex;
94
+ continue;
61
95
  }
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
- }
96
+ if (patternChar === "?" || patternChar === valueChar) {
97
+ patternIndex += 1;
98
+ valueIndex += 1;
99
+ continue;
100
+ }
101
+ if (starIndex !== -1) {
102
+ patternIndex = starIndex + 1;
103
+ backtrackValueIndex += 1;
104
+ valueIndex = backtrackValueIndex;
105
+ continue;
71
106
  }
107
+ return false;
72
108
  }
73
- return dp[m]?.[n];
109
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
110
+ patternIndex += 1;
111
+ }
112
+ return patternIndex === pattern.length;
74
113
  }
75
114
  };
76
115
 
@@ -118,6 +157,16 @@ var RedisTagIndex = class {
118
157
  async keysForTag(tag) {
119
158
  return this.client.smembers(this.tagKeysKey(tag));
120
159
  }
160
+ async keysForPrefix(prefix) {
161
+ const matches = [];
162
+ let cursor = "0";
163
+ do {
164
+ const [nextCursor, keys] = await this.client.sscan(this.knownKeysKey(), cursor, "COUNT", this.scanCount);
165
+ cursor = nextCursor;
166
+ matches.push(...keys.filter((key) => key.startsWith(prefix)));
167
+ } while (cursor !== "0");
168
+ return matches;
169
+ }
121
170
  async tagsForKey(key) {
122
171
  return this.client.smembers(this.keyTagsKey(key));
123
172
  }
@@ -228,6 +277,31 @@ async function main(argv = process.argv.slice(2)) {
228
277
  `);
229
278
  return;
230
279
  }
280
+ if (args.command === "inspect") {
281
+ if (!args.key) {
282
+ throw new Error("inspect requires --key <key>.");
283
+ }
284
+ const payload = await redis.getBuffer(args.key);
285
+ const ttl = await redis.ttl(args.key);
286
+ const decoded = decodeInspectablePayload(payload);
287
+ process.stdout.write(
288
+ `${JSON.stringify(
289
+ {
290
+ key: args.key,
291
+ exists: payload !== null,
292
+ ttlSeconds: ttl >= 0 ? ttl : null,
293
+ sizeBytes: payload?.byteLength ?? 0,
294
+ isEnvelope: isStoredValueEnvelope(decoded),
295
+ state: payload === null ? null : resolveStoredValue(decoded).state,
296
+ preview: summarizeInspectableValue(decoded)
297
+ },
298
+ null,
299
+ 2
300
+ )}
301
+ `
302
+ );
303
+ return;
304
+ }
231
305
  printUsage();
232
306
  process.exitCode = 1;
233
307
  } catch (error) {
@@ -268,6 +342,9 @@ function parseArgs(argv) {
268
342
  } else if (token === "--tag") {
269
343
  parsed.tag = value;
270
344
  index += 1;
345
+ } else if (token === "--key") {
346
+ parsed.key = value;
347
+ index += 1;
271
348
  } else if (token === "--tag-index-prefix") {
272
349
  parsed.tagIndexPrefix = value;
273
350
  index += 1;
@@ -294,9 +371,32 @@ async function scanKeys(redis, pattern) {
294
371
  }
295
372
  function printUsage() {
296
373
  process.stdout.write(
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"
374
+ "Usage:\n layercache stats --redis <url> [--pattern <glob>]\n layercache keys --redis <url> [--pattern <glob>]\n layercache inspect --redis <url> --key <key>\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 --key <key> Exact cache key to inspect\n --tag <tag> Invalidate by tag name\n --tag-index-prefix <prefix> Redis key prefix for tag index (default: layercache:tag-index)\n"
298
375
  );
299
376
  }
377
+ function decodeInspectablePayload(payload) {
378
+ if (payload === null) {
379
+ return null;
380
+ }
381
+ const text = payload.toString("utf8");
382
+ try {
383
+ return JSON.parse(text);
384
+ } catch {
385
+ return text.length > 256 ? `${text.slice(0, 256)}...` : text;
386
+ }
387
+ }
388
+ function summarizeInspectableValue(value) {
389
+ if (isStoredValueEnvelope(value)) {
390
+ return {
391
+ kind: value.kind,
392
+ value: value.value,
393
+ freshUntil: value.freshUntil,
394
+ staleUntil: value.staleUntil,
395
+ errorUntil: value.errorUntil
396
+ };
397
+ }
398
+ return value;
399
+ }
300
400
  if (process.argv[1]?.includes("cli.")) {
301
401
  void main();
302
402
  }
package/dist/cli.js CHANGED
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  RedisTagIndex
4
- } from "./chunk-BWM4MU2X.js";
4
+ } from "./chunk-GF47Y3XR.js";
5
+ import {
6
+ isStoredValueEnvelope,
7
+ resolveStoredValue
8
+ } from "./chunk-ZMDB5KOK.js";
5
9
 
6
10
  // src/cli.ts
7
11
  import Redis from "ioredis";
@@ -65,6 +69,31 @@ async function main(argv = process.argv.slice(2)) {
65
69
  `);
66
70
  return;
67
71
  }
72
+ if (args.command === "inspect") {
73
+ if (!args.key) {
74
+ throw new Error("inspect requires --key <key>.");
75
+ }
76
+ const payload = await redis.getBuffer(args.key);
77
+ const ttl = await redis.ttl(args.key);
78
+ const decoded = decodeInspectablePayload(payload);
79
+ process.stdout.write(
80
+ `${JSON.stringify(
81
+ {
82
+ key: args.key,
83
+ exists: payload !== null,
84
+ ttlSeconds: ttl >= 0 ? ttl : null,
85
+ sizeBytes: payload?.byteLength ?? 0,
86
+ isEnvelope: isStoredValueEnvelope(decoded),
87
+ state: payload === null ? null : resolveStoredValue(decoded).state,
88
+ preview: summarizeInspectableValue(decoded)
89
+ },
90
+ null,
91
+ 2
92
+ )}
93
+ `
94
+ );
95
+ return;
96
+ }
68
97
  printUsage();
69
98
  process.exitCode = 1;
70
99
  } catch (error) {
@@ -105,6 +134,9 @@ function parseArgs(argv) {
105
134
  } else if (token === "--tag") {
106
135
  parsed.tag = value;
107
136
  index += 1;
137
+ } else if (token === "--key") {
138
+ parsed.key = value;
139
+ index += 1;
108
140
  } else if (token === "--tag-index-prefix") {
109
141
  parsed.tagIndexPrefix = value;
110
142
  index += 1;
@@ -131,9 +163,32 @@ async function scanKeys(redis, pattern) {
131
163
  }
132
164
  function printUsage() {
133
165
  process.stdout.write(
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"
166
+ "Usage:\n layercache stats --redis <url> [--pattern <glob>]\n layercache keys --redis <url> [--pattern <glob>]\n layercache inspect --redis <url> --key <key>\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 --key <key> Exact cache key to inspect\n --tag <tag> Invalidate by tag name\n --tag-index-prefix <prefix> Redis key prefix for tag index (default: layercache:tag-index)\n"
135
167
  );
136
168
  }
169
+ function decodeInspectablePayload(payload) {
170
+ if (payload === null) {
171
+ return null;
172
+ }
173
+ const text = payload.toString("utf8");
174
+ try {
175
+ return JSON.parse(text);
176
+ } catch {
177
+ return text.length > 256 ? `${text.slice(0, 256)}...` : text;
178
+ }
179
+ }
180
+ function summarizeInspectableValue(value) {
181
+ if (isStoredValueEnvelope(value)) {
182
+ return {
183
+ kind: value.kind,
184
+ value: value.value,
185
+ freshUntil: value.freshUntil,
186
+ staleUntil: value.staleUntil,
187
+ errorUntil: value.errorUntil
188
+ };
189
+ }
190
+ return value;
191
+ }
137
192
  if (process.argv[1]?.includes("cli.")) {
138
193
  void main();
139
194
  }