layercache 1.2.0 → 1.2.2

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,59 +1,27 @@
1
- // src/invalidation/PatternMatcher.ts
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
- */
8
- static matches(pattern, 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];
36
- }
37
- };
1
+ import {
2
+ PatternMatcher
3
+ } from "./chunk-ZMDB5KOK.js";
38
4
 
39
5
  // src/invalidation/RedisTagIndex.ts
40
6
  var RedisTagIndex = class {
41
7
  client;
42
8
  prefix;
43
9
  scanCount;
10
+ knownKeysShards;
44
11
  constructor(options) {
45
12
  this.client = options.client;
46
13
  this.prefix = options.prefix ?? "layercache:tag-index";
47
14
  this.scanCount = options.scanCount ?? 100;
15
+ this.knownKeysShards = normalizeKnownKeysShards(options.knownKeysShards);
48
16
  }
49
17
  async touch(key) {
50
- await this.client.sadd(this.knownKeysKey(), key);
18
+ await this.client.sadd(this.knownKeysKeyFor(key), key);
51
19
  }
52
20
  async track(key, tags) {
53
21
  const keyTagsKey = this.keyTagsKey(key);
54
22
  const existingTags = await this.client.smembers(keyTagsKey);
55
23
  const pipeline = this.client.pipeline();
56
- pipeline.sadd(this.knownKeysKey(), key);
24
+ pipeline.sadd(this.knownKeysKeyFor(key), key);
57
25
  for (const tag of existingTags) {
58
26
  pipeline.srem(this.tagKeysKey(tag), key);
59
27
  }
@@ -70,7 +38,7 @@ var RedisTagIndex = class {
70
38
  const keyTagsKey = this.keyTagsKey(key);
71
39
  const existingTags = await this.client.smembers(keyTagsKey);
72
40
  const pipeline = this.client.pipeline();
73
- pipeline.srem(this.knownKeysKey(), key);
41
+ pipeline.srem(this.knownKeysKeyFor(key), key);
74
42
  pipeline.del(keyTagsKey);
75
43
  for (const tag of existingTags) {
76
44
  pipeline.srem(this.tagKeysKey(tag), key);
@@ -80,24 +48,38 @@ var RedisTagIndex = class {
80
48
  async keysForTag(tag) {
81
49
  return this.client.smembers(this.tagKeysKey(tag));
82
50
  }
51
+ async keysForPrefix(prefix) {
52
+ const matches = [];
53
+ for (const knownKeysKey of this.knownKeysKeys()) {
54
+ let cursor = "0";
55
+ do {
56
+ const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
57
+ cursor = nextCursor;
58
+ matches.push(...keys.filter((key) => key.startsWith(prefix)));
59
+ } while (cursor !== "0");
60
+ }
61
+ return matches;
62
+ }
83
63
  async tagsForKey(key) {
84
64
  return this.client.smembers(this.keyTagsKey(key));
85
65
  }
86
66
  async matchPattern(pattern) {
87
67
  const matches = [];
88
- let cursor = "0";
89
- do {
90
- const [nextCursor, keys] = await this.client.sscan(
91
- this.knownKeysKey(),
92
- cursor,
93
- "MATCH",
94
- pattern,
95
- "COUNT",
96
- this.scanCount
97
- );
98
- cursor = nextCursor;
99
- matches.push(...keys.filter((key) => PatternMatcher.matches(pattern, key)));
100
- } while (cursor !== "0");
68
+ for (const knownKeysKey of this.knownKeysKeys()) {
69
+ let cursor = "0";
70
+ do {
71
+ const [nextCursor, keys] = await this.client.sscan(
72
+ knownKeysKey,
73
+ cursor,
74
+ "MATCH",
75
+ pattern,
76
+ "COUNT",
77
+ this.scanCount
78
+ );
79
+ cursor = nextCursor;
80
+ matches.push(...keys.filter((key) => PatternMatcher.matches(pattern, key)));
81
+ } while (cursor !== "0");
82
+ }
101
83
  return matches;
102
84
  }
103
85
  async clear() {
@@ -118,8 +100,17 @@ var RedisTagIndex = class {
118
100
  } while (cursor !== "0");
119
101
  return matches;
120
102
  }
121
- knownKeysKey() {
122
- return `${this.prefix}:keys`;
103
+ knownKeysKeyFor(key) {
104
+ if (this.knownKeysShards === 1) {
105
+ return `${this.prefix}:keys`;
106
+ }
107
+ return `${this.prefix}:keys:${simpleHash(key) % this.knownKeysShards}`;
108
+ }
109
+ knownKeysKeys() {
110
+ if (this.knownKeysShards === 1) {
111
+ return [`${this.prefix}:keys`];
112
+ }
113
+ return Array.from({ length: this.knownKeysShards }, (_, index) => `${this.prefix}:keys:${index}`);
123
114
  }
124
115
  keyTagsKey(key) {
125
116
  return `${this.prefix}:key:${encodeURIComponent(key)}`;
@@ -128,8 +119,23 @@ var RedisTagIndex = class {
128
119
  return `${this.prefix}:tag:${encodeURIComponent(tag)}`;
129
120
  }
130
121
  };
122
+ function normalizeKnownKeysShards(value) {
123
+ if (value === void 0) {
124
+ return 1;
125
+ }
126
+ if (!Number.isInteger(value) || value <= 0) {
127
+ throw new Error("RedisTagIndex.knownKeysShards must be a positive integer.");
128
+ }
129
+ return value;
130
+ }
131
+ function simpleHash(value) {
132
+ let hash = 0;
133
+ for (let index = 0; index < value.length; index += 1) {
134
+ hash = hash * 31 + value.charCodeAt(index) >>> 0;
135
+ }
136
+ return hash;
137
+ }
131
138
 
132
139
  export {
133
- PatternMatcher,
134
140
  RedisTagIndex
135
141
  };
@@ -0,0 +1,159 @@
1
+ // src/internal/StoredValue.ts
2
+ function isStoredValueEnvelope(value) {
3
+ return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
4
+ }
5
+ function createStoredValueEnvelope(options) {
6
+ const now = options.now ?? Date.now();
7
+ const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
8
+ const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
9
+ const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
10
+ const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
11
+ const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
12
+ const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
13
+ return {
14
+ __layercache: 1,
15
+ kind: options.kind,
16
+ value: options.value,
17
+ freshUntil,
18
+ staleUntil,
19
+ errorUntil,
20
+ freshTtlSeconds: freshTtlSeconds ?? null,
21
+ staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
22
+ staleIfErrorSeconds: staleIfErrorSeconds ?? null
23
+ };
24
+ }
25
+ function resolveStoredValue(stored, now = Date.now()) {
26
+ if (!isStoredValueEnvelope(stored)) {
27
+ return { state: "fresh", value: stored, stored };
28
+ }
29
+ if (stored.freshUntil === null || stored.freshUntil > now) {
30
+ return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
31
+ }
32
+ if (stored.staleUntil !== null && stored.staleUntil > now) {
33
+ return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
34
+ }
35
+ if (stored.errorUntil !== null && stored.errorUntil > now) {
36
+ return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
37
+ }
38
+ return { state: "expired", value: null, stored, envelope: stored };
39
+ }
40
+ function unwrapStoredValue(stored) {
41
+ if (!isStoredValueEnvelope(stored)) {
42
+ return stored;
43
+ }
44
+ if (stored.kind === "empty") {
45
+ return null;
46
+ }
47
+ return stored.value ?? null;
48
+ }
49
+ function remainingStoredTtlSeconds(stored, now = Date.now()) {
50
+ if (!isStoredValueEnvelope(stored)) {
51
+ return void 0;
52
+ }
53
+ const expiry = maxExpiry(stored);
54
+ if (expiry === null) {
55
+ return void 0;
56
+ }
57
+ const remainingMs = expiry - now;
58
+ if (remainingMs <= 0) {
59
+ return 1;
60
+ }
61
+ return Math.max(1, Math.ceil(remainingMs / 1e3));
62
+ }
63
+ function remainingFreshTtlSeconds(stored, now = Date.now()) {
64
+ if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
65
+ return void 0;
66
+ }
67
+ const remainingMs = stored.freshUntil - now;
68
+ if (remainingMs <= 0) {
69
+ return 0;
70
+ }
71
+ return Math.max(1, Math.ceil(remainingMs / 1e3));
72
+ }
73
+ function refreshStoredEnvelope(stored, now = Date.now()) {
74
+ if (!isStoredValueEnvelope(stored)) {
75
+ return stored;
76
+ }
77
+ return createStoredValueEnvelope({
78
+ kind: stored.kind,
79
+ value: stored.value,
80
+ freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
81
+ staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
82
+ staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
83
+ now
84
+ });
85
+ }
86
+ function maxExpiry(stored) {
87
+ const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
88
+ (value) => value !== null
89
+ );
90
+ if (values.length === 0) {
91
+ return null;
92
+ }
93
+ return Math.max(...values);
94
+ }
95
+ function normalizePositiveSeconds(value) {
96
+ if (!value || value <= 0) {
97
+ return void 0;
98
+ }
99
+ return value;
100
+ }
101
+
102
+ // src/invalidation/PatternMatcher.ts
103
+ var PatternMatcher = class _PatternMatcher {
104
+ /**
105
+ * Tests whether a glob-style pattern matches a value.
106
+ * Supports `*` (any sequence of characters) and `?` (any single character).
107
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
108
+ * quadratic memory usage on long patterns/keys.
109
+ */
110
+ static matches(pattern, value) {
111
+ return _PatternMatcher.matchLinear(pattern, value);
112
+ }
113
+ /**
114
+ * Linear-time glob matching with O(1) extra memory.
115
+ */
116
+ static matchLinear(pattern, value) {
117
+ let patternIndex = 0;
118
+ let valueIndex = 0;
119
+ let starIndex = -1;
120
+ let backtrackValueIndex = 0;
121
+ while (valueIndex < value.length) {
122
+ const patternChar = pattern[patternIndex];
123
+ const valueChar = value[valueIndex];
124
+ if (patternChar === "*" && patternIndex < pattern.length) {
125
+ starIndex = patternIndex;
126
+ patternIndex += 1;
127
+ backtrackValueIndex = valueIndex;
128
+ continue;
129
+ }
130
+ if (patternChar === "?" || patternChar === valueChar) {
131
+ patternIndex += 1;
132
+ valueIndex += 1;
133
+ continue;
134
+ }
135
+ if (starIndex !== -1) {
136
+ patternIndex = starIndex + 1;
137
+ backtrackValueIndex += 1;
138
+ valueIndex = backtrackValueIndex;
139
+ continue;
140
+ }
141
+ return false;
142
+ }
143
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
144
+ patternIndex += 1;
145
+ }
146
+ return patternIndex === pattern.length;
147
+ }
148
+ };
149
+
150
+ export {
151
+ isStoredValueEnvelope,
152
+ createStoredValueEnvelope,
153
+ resolveStoredValue,
154
+ unwrapStoredValue,
155
+ remainingStoredTtlSeconds,
156
+ remainingFreshTtlSeconds,
157
+ refreshStoredEnvelope,
158
+ PatternMatcher
159
+ };
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;
108
+ }
109
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
110
+ patternIndex += 1;
72
111
  }
73
- return dp[m]?.[n];
112
+ return patternIndex === pattern.length;
74
113
  }
75
114
  };
76
115
 
@@ -79,19 +118,21 @@ var RedisTagIndex = class {
79
118
  client;
80
119
  prefix;
81
120
  scanCount;
121
+ knownKeysShards;
82
122
  constructor(options) {
83
123
  this.client = options.client;
84
124
  this.prefix = options.prefix ?? "layercache:tag-index";
85
125
  this.scanCount = options.scanCount ?? 100;
126
+ this.knownKeysShards = normalizeKnownKeysShards(options.knownKeysShards);
86
127
  }
87
128
  async touch(key) {
88
- await this.client.sadd(this.knownKeysKey(), key);
129
+ await this.client.sadd(this.knownKeysKeyFor(key), key);
89
130
  }
90
131
  async track(key, tags) {
91
132
  const keyTagsKey = this.keyTagsKey(key);
92
133
  const existingTags = await this.client.smembers(keyTagsKey);
93
134
  const pipeline = this.client.pipeline();
94
- pipeline.sadd(this.knownKeysKey(), key);
135
+ pipeline.sadd(this.knownKeysKeyFor(key), key);
95
136
  for (const tag of existingTags) {
96
137
  pipeline.srem(this.tagKeysKey(tag), key);
97
138
  }
@@ -108,7 +149,7 @@ var RedisTagIndex = class {
108
149
  const keyTagsKey = this.keyTagsKey(key);
109
150
  const existingTags = await this.client.smembers(keyTagsKey);
110
151
  const pipeline = this.client.pipeline();
111
- pipeline.srem(this.knownKeysKey(), key);
152
+ pipeline.srem(this.knownKeysKeyFor(key), key);
112
153
  pipeline.del(keyTagsKey);
113
154
  for (const tag of existingTags) {
114
155
  pipeline.srem(this.tagKeysKey(tag), key);
@@ -118,24 +159,38 @@ var RedisTagIndex = class {
118
159
  async keysForTag(tag) {
119
160
  return this.client.smembers(this.tagKeysKey(tag));
120
161
  }
162
+ async keysForPrefix(prefix) {
163
+ const matches = [];
164
+ for (const knownKeysKey of this.knownKeysKeys()) {
165
+ let cursor = "0";
166
+ do {
167
+ const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
168
+ cursor = nextCursor;
169
+ matches.push(...keys.filter((key) => key.startsWith(prefix)));
170
+ } while (cursor !== "0");
171
+ }
172
+ return matches;
173
+ }
121
174
  async tagsForKey(key) {
122
175
  return this.client.smembers(this.keyTagsKey(key));
123
176
  }
124
177
  async matchPattern(pattern) {
125
178
  const matches = [];
126
- let cursor = "0";
127
- do {
128
- const [nextCursor, keys] = await this.client.sscan(
129
- this.knownKeysKey(),
130
- cursor,
131
- "MATCH",
132
- pattern,
133
- "COUNT",
134
- this.scanCount
135
- );
136
- cursor = nextCursor;
137
- matches.push(...keys.filter((key) => PatternMatcher.matches(pattern, key)));
138
- } while (cursor !== "0");
179
+ for (const knownKeysKey of this.knownKeysKeys()) {
180
+ let cursor = "0";
181
+ do {
182
+ const [nextCursor, keys] = await this.client.sscan(
183
+ knownKeysKey,
184
+ cursor,
185
+ "MATCH",
186
+ pattern,
187
+ "COUNT",
188
+ this.scanCount
189
+ );
190
+ cursor = nextCursor;
191
+ matches.push(...keys.filter((key) => PatternMatcher.matches(pattern, key)));
192
+ } while (cursor !== "0");
193
+ }
139
194
  return matches;
140
195
  }
141
196
  async clear() {
@@ -156,8 +211,17 @@ var RedisTagIndex = class {
156
211
  } while (cursor !== "0");
157
212
  return matches;
158
213
  }
159
- knownKeysKey() {
160
- return `${this.prefix}:keys`;
214
+ knownKeysKeyFor(key) {
215
+ if (this.knownKeysShards === 1) {
216
+ return `${this.prefix}:keys`;
217
+ }
218
+ return `${this.prefix}:keys:${simpleHash(key) % this.knownKeysShards}`;
219
+ }
220
+ knownKeysKeys() {
221
+ if (this.knownKeysShards === 1) {
222
+ return [`${this.prefix}:keys`];
223
+ }
224
+ return Array.from({ length: this.knownKeysShards }, (_, index) => `${this.prefix}:keys:${index}`);
161
225
  }
162
226
  keyTagsKey(key) {
163
227
  return `${this.prefix}:key:${encodeURIComponent(key)}`;
@@ -166,6 +230,22 @@ var RedisTagIndex = class {
166
230
  return `${this.prefix}:tag:${encodeURIComponent(tag)}`;
167
231
  }
168
232
  };
233
+ function normalizeKnownKeysShards(value) {
234
+ if (value === void 0) {
235
+ return 1;
236
+ }
237
+ if (!Number.isInteger(value) || value <= 0) {
238
+ throw new Error("RedisTagIndex.knownKeysShards must be a positive integer.");
239
+ }
240
+ return value;
241
+ }
242
+ function simpleHash(value) {
243
+ let hash = 0;
244
+ for (let index = 0; index < value.length; index += 1) {
245
+ hash = hash * 31 + value.charCodeAt(index) >>> 0;
246
+ }
247
+ return hash;
248
+ }
169
249
 
170
250
  // src/cli.ts
171
251
  var CONNECT_TIMEOUT_MS = 5e3;
@@ -228,6 +308,31 @@ async function main(argv = process.argv.slice(2)) {
228
308
  `);
229
309
  return;
230
310
  }
311
+ if (args.command === "inspect") {
312
+ if (!args.key) {
313
+ throw new Error("inspect requires --key <key>.");
314
+ }
315
+ const payload = await redis.getBuffer(args.key);
316
+ const ttl = await redis.ttl(args.key);
317
+ const decoded = decodeInspectablePayload(payload);
318
+ process.stdout.write(
319
+ `${JSON.stringify(
320
+ {
321
+ key: args.key,
322
+ exists: payload !== null,
323
+ ttlSeconds: ttl >= 0 ? ttl : null,
324
+ sizeBytes: payload?.byteLength ?? 0,
325
+ isEnvelope: isStoredValueEnvelope(decoded),
326
+ state: payload === null ? null : resolveStoredValue(decoded).state,
327
+ preview: summarizeInspectableValue(decoded)
328
+ },
329
+ null,
330
+ 2
331
+ )}
332
+ `
333
+ );
334
+ return;
335
+ }
231
336
  printUsage();
232
337
  process.exitCode = 1;
233
338
  } catch (error) {
@@ -268,6 +373,9 @@ function parseArgs(argv) {
268
373
  } else if (token === "--tag") {
269
374
  parsed.tag = value;
270
375
  index += 1;
376
+ } else if (token === "--key") {
377
+ parsed.key = value;
378
+ index += 1;
271
379
  } else if (token === "--tag-index-prefix") {
272
380
  parsed.tagIndexPrefix = value;
273
381
  index += 1;
@@ -294,9 +402,32 @@ async function scanKeys(redis, pattern) {
294
402
  }
295
403
  function printUsage() {
296
404
  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"
405
+ "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
406
  );
299
407
  }
408
+ function decodeInspectablePayload(payload) {
409
+ if (payload === null) {
410
+ return null;
411
+ }
412
+ const text = payload.toString("utf8");
413
+ try {
414
+ return JSON.parse(text);
415
+ } catch {
416
+ return text.length > 256 ? `${text.slice(0, 256)}...` : text;
417
+ }
418
+ }
419
+ function summarizeInspectableValue(value) {
420
+ if (isStoredValueEnvelope(value)) {
421
+ return {
422
+ kind: value.kind,
423
+ value: value.value,
424
+ freshUntil: value.freshUntil,
425
+ staleUntil: value.staleUntil,
426
+ errorUntil: value.errorUntil
427
+ };
428
+ }
429
+ return value;
430
+ }
300
431
  if (process.argv[1]?.includes("cli.")) {
301
432
  void main();
302
433
  }