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.
- package/benchmarks/latency.ts +1 -1
- package/benchmarks/stampede.ts +1 -4
- package/dist/{chunk-IILH5XTS.js → chunk-QUB5VZFZ.js} +33 -4
- package/dist/cli.cjs +75 -7
- package/dist/cli.js +43 -4
- package/dist/index.cjs +894 -240
- package/dist/index.d.cts +291 -11
- package/dist/index.d.ts +291 -11
- package/dist/index.js +858 -236
- package/examples/express-api/index.ts +12 -8
- package/examples/nestjs-module/app.module.ts +2 -5
- package/examples/nextjs-api-routes/route.ts +1 -4
- package/package.json +6 -1
- package/packages/nestjs/dist/index.cjs +552 -220
- package/packages/nestjs/dist/index.d.cts +151 -10
- package/packages/nestjs/dist/index.d.ts +151 -10
- package/packages/nestjs/dist/index.js +552 -220
package/benchmarks/latency.ts
CHANGED
package/benchmarks/stampede.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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.")) {
|