@veryfront/ext-redis 0.1.1185 → 0.1.1189
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 +12 -0
- package/esm/_dnt.polyfills.d.ts +12 -0
- package/esm/_dnt.polyfills.d.ts.map +1 -0
- package/esm/_dnt.polyfills.js +15 -0
- package/esm/_dnt.shims.d.ts +11 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/_dnt.shims.js +68 -0
- package/esm/agent-memory.d.ts +60 -0
- package/esm/agent-memory.d.ts.map +1 -0
- package/esm/agent-memory.js +173 -0
- package/esm/cache-administration.d.ts +5 -0
- package/esm/cache-administration.d.ts.map +1 -0
- package/esm/cache-administration.js +113 -0
- package/esm/cache-backend.d.ts +46 -0
- package/esm/cache-backend.d.ts.map +1 -0
- package/esm/cache-backend.js +778 -0
- package/esm/index.d.ts +7 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +7 -0
- package/esm/rate-limit-store.d.ts +40 -0
- package/esm/rate-limit-store.d.ts.map +1 -0
- package/esm/rate-limit-store.js +306 -0
- package/esm/redis-client-manager.d.ts +8 -0
- package/esm/redis-client-manager.d.ts.map +1 -1
- package/esm/redis-client-manager.js +22 -1
- package/esm/redis-runtime-provider.js +1 -1
- package/esm/render-cache-store.d.ts +34 -0
- package/esm/render-cache-store.d.ts.map +1 -0
- package/esm/render-cache-store.js +191 -0
- package/esm/revisioned-cache-record.d.ts +18 -0
- package/esm/revisioned-cache-record.d.ts.map +1 -0
- package/esm/revisioned-cache-record.js +68 -0
- package/esm/routing-invalidation-bus.d.ts +33 -0
- package/esm/routing-invalidation-bus.d.ts.map +1 -0
- package/esm/routing-invalidation-bus.js +760 -0
- package/package.json +7 -4
package/esm/index.d.ts
CHANGED
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module extensions/ext-redis
|
|
5
5
|
*/
|
|
6
|
+
import "./_dnt.polyfills.js";
|
|
6
7
|
import type { ExtensionFactory } from "veryfront/extensions/types";
|
|
7
8
|
declare const extRedis: ExtensionFactory;
|
|
8
9
|
export default extRedis;
|
|
10
|
+
export { RedisMemory } from "./agent-memory.js";
|
|
11
|
+
export { createRedisCacheAdministration } from "./cache-administration.js";
|
|
12
|
+
export { RedisCacheBackend } from "./cache-backend.js";
|
|
13
|
+
export { RedisRateLimitStore } from "./rate-limit-store.js";
|
|
14
|
+
export { RedisCacheStore } from "./render-cache-store.js";
|
|
15
|
+
export { startProxyRoutingInvalidationBus } from "./routing-invalidation-bus.js";
|
|
9
16
|
export { createRedisRuntimeProvider } from "./redis-runtime-provider.js";
|
|
10
17
|
export type { RedisClient, RedisClientOptions, RedisEventPublisherConfig, RedisEventPublisherImplementation, RedisRuntimeProvider, } from "veryfront/extensions/distributed";
|
|
11
18
|
//# sourceMappingURL=index.d.ts.map
|
package/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAOnE,QAAA,MAAM,QAAQ,EAAE,gBAqCf,CAAC;AAEF,eAAe,QAAQ,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,8BAA8B,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,gCAAgC,EAAE,MAAM,+BAA+B,CAAC;AACjF,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AACzE,YAAY,EACV,WAAW,EACX,kBAAkB,EAClB,yBAAyB,EACzB,iCAAiC,EACjC,oBAAoB,GACrB,MAAM,kCAAkC,CAAC"}
|
package/esm/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module extensions/ext-redis
|
|
5
5
|
*/
|
|
6
|
+
import "./_dnt.polyfills.js";
|
|
6
7
|
import { RedisRuntimeProviderName, } from "veryfront/extensions/distributed";
|
|
7
8
|
import { createRedisRuntimeProvider } from "./redis-runtime-provider.js";
|
|
8
9
|
const extRedis = () => {
|
|
@@ -48,4 +49,10 @@ const extRedis = () => {
|
|
|
48
49
|
};
|
|
49
50
|
};
|
|
50
51
|
export default extRedis;
|
|
52
|
+
export { RedisMemory } from "./agent-memory.js";
|
|
53
|
+
export { createRedisCacheAdministration } from "./cache-administration.js";
|
|
54
|
+
export { RedisCacheBackend } from "./cache-backend.js";
|
|
55
|
+
export { RedisRateLimitStore } from "./rate-limit-store.js";
|
|
56
|
+
export { RedisCacheStore } from "./render-cache-store.js";
|
|
57
|
+
export { startProxyRoutingInvalidationBus } from "./routing-invalidation-bus.js";
|
|
51
58
|
export { createRedisRuntimeProvider } from "./redis-runtime-provider.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type RateLimitEntry, type RateLimitStore } from "veryfront/extensions/distributed/rate-limit-support";
|
|
2
|
+
/** Options accepted by redis rate limit. */
|
|
3
|
+
export interface RedisRateLimitOptions {
|
|
4
|
+
url?: string;
|
|
5
|
+
keyPrefix?: string;
|
|
6
|
+
/** Maximum time allowed for loading and connecting the Redis client. */
|
|
7
|
+
connectTimeoutMs?: number;
|
|
8
|
+
/** Maximum time allowed for an individual Redis command. */
|
|
9
|
+
operationTimeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
/** Implement redis rate limit store. */
|
|
12
|
+
export declare class RedisRateLimitStore implements RateLimitStore {
|
|
13
|
+
private client;
|
|
14
|
+
private connectingClient;
|
|
15
|
+
private clientPromise;
|
|
16
|
+
private cancelPendingConnection;
|
|
17
|
+
private clientGeneration;
|
|
18
|
+
private readonly disconnectPromises;
|
|
19
|
+
private readonly disconnectedClients;
|
|
20
|
+
private readonly pendingDisconnectClients;
|
|
21
|
+
private readonly reportedClientErrors;
|
|
22
|
+
private readonly url?;
|
|
23
|
+
private readonly keyPrefix;
|
|
24
|
+
private readonly connectTimeoutMs;
|
|
25
|
+
private readonly operationTimeoutMs;
|
|
26
|
+
constructor(options?: RedisRateLimitOptions);
|
|
27
|
+
private ensureClient;
|
|
28
|
+
private invalidateClient;
|
|
29
|
+
private attachClientLifecycleHandlers;
|
|
30
|
+
private loadClientFactory;
|
|
31
|
+
private disconnectBestEffort;
|
|
32
|
+
private disconnectClient;
|
|
33
|
+
private withTimeout;
|
|
34
|
+
private connectClient;
|
|
35
|
+
private storageKey;
|
|
36
|
+
increment(key: string, windowMs: number): Promise<RateLimitEntry>;
|
|
37
|
+
reset(key: string): Promise<void>;
|
|
38
|
+
destroy(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=rate-limit-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit-store.d.ts","sourceRoot":"","sources":["../src/rate-limit-store.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,cAAc,EAGpB,MAAM,qDAAqD,CAAC;AAyC7D,4CAA4C;AAC5C,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,wCAAwC;AACxC,qBAAa,mBAAoB,YAAW,cAAc;IACxD,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,aAAa,CAAqC;IAC1D,OAAO,CAAC,uBAAuB,CAA6B;IAC5D,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6C;IAChF,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA8B;IAClE,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAA0B;IACnE,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAA8B;IACnE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;gBAEhC,OAAO,GAAE,qBAA0B;IA0B/C,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,gBAAgB;IAiBxB,OAAO,CAAC,6BAA6B;YAoBvB,iBAAiB;YAIjB,oBAAoB;IAUlC,OAAO,CAAC,gBAAgB;YAmCV,WAAW;YAsBX,aAAa;IAgE3B,OAAO,CAAC,UAAU;IAIZ,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IA6BjE,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBjC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CA6B/B"}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import * as dntShim from "./_dnt.shims.js";
|
|
2
|
+
import { createError, toError } from "veryfront/errors";
|
|
3
|
+
import { serverLogger } from "veryfront/utils/logger";
|
|
4
|
+
import { createClient } from "redis";
|
|
5
|
+
import { MAX_TIMER_DELAY_MS, requireRateLimitKey, requireRateLimitWindowMs, } from "veryfront/extensions/distributed/rate-limit-support";
|
|
6
|
+
const logger = serverLogger.component("redis-ratelimit");
|
|
7
|
+
const DEFAULT_REDIS_CONNECT_TIMEOUT_MS = 5_000;
|
|
8
|
+
const DEFAULT_REDIS_OPERATION_TIMEOUT_MS = 5_000;
|
|
9
|
+
const INCREMENT_WITH_TTL_SCRIPT = `
|
|
10
|
+
local count = redis.call("INCR", KEYS[1])
|
|
11
|
+
local ttl = redis.call("PTTL", KEYS[1])
|
|
12
|
+
if ttl < 0 then
|
|
13
|
+
redis.call("PEXPIRE", KEYS[1], ARGV[1])
|
|
14
|
+
ttl = tonumber(ARGV[1])
|
|
15
|
+
end
|
|
16
|
+
return { count, ttl }
|
|
17
|
+
`;
|
|
18
|
+
/** Implement redis rate limit store. */
|
|
19
|
+
export class RedisRateLimitStore {
|
|
20
|
+
client = null;
|
|
21
|
+
connectingClient = null;
|
|
22
|
+
clientPromise = null;
|
|
23
|
+
cancelPendingConnection = null;
|
|
24
|
+
clientGeneration = 0;
|
|
25
|
+
disconnectPromises = new WeakMap();
|
|
26
|
+
disconnectedClients = new WeakSet();
|
|
27
|
+
pendingDisconnectClients = new Set();
|
|
28
|
+
reportedClientErrors = new WeakSet();
|
|
29
|
+
url;
|
|
30
|
+
keyPrefix;
|
|
31
|
+
connectTimeoutMs;
|
|
32
|
+
operationTimeoutMs;
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
if (typeof options !== "object" ||
|
|
35
|
+
options === null ||
|
|
36
|
+
Array.isArray(options)) {
|
|
37
|
+
throw new TypeError("Redis rate limit options must be an object");
|
|
38
|
+
}
|
|
39
|
+
if (options.url !== undefined && typeof options.url !== "string") {
|
|
40
|
+
throw new TypeError("Redis rate limit url must be a string");
|
|
41
|
+
}
|
|
42
|
+
this.url = options.url;
|
|
43
|
+
this.connectTimeoutMs = requireTimeoutMs(options.connectTimeoutMs ?? DEFAULT_REDIS_CONNECT_TIMEOUT_MS, "connectTimeoutMs");
|
|
44
|
+
this.operationTimeoutMs = requireTimeoutMs(options.operationTimeoutMs ?? DEFAULT_REDIS_OPERATION_TIMEOUT_MS, "operationTimeoutMs");
|
|
45
|
+
this.keyPrefix = requireRateLimitKey(options.keyPrefix ?? "veryfront:ratelimit:", "Redis rate limit keyPrefix");
|
|
46
|
+
}
|
|
47
|
+
ensureClient() {
|
|
48
|
+
if (this.client)
|
|
49
|
+
return Promise.resolve(this.client);
|
|
50
|
+
if (this.clientPromise)
|
|
51
|
+
return this.clientPromise;
|
|
52
|
+
const generation = this.clientGeneration;
|
|
53
|
+
const pending = this.connectClient(generation).finally(() => {
|
|
54
|
+
if (this.clientPromise === pending)
|
|
55
|
+
this.clientPromise = null;
|
|
56
|
+
});
|
|
57
|
+
this.clientPromise = pending;
|
|
58
|
+
return pending;
|
|
59
|
+
}
|
|
60
|
+
invalidateClient(client, generation, disconnect) {
|
|
61
|
+
if (generation !== this.clientGeneration)
|
|
62
|
+
return;
|
|
63
|
+
if (this.client !== client)
|
|
64
|
+
return;
|
|
65
|
+
this.clientGeneration++;
|
|
66
|
+
this.client = null;
|
|
67
|
+
this.clientPromise = null;
|
|
68
|
+
if (disconnect) {
|
|
69
|
+
void this.disconnectBestEffort(client);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
attachClientLifecycleHandlers(client, generation = this.clientGeneration) {
|
|
73
|
+
client.on?.("error", (err) => {
|
|
74
|
+
if (generation !== this.clientGeneration)
|
|
75
|
+
return;
|
|
76
|
+
if (!this.reportedClientErrors.has(client)) {
|
|
77
|
+
this.reportedClientErrors.add(client);
|
|
78
|
+
logger.error("client error", {
|
|
79
|
+
errorName: err instanceof Error ? err.name : typeof err,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
this.invalidateClient(client, generation, true);
|
|
83
|
+
});
|
|
84
|
+
client.on?.("end", () => {
|
|
85
|
+
this.invalidateClient(client, generation, false);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async loadClientFactory() {
|
|
89
|
+
return createClient;
|
|
90
|
+
}
|
|
91
|
+
async disconnectBestEffort(client) {
|
|
92
|
+
try {
|
|
93
|
+
await this.disconnectClient(client);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
logger.warn("client disconnect failed", {
|
|
97
|
+
errorName: error instanceof Error ? error.name : typeof error,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
disconnectClient(client) {
|
|
102
|
+
if (this.disconnectedClients.has(client))
|
|
103
|
+
return Promise.resolve();
|
|
104
|
+
const existing = this.disconnectPromises.get(client);
|
|
105
|
+
if (existing)
|
|
106
|
+
return existing;
|
|
107
|
+
let resolveDisconnect;
|
|
108
|
+
let rejectDisconnect;
|
|
109
|
+
const pending = new Promise((resolve, reject) => {
|
|
110
|
+
resolveDisconnect = resolve;
|
|
111
|
+
rejectDisconnect = reject;
|
|
112
|
+
});
|
|
113
|
+
this.disconnectPromises.set(client, pending);
|
|
114
|
+
this.pendingDisconnectClients.add(client);
|
|
115
|
+
try {
|
|
116
|
+
Promise.resolve(client.disconnect()).then(() => {
|
|
117
|
+
this.disconnectPromises.delete(client);
|
|
118
|
+
this.pendingDisconnectClients.delete(client);
|
|
119
|
+
this.disconnectedClients.add(client);
|
|
120
|
+
resolveDisconnect();
|
|
121
|
+
}, (error) => {
|
|
122
|
+
this.disconnectPromises.delete(client);
|
|
123
|
+
rejectDisconnect(error);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
this.disconnectPromises.delete(client);
|
|
128
|
+
rejectDisconnect(error);
|
|
129
|
+
}
|
|
130
|
+
return pending;
|
|
131
|
+
}
|
|
132
|
+
async withTimeout(operation, timeoutMs, operationName, cancellation) {
|
|
133
|
+
let timeoutId;
|
|
134
|
+
const timeout = new Promise((_, reject) => {
|
|
135
|
+
timeoutId = dntShim.setTimeout(() => {
|
|
136
|
+
reject(createTimeoutError(operationName, timeoutMs));
|
|
137
|
+
}, timeoutMs);
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
return await Promise.race(cancellation === undefined ? [operation, timeout] : [operation, timeout, cancellation]);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
if (timeoutId !== undefined)
|
|
144
|
+
clearTimeout(timeoutId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async connectClient(generation) {
|
|
148
|
+
const superseded = new Error("Redis rate limit client connection was superseded");
|
|
149
|
+
superseded.name = "AbortError";
|
|
150
|
+
let cancelConnection;
|
|
151
|
+
let cancelled = false;
|
|
152
|
+
const cancellation = new Promise((_, reject) => {
|
|
153
|
+
cancelConnection = () => {
|
|
154
|
+
if (cancelled)
|
|
155
|
+
return;
|
|
156
|
+
cancelled = true;
|
|
157
|
+
reject(superseded);
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
this.cancelPendingConnection = cancelConnection;
|
|
161
|
+
let client;
|
|
162
|
+
try {
|
|
163
|
+
const createClient = await this.withTimeout(this.loadClientFactory(), this.connectTimeoutMs, "client loading", cancellation);
|
|
164
|
+
if (generation !== this.clientGeneration)
|
|
165
|
+
throw superseded;
|
|
166
|
+
client = createClient({
|
|
167
|
+
...(this.url === undefined ? {} : { url: this.url }),
|
|
168
|
+
socket: {
|
|
169
|
+
connectTimeout: this.connectTimeoutMs,
|
|
170
|
+
reconnectStrategy: false,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
this.connectingClient = client;
|
|
174
|
+
this.attachClientLifecycleHandlers(client, generation);
|
|
175
|
+
await this.withTimeout(client.connect(), this.connectTimeoutMs, "connection", cancellation);
|
|
176
|
+
if (this.connectingClient === client)
|
|
177
|
+
this.connectingClient = null;
|
|
178
|
+
if (generation !== this.clientGeneration) {
|
|
179
|
+
await this.disconnectBestEffort(client);
|
|
180
|
+
throw superseded;
|
|
181
|
+
}
|
|
182
|
+
this.client = client;
|
|
183
|
+
return client;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (client && this.connectingClient === client) {
|
|
187
|
+
this.connectingClient = null;
|
|
188
|
+
}
|
|
189
|
+
if (generation === this.clientGeneration)
|
|
190
|
+
this.clientGeneration++;
|
|
191
|
+
if (client)
|
|
192
|
+
await this.disconnectBestEffort(client);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
if (this.cancelPendingConnection === cancelConnection) {
|
|
197
|
+
this.cancelPendingConnection = null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
storageKey(key) {
|
|
202
|
+
return `${this.keyPrefix}${key}`;
|
|
203
|
+
}
|
|
204
|
+
async increment(key, windowMs) {
|
|
205
|
+
const normalizedKey = requireRateLimitKey(key);
|
|
206
|
+
const normalizedWindowMs = requireRateLimitWindowMs(windowMs);
|
|
207
|
+
const client = await this.ensureClient();
|
|
208
|
+
const generation = this.clientGeneration;
|
|
209
|
+
const redisKey = this.storageKey(normalizedKey);
|
|
210
|
+
let result;
|
|
211
|
+
try {
|
|
212
|
+
result = await this.withTimeout(client.eval(INCREMENT_WITH_TTL_SCRIPT, {
|
|
213
|
+
keys: [redisKey],
|
|
214
|
+
arguments: [String(normalizedWindowMs)],
|
|
215
|
+
}), this.operationTimeoutMs, "increment");
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (isTimeoutError(error)) {
|
|
219
|
+
this.invalidateClient(client, generation, true);
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
const [count, pttl] = parseIncrementResult(result);
|
|
224
|
+
const ttl = pttl > 0 ? requireRateLimitWindowMs(pttl) : normalizedWindowMs;
|
|
225
|
+
return { count, resetAt: Date.now() + ttl };
|
|
226
|
+
}
|
|
227
|
+
async reset(key) {
|
|
228
|
+
const client = await this.ensureClient();
|
|
229
|
+
const generation = this.clientGeneration;
|
|
230
|
+
try {
|
|
231
|
+
await this.withTimeout(client.del(this.storageKey(requireRateLimitKey(key))), this.operationTimeoutMs, "reset");
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (isTimeoutError(error)) {
|
|
235
|
+
this.invalidateClient(client, generation, true);
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async destroy() {
|
|
241
|
+
const client = this.client;
|
|
242
|
+
const connectingClient = this.connectingClient;
|
|
243
|
+
const pending = this.clientPromise;
|
|
244
|
+
const cancelPendingConnection = this.cancelPendingConnection;
|
|
245
|
+
const clientsToDisconnect = new Set(this.pendingDisconnectClients);
|
|
246
|
+
if (client)
|
|
247
|
+
clientsToDisconnect.add(client);
|
|
248
|
+
if (connectingClient)
|
|
249
|
+
clientsToDisconnect.add(connectingClient);
|
|
250
|
+
this.clientGeneration++;
|
|
251
|
+
this.client = null;
|
|
252
|
+
this.connectingClient = null;
|
|
253
|
+
this.clientPromise = null;
|
|
254
|
+
this.cancelPendingConnection = null;
|
|
255
|
+
cancelPendingConnection?.();
|
|
256
|
+
let disconnectFailed = false;
|
|
257
|
+
let disconnectError;
|
|
258
|
+
await Promise.all([...clientsToDisconnect].map((clientToDisconnect) => this.disconnectClient(clientToDisconnect).catch((error) => {
|
|
259
|
+
disconnectFailed = true;
|
|
260
|
+
disconnectError ??= error;
|
|
261
|
+
})));
|
|
262
|
+
pending?.catch(() => { });
|
|
263
|
+
if (disconnectFailed)
|
|
264
|
+
throw disconnectError;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function requireTimeoutMs(value, name) {
|
|
268
|
+
if (typeof value !== "number" ||
|
|
269
|
+
!Number.isSafeInteger(value) ||
|
|
270
|
+
value <= 0 ||
|
|
271
|
+
value > MAX_TIMER_DELAY_MS) {
|
|
272
|
+
throw new RangeError(`Redis rate limit ${name} must be an integer between 1 and ${MAX_TIMER_DELAY_MS}`);
|
|
273
|
+
}
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
function createTimeoutError(operationName, timeoutMs) {
|
|
277
|
+
const error = new Error(`Redis rate limit ${operationName} timed out after ${timeoutMs}ms`);
|
|
278
|
+
error.name = "TimeoutError";
|
|
279
|
+
return error;
|
|
280
|
+
}
|
|
281
|
+
function isTimeoutError(error) {
|
|
282
|
+
return error instanceof Error && error.name === "TimeoutError";
|
|
283
|
+
}
|
|
284
|
+
function parseIncrementResult(result) {
|
|
285
|
+
if (!Array.isArray(result) || result.length < 2) {
|
|
286
|
+
throw toError(createError({
|
|
287
|
+
type: "config",
|
|
288
|
+
message: "Redis rate limit eval returned an invalid result.",
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
const count = Number(result[0]);
|
|
292
|
+
const ttl = Number(result[1]);
|
|
293
|
+
if (!Number.isSafeInteger(count) || count < 1) {
|
|
294
|
+
throw toError(createError({
|
|
295
|
+
type: "config",
|
|
296
|
+
message: "Redis rate limit eval returned an invalid count.",
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
if (!Number.isSafeInteger(ttl)) {
|
|
300
|
+
throw toError(createError({
|
|
301
|
+
type: "config",
|
|
302
|
+
message: "Redis rate limit eval returned an invalid TTL.",
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
return [count, ttl];
|
|
306
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RedisClient, RedisClientOptions } from "veryfront/extensions/distributed";
|
|
2
|
+
export type { RedisClient, RedisClientOptions } from "veryfront/extensions/distributed";
|
|
2
3
|
/** Minimal `@redis/client` factory options used by this extension. */
|
|
3
4
|
export interface RedisClientFactoryOptions {
|
|
4
5
|
url?: string;
|
|
@@ -23,6 +24,7 @@ export interface RedisClientOpenLifecycle {
|
|
|
23
24
|
export interface RedisClientManager {
|
|
24
25
|
getClient(options?: RedisClientOptions): Promise<RedisClient>;
|
|
25
26
|
disconnect(): Promise<void>;
|
|
27
|
+
isConfigured(options?: RedisClientOptions): boolean;
|
|
26
28
|
}
|
|
27
29
|
/** Internal handoff of a provisional client that still requires cleanup. */
|
|
28
30
|
export declare function takeRedisClientSetupCleanupClient(error: unknown): RedisClient | undefined;
|
|
@@ -30,4 +32,10 @@ export declare function takeRedisClientSetupCleanupClient(error: unknown): Redis
|
|
|
30
32
|
export declare function openRedisClient(options?: RedisClientOptions, dependencies?: RedisClientManagerDependencies, lifecycle?: RedisClientOpenLifecycle): Promise<RedisClient>;
|
|
31
33
|
/** Create an isolated, concurrency-safe Redis client manager. */
|
|
32
34
|
export declare function createRedisClientManager(dependencies?: RedisClientManagerDependencies): RedisClientManager;
|
|
35
|
+
/** Acquire a connection from the extension-owned shared manager. */
|
|
36
|
+
export declare function getRedisClient(options?: RedisClientOptions): Promise<RedisClient>;
|
|
37
|
+
/** Disconnect the shared manager so the next call reconnects from a clean state. */
|
|
38
|
+
export declare function disconnectRedisClient(): Promise<void>;
|
|
39
|
+
/** Report whether the shared manager has an explicit Redis endpoint. */
|
|
40
|
+
export declare function isRedisConfigured(options?: RedisClientOptions): boolean;
|
|
33
41
|
//# sourceMappingURL=redis-client-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis-client-manager.d.ts","sourceRoot":"","sources":["../src/redis-client-manager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAyBxF,sEAAsE;AACtE,MAAM,WAAW,yBAAyB;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE;QACP,GAAG,CAAC,EAAE,OAAO,CAAC;QACd,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,iBAAiB,CAAC,EAAE,KAAK,CAAC;KAC3B,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,yBAAyB,KAAK,WAAW,CAAC;AAErF,MAAM,WAAW,8BAA8B;IAC7C,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC7C,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAChD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,CAAC,MAAM,EAAE,WAAW,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;CACrE;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9D,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"redis-client-manager.d.ts","sourceRoot":"","sources":["../src/redis-client-manager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AACxF,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAyBxF,sEAAsE;AACtE,MAAM,WAAW,yBAAyB;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE;QACP,GAAG,CAAC,EAAE,OAAO,CAAC;QACd,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,iBAAiB,CAAC,EAAE,KAAK,CAAC;KAC3B,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,yBAAyB,KAAK,WAAW,CAAC;AAErF,MAAM,WAAW,8BAA8B;IAC7C,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC7C,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAChD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,eAAe,CAAC,CAAC,MAAM,EAAE,WAAW,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;CACrE;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9D,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,YAAY,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC;CACrD;AAeD,4EAA4E;AAC5E,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAKzF;AAuOD,qDAAqD;AACrD,wBAAsB,eAAe,CACnC,OAAO,GAAE,kBAAuB,EAChC,YAAY,GAAE,8BAAmC,EACjD,SAAS,GAAE,wBAA6B,GACvC,OAAO,CAAC,WAAW,CAAC,CA4DtB;AAOD,iEAAiE;AACjE,wBAAgB,wBAAwB,CACtC,YAAY,GAAE,8BAAmC,GAChD,kBAAkB,CAqRpB;AAID,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC,CAErF;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAErD;AAED,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAE3E"}
|
|
@@ -531,5 +531,26 @@ export function createRedisClientManager(dependencies = {}) {
|
|
|
531
531
|
disconnecting = tracked;
|
|
532
532
|
return tracked;
|
|
533
533
|
}
|
|
534
|
-
|
|
534
|
+
function isConfigured(options = {}) {
|
|
535
|
+
const capturedOptions = captureClientOptions(options);
|
|
536
|
+
const configured = capturedOptions.url ?? readEnv("REDIS_URL");
|
|
537
|
+
if (configured === undefined || configured.length === 0)
|
|
538
|
+
return false;
|
|
539
|
+
requireRedisUrl(configured);
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
return { getClient, disconnect, isConfigured };
|
|
543
|
+
}
|
|
544
|
+
const defaultManager = createRedisClientManager();
|
|
545
|
+
/** Acquire a connection from the extension-owned shared manager. */
|
|
546
|
+
export function getRedisClient(options = {}) {
|
|
547
|
+
return defaultManager.getClient(options);
|
|
548
|
+
}
|
|
549
|
+
/** Disconnect the shared manager so the next call reconnects from a clean state. */
|
|
550
|
+
export function disconnectRedisClient() {
|
|
551
|
+
return defaultManager.disconnect();
|
|
552
|
+
}
|
|
553
|
+
/** Report whether the shared manager has an explicit Redis endpoint. */
|
|
554
|
+
export function isRedisConfigured(options = {}) {
|
|
555
|
+
return defaultManager.isConfigured(options);
|
|
535
556
|
}
|
|
@@ -47,7 +47,7 @@ const CLIENT_METHODS = [
|
|
|
47
47
|
"pExpire",
|
|
48
48
|
"pTTL",
|
|
49
49
|
];
|
|
50
|
-
const OPTIONAL_CLIENT_METHODS = ["ttl", "on"];
|
|
50
|
+
const OPTIONAL_CLIENT_METHODS = ["ttl", "info", "on"];
|
|
51
51
|
function readOptionalClientMethod(target, name) {
|
|
52
52
|
let owner = target;
|
|
53
53
|
const visited = new Set();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type CachePayload, type RenderCacheStore as CacheStore } from "veryfront/extensions/distributed/cache-support";
|
|
2
|
+
import { type RedisClientManager } from "./redis-client-manager.js";
|
|
3
|
+
export interface RedisCacheStoreOptions {
|
|
4
|
+
url?: string;
|
|
5
|
+
connectTimeoutMs?: number;
|
|
6
|
+
/** Canonical provider-neutral cache namespace. */
|
|
7
|
+
keyPrefix?: string;
|
|
8
|
+
/** TTL in seconds for cache entries (default: 3600 = 1 hour) */
|
|
9
|
+
ttlSeconds?: number;
|
|
10
|
+
/** Optional connection manager for embedding/tests. */
|
|
11
|
+
clientManager?: RedisClientManager;
|
|
12
|
+
}
|
|
13
|
+
export declare class RedisCacheStore implements CacheStore {
|
|
14
|
+
private readonly connectionOptions;
|
|
15
|
+
private readonly keyPrefix;
|
|
16
|
+
private readonly ttlSeconds;
|
|
17
|
+
private readonly clientManager;
|
|
18
|
+
constructor(options?: RedisCacheStoreOptions);
|
|
19
|
+
private ensureClient;
|
|
20
|
+
private storageKey;
|
|
21
|
+
private resetRedisConnection;
|
|
22
|
+
private resetAfterFailure;
|
|
23
|
+
private scanKeys;
|
|
24
|
+
private resolveRetentionTtlSeconds;
|
|
25
|
+
private deleteKeys;
|
|
26
|
+
private deleteRedisKey;
|
|
27
|
+
get(key: string): Promise<CachePayload | undefined>;
|
|
28
|
+
set(key: string, value: CachePayload): Promise<void>;
|
|
29
|
+
delete(key: string): Promise<void>;
|
|
30
|
+
deleteByPrefix(prefix: string): Promise<number>;
|
|
31
|
+
clear(): Promise<void>;
|
|
32
|
+
destroy(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=render-cache-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-cache-store.d.ts","sourceRoot":"","sources":["../src/render-cache-store.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,YAAY,EAIjB,KAAK,gBAAgB,IAAI,UAAU,EAGpC,MAAM,gDAAgD,CAAC;AACxD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AAcnC,MAAM,WAAW,sBAAsB;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC;AAED,qBAAa,eAAgB,YAAW,UAAU;IAChD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAsD;IACxF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;gBAEvC,OAAO,GAAE,sBAA2B;YAclC,YAAY;IAI1B,OAAO,CAAC,UAAU;YAIJ,oBAAoB;YAIpB,iBAAiB;YAQjB,QAAQ;IA4CtB,OAAO,CAAC,0BAA0B;YAYpB,UAAU;YAaV,cAAc;IAOtB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAiBnD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWlC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAa/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/B"}
|