layercache 1.1.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/README.md +165 -7
- package/dist/chunk-46UH7LNM.js +312 -0
- package/dist/{chunk-QUB5VZFZ.js → chunk-GF47Y3XR.js} +16 -38
- package/dist/chunk-ZMDB5KOK.js +159 -0
- package/dist/cli.cjs +133 -23
- package/dist/cli.js +66 -4
- package/dist/edge-C1sBhTfv.d.cts +667 -0
- package/dist/edge-C1sBhTfv.d.ts +667 -0
- package/dist/edge.cjs +399 -0
- package/dist/edge.d.cts +2 -0
- package/dist/edge.d.ts +2 -0
- package/dist/edge.js +14 -0
- package/dist/index.cjs +1259 -192
- package/dist/index.d.cts +132 -480
- package/dist/index.d.ts +132 -480
- package/dist/index.js +1115 -474
- package/package.json +7 -2
- package/packages/nestjs/dist/index.cjs +1025 -327
- package/packages/nestjs/dist/index.d.cts +167 -1
- package/packages/nestjs/dist/index.d.ts +167 -1
- package/packages/nestjs/dist/index.js +1013 -325
|
@@ -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
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
|
71
100
|
}
|
|
101
|
+
if (starIndex !== -1) {
|
|
102
|
+
patternIndex = starIndex + 1;
|
|
103
|
+
backtrackValueIndex += 1;
|
|
104
|
+
valueIndex = backtrackValueIndex;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
110
|
+
patternIndex += 1;
|
|
72
111
|
}
|
|
73
|
-
return
|
|
112
|
+
return patternIndex === pattern.length;
|
|
74
113
|
}
|
|
75
114
|
};
|
|
76
115
|
|
|
@@ -118,6 +157,19 @@ 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
|
+
}
|
|
170
|
+
async tagsForKey(key) {
|
|
171
|
+
return this.client.smembers(this.keyTagsKey(key));
|
|
172
|
+
}
|
|
121
173
|
async matchPattern(pattern) {
|
|
122
174
|
const matches = [];
|
|
123
175
|
let cursor = "0";
|
|
@@ -211,7 +263,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
211
263
|
const tagIndex = new RedisTagIndex({ client: redis, prefix: args.tagIndexPrefix ?? "layercache:tag-index" });
|
|
212
264
|
const keys2 = await tagIndex.keysForTag(args.tag);
|
|
213
265
|
if (keys2.length > 0) {
|
|
214
|
-
await redis
|
|
266
|
+
await batchDelete(redis, keys2);
|
|
215
267
|
}
|
|
216
268
|
process.stdout.write(`${JSON.stringify({ deletedKeys: keys2.length, tag: args.tag }, null, 2)}
|
|
217
269
|
`);
|
|
@@ -219,12 +271,37 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
219
271
|
}
|
|
220
272
|
const keys = await scanKeys(redis, args.pattern ?? "*");
|
|
221
273
|
if (keys.length > 0) {
|
|
222
|
-
await redis
|
|
274
|
+
await batchDelete(redis, keys);
|
|
223
275
|
}
|
|
224
276
|
process.stdout.write(`${JSON.stringify({ deletedKeys: keys.length, pattern: args.pattern ?? "*" }, null, 2)}
|
|
225
277
|
`);
|
|
226
278
|
return;
|
|
227
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
|
+
}
|
|
228
305
|
printUsage();
|
|
229
306
|
process.exitCode = 1;
|
|
230
307
|
} catch (error) {
|
|
@@ -265,6 +342,9 @@ function parseArgs(argv) {
|
|
|
265
342
|
} else if (token === "--tag") {
|
|
266
343
|
parsed.tag = value;
|
|
267
344
|
index += 1;
|
|
345
|
+
} else if (token === "--key") {
|
|
346
|
+
parsed.key = value;
|
|
347
|
+
index += 1;
|
|
268
348
|
} else if (token === "--tag-index-prefix") {
|
|
269
349
|
parsed.tagIndexPrefix = value;
|
|
270
350
|
index += 1;
|
|
@@ -272,6 +352,13 @@ function parseArgs(argv) {
|
|
|
272
352
|
}
|
|
273
353
|
return parsed;
|
|
274
354
|
}
|
|
355
|
+
var BATCH_DELETE_SIZE = 500;
|
|
356
|
+
async function batchDelete(redis, keys) {
|
|
357
|
+
for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
|
|
358
|
+
const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
|
|
359
|
+
await redis.del(...batch);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
275
362
|
async function scanKeys(redis, pattern) {
|
|
276
363
|
const keys = [];
|
|
277
364
|
let cursor = "0";
|
|
@@ -284,9 +371,32 @@ async function scanKeys(redis, pattern) {
|
|
|
284
371
|
}
|
|
285
372
|
function printUsage() {
|
|
286
373
|
process.stdout.write(
|
|
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"
|
|
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"
|
|
288
375
|
);
|
|
289
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
|
+
}
|
|
290
400
|
if (process.argv[1]?.includes("cli.")) {
|
|
291
401
|
void main();
|
|
292
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-
|
|
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";
|
|
@@ -51,7 +55,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
51
55
|
const tagIndex = new RedisTagIndex({ client: redis, prefix: args.tagIndexPrefix ?? "layercache:tag-index" });
|
|
52
56
|
const keys2 = await tagIndex.keysForTag(args.tag);
|
|
53
57
|
if (keys2.length > 0) {
|
|
54
|
-
await redis
|
|
58
|
+
await batchDelete(redis, keys2);
|
|
55
59
|
}
|
|
56
60
|
process.stdout.write(`${JSON.stringify({ deletedKeys: keys2.length, tag: args.tag }, null, 2)}
|
|
57
61
|
`);
|
|
@@ -59,12 +63,37 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
59
63
|
}
|
|
60
64
|
const keys = await scanKeys(redis, args.pattern ?? "*");
|
|
61
65
|
if (keys.length > 0) {
|
|
62
|
-
await redis
|
|
66
|
+
await batchDelete(redis, keys);
|
|
63
67
|
}
|
|
64
68
|
process.stdout.write(`${JSON.stringify({ deletedKeys: keys.length, pattern: args.pattern ?? "*" }, null, 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;
|
|
@@ -112,6 +144,13 @@ function parseArgs(argv) {
|
|
|
112
144
|
}
|
|
113
145
|
return parsed;
|
|
114
146
|
}
|
|
147
|
+
var BATCH_DELETE_SIZE = 500;
|
|
148
|
+
async function batchDelete(redis, keys) {
|
|
149
|
+
for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
|
|
150
|
+
const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
|
|
151
|
+
await redis.del(...batch);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
115
154
|
async function scanKeys(redis, pattern) {
|
|
116
155
|
const keys = [];
|
|
117
156
|
let cursor = "0";
|
|
@@ -124,9 +163,32 @@ async function scanKeys(redis, pattern) {
|
|
|
124
163
|
}
|
|
125
164
|
function printUsage() {
|
|
126
165
|
process.stdout.write(
|
|
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"
|
|
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"
|
|
128
167
|
);
|
|
129
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
|
+
}
|
|
130
192
|
if (process.argv[1]?.includes("cli.")) {
|
|
131
193
|
void main();
|
|
132
194
|
}
|