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.
- package/README.md +110 -8
- package/dist/chunk-46UH7LNM.js +312 -0
- package/dist/{chunk-BWM4MU2X.js → chunk-IXCMHVHP.js} +62 -56
- package/dist/chunk-ZMDB5KOK.js +159 -0
- package/dist/cli.cjs +170 -39
- package/dist/cli.js +57 -2
- package/dist/edge-DLpdQN0W.d.cts +672 -0
- package/dist/edge-DLpdQN0W.d.ts +672 -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 +1173 -221
- package/dist/index.d.cts +51 -568
- package/dist/index.d.ts +51 -568
- package/dist/index.js +1005 -505
- package/package.json +8 -3
- package/packages/nestjs/dist/index.cjs +980 -370
- package/packages/nestjs/dist/index.d.cts +80 -0
- package/packages/nestjs/dist/index.d.ts +80 -0
- package/packages/nestjs/dist/index.js +968 -368
|
@@ -1,59 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
}
|