layercache 2.0.0 → 3.0.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 +22 -6
- package/dist/{chunk-FFZCC7EQ.js → chunk-5CIBABDH.js} +149 -19
- package/dist/{chunk-7KMKQ6QZ.js → chunk-NBMG7DHT.js} +118 -13
- package/dist/cli.cjs +186 -25
- package/dist/cli.js +69 -13
- package/dist/{edge-D2FpRlyS.d.cts → edge-BDyuPmIq.d.cts} +509 -0
- package/dist/{edge-D2FpRlyS.d.ts → edge-BDyuPmIq.d.ts} +509 -0
- package/dist/edge.cjs +148 -19
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +1 -1
- package/dist/index.cjs +797 -82
- package/dist/index.d.cts +289 -3
- package/dist/index.d.ts +289 -3
- package/dist/index.js +528 -47
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -216,20 +216,30 @@ var PatternMatcher = class _PatternMatcher {
|
|
|
216
216
|
};
|
|
217
217
|
|
|
218
218
|
// src/invalidation/RedisTagIndex.ts
|
|
219
|
+
var DEFAULT_KNOWN_KEYS_SHARDS = 16;
|
|
219
220
|
var RedisTagIndex = class {
|
|
220
221
|
client;
|
|
221
222
|
prefix;
|
|
222
223
|
scanCount;
|
|
223
224
|
knownKeysShards;
|
|
225
|
+
logger;
|
|
226
|
+
warnedLegacyKnownKeys = false;
|
|
224
227
|
constructor(options) {
|
|
225
228
|
this.client = options.client;
|
|
226
229
|
this.prefix = options.prefix ?? "layercache:tag-index";
|
|
227
230
|
this.scanCount = options.scanCount ?? 100;
|
|
228
231
|
this.knownKeysShards = normalizeKnownKeysShards(options.knownKeysShards);
|
|
232
|
+
this.logger = options.logger;
|
|
229
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Records a key as known without changing tag assignments.
|
|
236
|
+
*/
|
|
230
237
|
async touch(key) {
|
|
231
238
|
await this.client.sadd(this.knownKeysKeyFor(key), key);
|
|
232
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Replaces the tags associated with a key and records the key as known.
|
|
242
|
+
*/
|
|
233
243
|
async track(key, tags) {
|
|
234
244
|
const keyTagsKey = this.keyTagsKey(key);
|
|
235
245
|
const existingTags = await this.client.smembers(keyTagsKey);
|
|
@@ -247,20 +257,32 @@ var RedisTagIndex = class {
|
|
|
247
257
|
}
|
|
248
258
|
await pipeline.exec();
|
|
249
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Removes a key from all tag mappings and known-key tracking.
|
|
262
|
+
*/
|
|
250
263
|
async remove(key) {
|
|
251
264
|
const keyTagsKey = this.keyTagsKey(key);
|
|
252
265
|
const existingTags = await this.client.smembers(keyTagsKey);
|
|
253
266
|
const pipeline = this.client.pipeline();
|
|
254
267
|
pipeline.srem(this.knownKeysKeyFor(key), key);
|
|
268
|
+
if (this.knownKeysShards > 1) {
|
|
269
|
+
pipeline.srem(this.legacyKnownKeysKey(), key);
|
|
270
|
+
}
|
|
255
271
|
pipeline.del(keyTagsKey);
|
|
256
272
|
for (const tag of existingTags) {
|
|
257
273
|
pipeline.srem(this.tagKeysKey(tag), key);
|
|
258
274
|
}
|
|
259
275
|
await pipeline.exec();
|
|
260
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Returns keys currently associated with a tag.
|
|
279
|
+
*/
|
|
261
280
|
async keysForTag(tag) {
|
|
262
281
|
return this.client.smembers(this.tagKeysKey(tag));
|
|
263
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Visits keys currently associated with a tag.
|
|
285
|
+
*/
|
|
264
286
|
async forEachKeyForTag(tag, visitor) {
|
|
265
287
|
let cursor = "0";
|
|
266
288
|
const tagKey = this.tagKeysKey(tag);
|
|
@@ -272,38 +294,56 @@ var RedisTagIndex = class {
|
|
|
272
294
|
}
|
|
273
295
|
} while (cursor !== "0");
|
|
274
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Returns known keys that start with a prefix.
|
|
299
|
+
*/
|
|
275
300
|
async keysForPrefix(prefix) {
|
|
276
|
-
const matches =
|
|
277
|
-
for (const knownKeysKey of this.
|
|
301
|
+
const matches = /* @__PURE__ */ new Set();
|
|
302
|
+
for (const knownKeysKey of await this.knownKeysKeysForRead()) {
|
|
278
303
|
let cursor = "0";
|
|
279
304
|
do {
|
|
280
305
|
const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
|
|
281
306
|
cursor = nextCursor;
|
|
282
|
-
|
|
307
|
+
for (const key of keys) {
|
|
308
|
+
if (key.startsWith(prefix)) {
|
|
309
|
+
matches.add(key);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
283
312
|
} while (cursor !== "0");
|
|
284
313
|
}
|
|
285
|
-
return matches;
|
|
314
|
+
return [...matches];
|
|
286
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Visits known keys that start with a prefix.
|
|
318
|
+
*/
|
|
287
319
|
async forEachKeyForPrefix(prefix, visitor) {
|
|
288
|
-
|
|
320
|
+
const visited = /* @__PURE__ */ new Set();
|
|
321
|
+
for (const knownKeysKey of await this.knownKeysKeysForRead()) {
|
|
289
322
|
let cursor = "0";
|
|
290
323
|
do {
|
|
291
324
|
const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
|
|
292
325
|
cursor = nextCursor;
|
|
293
326
|
for (const key of keys) {
|
|
294
|
-
if (key.startsWith(prefix)) {
|
|
327
|
+
if (key.startsWith(prefix) && !visited.has(key)) {
|
|
328
|
+
visited.add(key);
|
|
295
329
|
await visitor(key);
|
|
296
330
|
}
|
|
297
331
|
}
|
|
298
332
|
} while (cursor !== "0");
|
|
299
333
|
}
|
|
300
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Returns the tags currently associated with a key.
|
|
337
|
+
*/
|
|
301
338
|
async tagsForKey(key) {
|
|
302
339
|
return this.client.smembers(this.keyTagsKey(key));
|
|
303
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Returns known keys matching a wildcard pattern.
|
|
343
|
+
*/
|
|
304
344
|
async matchPattern(pattern) {
|
|
305
|
-
const matches =
|
|
306
|
-
for (const knownKeysKey of this.
|
|
345
|
+
const matches = /* @__PURE__ */ new Set();
|
|
346
|
+
for (const knownKeysKey of await this.knownKeysKeysForRead()) {
|
|
307
347
|
let cursor = "0";
|
|
308
348
|
do {
|
|
309
349
|
const [nextCursor, keys] = await this.client.sscan(
|
|
@@ -315,13 +355,21 @@ var RedisTagIndex = class {
|
|
|
315
355
|
this.scanCount
|
|
316
356
|
);
|
|
317
357
|
cursor = nextCursor;
|
|
318
|
-
|
|
358
|
+
for (const key of keys) {
|
|
359
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
360
|
+
matches.add(key);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
319
363
|
} while (cursor !== "0");
|
|
320
364
|
}
|
|
321
|
-
return matches;
|
|
365
|
+
return [...matches];
|
|
322
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Visits known keys matching a wildcard pattern.
|
|
369
|
+
*/
|
|
323
370
|
async forEachKeyMatchingPattern(pattern, visitor) {
|
|
324
|
-
|
|
371
|
+
const visited = /* @__PURE__ */ new Set();
|
|
372
|
+
for (const knownKeysKey of await this.knownKeysKeysForRead()) {
|
|
325
373
|
let cursor = "0";
|
|
326
374
|
do {
|
|
327
375
|
const [nextCursor, keys] = await this.client.sscan(
|
|
@@ -334,13 +382,17 @@ var RedisTagIndex = class {
|
|
|
334
382
|
);
|
|
335
383
|
cursor = nextCursor;
|
|
336
384
|
for (const key of keys) {
|
|
337
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
385
|
+
if (PatternMatcher.matches(pattern, key) && !visited.has(key)) {
|
|
386
|
+
visited.add(key);
|
|
338
387
|
await visitor(key);
|
|
339
388
|
}
|
|
340
389
|
}
|
|
341
390
|
} while (cursor !== "0");
|
|
342
391
|
}
|
|
343
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Clears all Redis tag-index state under this prefix.
|
|
395
|
+
*/
|
|
344
396
|
async clear() {
|
|
345
397
|
const indexKeys = await this.scanIndexKeys();
|
|
346
398
|
if (indexKeys.length === 0) {
|
|
@@ -348,6 +400,31 @@ var RedisTagIndex = class {
|
|
|
348
400
|
}
|
|
349
401
|
await this.client.del(...indexKeys);
|
|
350
402
|
}
|
|
403
|
+
async migrateLegacyKnownKeys() {
|
|
404
|
+
if (this.knownKeysShards === 1) {
|
|
405
|
+
return { migratedKeys: 0 };
|
|
406
|
+
}
|
|
407
|
+
const legacyKey = this.legacyKnownKeysKey();
|
|
408
|
+
let cursor = "0";
|
|
409
|
+
let migratedKeys = 0;
|
|
410
|
+
do {
|
|
411
|
+
const [nextCursor, keys] = await this.client.sscan(legacyKey, cursor, "COUNT", this.scanCount);
|
|
412
|
+
cursor = nextCursor;
|
|
413
|
+
if (keys.length === 0) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const pipeline = this.client.pipeline();
|
|
417
|
+
for (const key of keys) {
|
|
418
|
+
pipeline.sadd(this.knownKeysKeyFor(key), key);
|
|
419
|
+
}
|
|
420
|
+
await pipeline.exec();
|
|
421
|
+
migratedKeys += keys.length;
|
|
422
|
+
} while (cursor !== "0");
|
|
423
|
+
if (migratedKeys > 0) {
|
|
424
|
+
await this.client.del(legacyKey);
|
|
425
|
+
}
|
|
426
|
+
return { migratedKeys };
|
|
427
|
+
}
|
|
351
428
|
async scanIndexKeys() {
|
|
352
429
|
const matches = [];
|
|
353
430
|
let cursor = "0";
|
|
@@ -365,12 +442,40 @@ var RedisTagIndex = class {
|
|
|
365
442
|
}
|
|
366
443
|
return `${this.prefix}:keys:${simpleHash(key) % this.knownKeysShards}`;
|
|
367
444
|
}
|
|
445
|
+
async knownKeysKeysForRead() {
|
|
446
|
+
if (this.knownKeysShards === 1) {
|
|
447
|
+
return [this.legacyKnownKeysKey()];
|
|
448
|
+
}
|
|
449
|
+
const shardedKeys = this.knownKeysKeys();
|
|
450
|
+
const legacyKey = this.legacyKnownKeysKey();
|
|
451
|
+
const legacyExists = await this.client.exists(legacyKey) > 0;
|
|
452
|
+
if (!legacyExists) {
|
|
453
|
+
return shardedKeys;
|
|
454
|
+
}
|
|
455
|
+
this.warnLegacyKnownKeys(legacyKey);
|
|
456
|
+
return [legacyKey, ...shardedKeys];
|
|
457
|
+
}
|
|
368
458
|
knownKeysKeys() {
|
|
369
459
|
if (this.knownKeysShards === 1) {
|
|
370
460
|
return [`${this.prefix}:keys`];
|
|
371
461
|
}
|
|
372
462
|
return Array.from({ length: this.knownKeysShards }, (_, index) => `${this.prefix}:keys:${index}`);
|
|
373
463
|
}
|
|
464
|
+
legacyKnownKeysKey() {
|
|
465
|
+
return `${this.prefix}:keys`;
|
|
466
|
+
}
|
|
467
|
+
warnLegacyKnownKeys(legacyKey) {
|
|
468
|
+
if (this.warnedLegacyKnownKeys) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
this.warnedLegacyKnownKeys = true;
|
|
472
|
+
const message = "RedisTagIndex detected a legacy RedisTagIndex known-key set. Run `layercache migrate-tag-index` to migrate keys into the sharded layout.";
|
|
473
|
+
if (this.logger?.warn) {
|
|
474
|
+
this.logger.warn(message, { legacyKey, knownKeysShards: this.knownKeysShards });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
console.warn(`[layercache] ${message}`, { legacyKey, knownKeysShards: this.knownKeysShards });
|
|
478
|
+
}
|
|
374
479
|
keyTagsKey(key) {
|
|
375
480
|
return `${this.prefix}:key:${encodeURIComponent(key)}`;
|
|
376
481
|
}
|
|
@@ -380,7 +485,7 @@ var RedisTagIndex = class {
|
|
|
380
485
|
};
|
|
381
486
|
function normalizeKnownKeysShards(value) {
|
|
382
487
|
if (value === void 0) {
|
|
383
|
-
return
|
|
488
|
+
return DEFAULT_KNOWN_KEYS_SHARDS;
|
|
384
489
|
}
|
|
385
490
|
if (!Number.isInteger(value) || value <= 0) {
|
|
386
491
|
throw new Error("RedisTagIndex.knownKeysShards must be a positive integer.");
|
|
@@ -398,7 +503,11 @@ function simpleHash(value) {
|
|
|
398
503
|
// src/cli.ts
|
|
399
504
|
var CONNECT_TIMEOUT_MS = 5e3;
|
|
400
505
|
async function main(argv = process.argv.slice(2)) {
|
|
506
|
+
process.exitCode = void 0;
|
|
401
507
|
const args = parseArgs(argv);
|
|
508
|
+
if (args.parseError) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
402
511
|
if (!args.command || !args.redisUrl) {
|
|
403
512
|
printUsage();
|
|
404
513
|
process.exitCode = 1;
|
|
@@ -418,6 +527,13 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
418
527
|
process.exitCode = 1;
|
|
419
528
|
return;
|
|
420
529
|
}
|
|
530
|
+
if (process.env.NODE_ENV === "production" && !args.allowPlaintext) {
|
|
531
|
+
process.stderr.write(
|
|
532
|
+
"Error: refusing plaintext redis:// connection because NODE_ENV=production. Use rediss:// for TLS-encrypted connections, or pass --allow-plaintext to explicitly override.\n"
|
|
533
|
+
);
|
|
534
|
+
process.exitCode = 1;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
421
537
|
process.stderr.write(
|
|
422
538
|
"Warning: connecting to Redis without TLS (redis://). All data including cached values and credentials will be transmitted in plaintext. Use rediss:// in production environments, or set --require-tls.\n"
|
|
423
539
|
);
|
|
@@ -435,7 +551,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
435
551
|
if (args.command === "stats") {
|
|
436
552
|
const pattern = args.pattern ?? "*";
|
|
437
553
|
if (args.pattern && !validateCliInput(args.pattern, validatePattern)) return;
|
|
438
|
-
const keys = await scanKeys(redis, pattern);
|
|
554
|
+
const keys = await scanKeys(redis, pattern, args.scanLimit ?? DEFAULT_SCAN_MAX_KEYS);
|
|
439
555
|
process.stdout.write(`${JSON.stringify({ totalKeys: keys.length, pattern }, null, 2)}
|
|
440
556
|
`);
|
|
441
557
|
return;
|
|
@@ -443,7 +559,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
443
559
|
if (args.command === "keys") {
|
|
444
560
|
const pattern = args.pattern ?? "*";
|
|
445
561
|
if (args.pattern && !validateCliInput(args.pattern, validatePattern)) return;
|
|
446
|
-
const keys = await scanKeys(redis, pattern);
|
|
562
|
+
const keys = await scanKeys(redis, pattern, args.scanLimit ?? DEFAULT_SCAN_MAX_KEYS);
|
|
447
563
|
if (keys.length > 0) {
|
|
448
564
|
process.stdout.write(`${keys.join("\n")}
|
|
449
565
|
`);
|
|
@@ -464,7 +580,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
464
580
|
}
|
|
465
581
|
const effectivePattern = args.pattern ?? "*";
|
|
466
582
|
if (args.pattern && !validateCliInput(args.pattern, validatePattern)) return;
|
|
467
|
-
const keys = await scanKeys(redis, effectivePattern);
|
|
583
|
+
const keys = await scanKeys(redis, effectivePattern, args.scanLimit ?? DEFAULT_SCAN_MAX_KEYS);
|
|
468
584
|
if (!args.pattern && !args.force && keys.length > 0) {
|
|
469
585
|
process.stderr.write(`Warning: this operation will invalidate ${keys.length} keys. Use --force to confirm.
|
|
470
586
|
`);
|
|
@@ -474,6 +590,17 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
474
590
|
await batchDelete(redis, keys);
|
|
475
591
|
}
|
|
476
592
|
process.stdout.write(`${JSON.stringify({ deletedKeys: keys.length, pattern: effectivePattern }, null, 2)}
|
|
593
|
+
`);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (args.command === "migrate-tag-index") {
|
|
597
|
+
const tagIndex = new RedisTagIndex({
|
|
598
|
+
client: redis,
|
|
599
|
+
prefix: args.tagIndexPrefix ?? "layercache:tag-index",
|
|
600
|
+
knownKeysShards: args.knownKeysShards
|
|
601
|
+
});
|
|
602
|
+
const result = await tagIndex.migrateLegacyKnownKeys();
|
|
603
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
477
604
|
`);
|
|
478
605
|
return;
|
|
479
606
|
}
|
|
@@ -538,6 +665,7 @@ function parseArgs(argv) {
|
|
|
538
665
|
if (!value || value.startsWith("--")) {
|
|
539
666
|
process.stderr.write("Error: --redis requires a value (e.g. redis://localhost:6379)\n");
|
|
540
667
|
process.exitCode = 1;
|
|
668
|
+
parsed.parseError = true;
|
|
541
669
|
return parsed;
|
|
542
670
|
}
|
|
543
671
|
parsed.redisUrl = value;
|
|
@@ -554,8 +682,42 @@ function parseArgs(argv) {
|
|
|
554
682
|
} else if (token === "--tag-index-prefix") {
|
|
555
683
|
parsed.tagIndexPrefix = value;
|
|
556
684
|
index += 1;
|
|
685
|
+
} else if (token === "--known-key-shards") {
|
|
686
|
+
if (!value || value.startsWith("--")) {
|
|
687
|
+
process.stderr.write("Error: --known-key-shards requires a positive integer value.\n");
|
|
688
|
+
process.exitCode = 1;
|
|
689
|
+
parsed.parseError = true;
|
|
690
|
+
return parsed;
|
|
691
|
+
}
|
|
692
|
+
const knownKeysShards = Number(value);
|
|
693
|
+
if (!Number.isSafeInteger(knownKeysShards) || knownKeysShards <= 0) {
|
|
694
|
+
process.stderr.write("Error: --known-key-shards requires a positive integer value.\n");
|
|
695
|
+
process.exitCode = 1;
|
|
696
|
+
parsed.parseError = true;
|
|
697
|
+
return parsed;
|
|
698
|
+
}
|
|
699
|
+
parsed.knownKeysShards = knownKeysShards;
|
|
700
|
+
index += 1;
|
|
701
|
+
} else if (token === "--limit") {
|
|
702
|
+
if (!value || value.startsWith("--")) {
|
|
703
|
+
process.stderr.write("Error: --limit requires a positive integer value.\n");
|
|
704
|
+
process.exitCode = 1;
|
|
705
|
+
parsed.parseError = true;
|
|
706
|
+
return parsed;
|
|
707
|
+
}
|
|
708
|
+
const limit = Number(value);
|
|
709
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
710
|
+
process.stderr.write("Error: --limit requires a positive integer value.\n");
|
|
711
|
+
process.exitCode = 1;
|
|
712
|
+
parsed.parseError = true;
|
|
713
|
+
return parsed;
|
|
714
|
+
}
|
|
715
|
+
parsed.scanLimit = limit;
|
|
716
|
+
index += 1;
|
|
557
717
|
} else if (token === "--require-tls") {
|
|
558
718
|
parsed.requireTls = true;
|
|
719
|
+
} else if (token === "--allow-plaintext") {
|
|
720
|
+
parsed.allowPlaintext = true;
|
|
559
721
|
} else if (token === "--force") {
|
|
560
722
|
parsed.force = true;
|
|
561
723
|
}
|
|
@@ -563,25 +725,24 @@ function parseArgs(argv) {
|
|
|
563
725
|
return parsed;
|
|
564
726
|
}
|
|
565
727
|
var BATCH_DELETE_SIZE = 500;
|
|
566
|
-
var
|
|
728
|
+
var DEFAULT_SCAN_MAX_KEYS = 1e5;
|
|
567
729
|
async function batchDelete(redis, keys) {
|
|
568
730
|
for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
|
|
569
731
|
const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
|
|
570
732
|
await redis.del(...batch);
|
|
571
733
|
}
|
|
572
734
|
}
|
|
573
|
-
async function scanKeys(redis, pattern) {
|
|
735
|
+
async function scanKeys(redis, pattern, limit) {
|
|
574
736
|
const keys = [];
|
|
575
737
|
let cursor = "0";
|
|
576
738
|
do {
|
|
577
739
|
const [nextCursor, batch] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
|
|
578
740
|
cursor = nextCursor;
|
|
579
|
-
keys.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
`
|
|
584
|
-
);
|
|
741
|
+
const remaining = limit - keys.length;
|
|
742
|
+
keys.push(...batch.slice(0, remaining));
|
|
743
|
+
if (keys.length >= limit) {
|
|
744
|
+
process.stderr.write(`Warning: stopped scanning after ${limit} keys. Use --limit to raise the scan cap.
|
|
745
|
+
`);
|
|
585
746
|
return keys;
|
|
586
747
|
}
|
|
587
748
|
} while (cursor !== "0");
|
|
@@ -589,7 +750,7 @@ async function scanKeys(redis, pattern) {
|
|
|
589
750
|
}
|
|
590
751
|
function printUsage() {
|
|
591
752
|
process.stdout.write(
|
|
592
|
-
"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 --require-tls Reject non-TLS (redis://) connections\n"
|
|
753
|
+
"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 layercache migrate-tag-index --redis <url> [--tag-index-prefix <prefix>] [--known-key-shards <count>]\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 --known-key-shards <count> Shard count for RedisTagIndex migration (default: 16)\n --limit <count> Maximum Redis keys to scan (default: 100000)\n --require-tls Reject non-TLS (redis://) connections\n --allow-plaintext Explicitly allow redis:// when NODE_ENV=production\n"
|
|
593
754
|
);
|
|
594
755
|
}
|
|
595
756
|
function decodeInspectablePayload(payload) {
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
validateCacheKey,
|
|
5
5
|
validatePattern,
|
|
6
6
|
validateTag
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-NBMG7DHT.js";
|
|
8
8
|
import {
|
|
9
9
|
isStoredValueEnvelope,
|
|
10
10
|
resolveStoredValue
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
import Redis from "ioredis";
|
|
15
15
|
var CONNECT_TIMEOUT_MS = 5e3;
|
|
16
16
|
async function main(argv = process.argv.slice(2)) {
|
|
17
|
+
process.exitCode = void 0;
|
|
17
18
|
const args = parseArgs(argv);
|
|
19
|
+
if (args.parseError) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
18
22
|
if (!args.command || !args.redisUrl) {
|
|
19
23
|
printUsage();
|
|
20
24
|
process.exitCode = 1;
|
|
@@ -34,6 +38,13 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
34
38
|
process.exitCode = 1;
|
|
35
39
|
return;
|
|
36
40
|
}
|
|
41
|
+
if (process.env.NODE_ENV === "production" && !args.allowPlaintext) {
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
"Error: refusing plaintext redis:// connection because NODE_ENV=production. Use rediss:// for TLS-encrypted connections, or pass --allow-plaintext to explicitly override.\n"
|
|
44
|
+
);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
37
48
|
process.stderr.write(
|
|
38
49
|
"Warning: connecting to Redis without TLS (redis://). All data including cached values and credentials will be transmitted in plaintext. Use rediss:// in production environments, or set --require-tls.\n"
|
|
39
50
|
);
|
|
@@ -51,7 +62,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
51
62
|
if (args.command === "stats") {
|
|
52
63
|
const pattern = args.pattern ?? "*";
|
|
53
64
|
if (args.pattern && !validateCliInput(args.pattern, validatePattern)) return;
|
|
54
|
-
const keys = await scanKeys(redis, pattern);
|
|
65
|
+
const keys = await scanKeys(redis, pattern, args.scanLimit ?? DEFAULT_SCAN_MAX_KEYS);
|
|
55
66
|
process.stdout.write(`${JSON.stringify({ totalKeys: keys.length, pattern }, null, 2)}
|
|
56
67
|
`);
|
|
57
68
|
return;
|
|
@@ -59,7 +70,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
59
70
|
if (args.command === "keys") {
|
|
60
71
|
const pattern = args.pattern ?? "*";
|
|
61
72
|
if (args.pattern && !validateCliInput(args.pattern, validatePattern)) return;
|
|
62
|
-
const keys = await scanKeys(redis, pattern);
|
|
73
|
+
const keys = await scanKeys(redis, pattern, args.scanLimit ?? DEFAULT_SCAN_MAX_KEYS);
|
|
63
74
|
if (keys.length > 0) {
|
|
64
75
|
process.stdout.write(`${keys.join("\n")}
|
|
65
76
|
`);
|
|
@@ -80,7 +91,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
80
91
|
}
|
|
81
92
|
const effectivePattern = args.pattern ?? "*";
|
|
82
93
|
if (args.pattern && !validateCliInput(args.pattern, validatePattern)) return;
|
|
83
|
-
const keys = await scanKeys(redis, effectivePattern);
|
|
94
|
+
const keys = await scanKeys(redis, effectivePattern, args.scanLimit ?? DEFAULT_SCAN_MAX_KEYS);
|
|
84
95
|
if (!args.pattern && !args.force && keys.length > 0) {
|
|
85
96
|
process.stderr.write(`Warning: this operation will invalidate ${keys.length} keys. Use --force to confirm.
|
|
86
97
|
`);
|
|
@@ -90,6 +101,17 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
90
101
|
await batchDelete(redis, keys);
|
|
91
102
|
}
|
|
92
103
|
process.stdout.write(`${JSON.stringify({ deletedKeys: keys.length, pattern: effectivePattern }, null, 2)}
|
|
104
|
+
`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (args.command === "migrate-tag-index") {
|
|
108
|
+
const tagIndex = new RedisTagIndex({
|
|
109
|
+
client: redis,
|
|
110
|
+
prefix: args.tagIndexPrefix ?? "layercache:tag-index",
|
|
111
|
+
knownKeysShards: args.knownKeysShards
|
|
112
|
+
});
|
|
113
|
+
const result = await tagIndex.migrateLegacyKnownKeys();
|
|
114
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
93
115
|
`);
|
|
94
116
|
return;
|
|
95
117
|
}
|
|
@@ -154,6 +176,7 @@ function parseArgs(argv) {
|
|
|
154
176
|
if (!value || value.startsWith("--")) {
|
|
155
177
|
process.stderr.write("Error: --redis requires a value (e.g. redis://localhost:6379)\n");
|
|
156
178
|
process.exitCode = 1;
|
|
179
|
+
parsed.parseError = true;
|
|
157
180
|
return parsed;
|
|
158
181
|
}
|
|
159
182
|
parsed.redisUrl = value;
|
|
@@ -170,8 +193,42 @@ function parseArgs(argv) {
|
|
|
170
193
|
} else if (token === "--tag-index-prefix") {
|
|
171
194
|
parsed.tagIndexPrefix = value;
|
|
172
195
|
index += 1;
|
|
196
|
+
} else if (token === "--known-key-shards") {
|
|
197
|
+
if (!value || value.startsWith("--")) {
|
|
198
|
+
process.stderr.write("Error: --known-key-shards requires a positive integer value.\n");
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
parsed.parseError = true;
|
|
201
|
+
return parsed;
|
|
202
|
+
}
|
|
203
|
+
const knownKeysShards = Number(value);
|
|
204
|
+
if (!Number.isSafeInteger(knownKeysShards) || knownKeysShards <= 0) {
|
|
205
|
+
process.stderr.write("Error: --known-key-shards requires a positive integer value.\n");
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
parsed.parseError = true;
|
|
208
|
+
return parsed;
|
|
209
|
+
}
|
|
210
|
+
parsed.knownKeysShards = knownKeysShards;
|
|
211
|
+
index += 1;
|
|
212
|
+
} else if (token === "--limit") {
|
|
213
|
+
if (!value || value.startsWith("--")) {
|
|
214
|
+
process.stderr.write("Error: --limit requires a positive integer value.\n");
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
parsed.parseError = true;
|
|
217
|
+
return parsed;
|
|
218
|
+
}
|
|
219
|
+
const limit = Number(value);
|
|
220
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
221
|
+
process.stderr.write("Error: --limit requires a positive integer value.\n");
|
|
222
|
+
process.exitCode = 1;
|
|
223
|
+
parsed.parseError = true;
|
|
224
|
+
return parsed;
|
|
225
|
+
}
|
|
226
|
+
parsed.scanLimit = limit;
|
|
227
|
+
index += 1;
|
|
173
228
|
} else if (token === "--require-tls") {
|
|
174
229
|
parsed.requireTls = true;
|
|
230
|
+
} else if (token === "--allow-plaintext") {
|
|
231
|
+
parsed.allowPlaintext = true;
|
|
175
232
|
} else if (token === "--force") {
|
|
176
233
|
parsed.force = true;
|
|
177
234
|
}
|
|
@@ -179,25 +236,24 @@ function parseArgs(argv) {
|
|
|
179
236
|
return parsed;
|
|
180
237
|
}
|
|
181
238
|
var BATCH_DELETE_SIZE = 500;
|
|
182
|
-
var
|
|
239
|
+
var DEFAULT_SCAN_MAX_KEYS = 1e5;
|
|
183
240
|
async function batchDelete(redis, keys) {
|
|
184
241
|
for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
|
|
185
242
|
const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
|
|
186
243
|
await redis.del(...batch);
|
|
187
244
|
}
|
|
188
245
|
}
|
|
189
|
-
async function scanKeys(redis, pattern) {
|
|
246
|
+
async function scanKeys(redis, pattern, limit) {
|
|
190
247
|
const keys = [];
|
|
191
248
|
let cursor = "0";
|
|
192
249
|
do {
|
|
193
250
|
const [nextCursor, batch] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
|
|
194
251
|
cursor = nextCursor;
|
|
195
|
-
keys.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
`
|
|
200
|
-
);
|
|
252
|
+
const remaining = limit - keys.length;
|
|
253
|
+
keys.push(...batch.slice(0, remaining));
|
|
254
|
+
if (keys.length >= limit) {
|
|
255
|
+
process.stderr.write(`Warning: stopped scanning after ${limit} keys. Use --limit to raise the scan cap.
|
|
256
|
+
`);
|
|
201
257
|
return keys;
|
|
202
258
|
}
|
|
203
259
|
} while (cursor !== "0");
|
|
@@ -205,7 +261,7 @@ async function scanKeys(redis, pattern) {
|
|
|
205
261
|
}
|
|
206
262
|
function printUsage() {
|
|
207
263
|
process.stdout.write(
|
|
208
|
-
"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 --require-tls Reject non-TLS (redis://) connections\n"
|
|
264
|
+
"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 layercache migrate-tag-index --redis <url> [--tag-index-prefix <prefix>] [--known-key-shards <count>]\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 --known-key-shards <count> Shard count for RedisTagIndex migration (default: 16)\n --limit <count> Maximum Redis keys to scan (default: 100000)\n --require-tls Reject non-TLS (redis://) connections\n --allow-plaintext Explicitly allow redis:// when NODE_ENV=production\n"
|
|
209
265
|
);
|
|
210
266
|
}
|
|
211
267
|
function decodeInspectablePayload(payload) {
|