@veryfront/ext-cache-redis 0.1.1288-rc.25587 → 0.1.1288-rc.25610
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
CHANGED
|
@@ -76,6 +76,14 @@ ASCII characters and cannot contain Redis glob metacharacters (`*`, `?`, `[`,
|
|
|
76
76
|
`has(key)`, `stats()`, and `close()`. Entry expiry is carried in
|
|
77
77
|
`entry.expiresAt`. The proxy uses the contract for OAuth service-token caching.
|
|
78
78
|
|
|
79
|
+
When a SET receives the exact Redis replica-write refusal, the store disposes
|
|
80
|
+
the old client and retries that write once through a fresh connection to the
|
|
81
|
+
configured writer URL. The retry uses the same entry and its remaining lifetime;
|
|
82
|
+
it does not mint another token. Other operation errors and a repeated refusal
|
|
83
|
+
fail closed. Concurrent operations share connection startup, and detached client
|
|
84
|
+
errors cannot invalidate a replacement. `close()` disposes the client and is
|
|
85
|
+
final: subsequent operations cannot reopen the store.
|
|
86
|
+
|
|
79
87
|
## Capabilities
|
|
80
88
|
|
|
81
89
|
- **net `*`:** Redis connection. Narrow to a specific host in your own deployment policy if you're not using a wildcard.
|
|
@@ -40,6 +40,8 @@ export declare class RedisTokenCacheStore implements TokenCacheStore {
|
|
|
40
40
|
private hits;
|
|
41
41
|
private misses;
|
|
42
42
|
private connected;
|
|
43
|
+
private connecting;
|
|
44
|
+
private closed;
|
|
43
45
|
constructor(options: RedisTokenCacheStoreOptions, deps?: {
|
|
44
46
|
logger?: RedisCacheLogger;
|
|
45
47
|
clientFactory?: RedisClientFactory;
|
|
@@ -52,7 +54,9 @@ export declare class RedisTokenCacheStore implements TokenCacheStore {
|
|
|
52
54
|
has(key: string): Promise<boolean>;
|
|
53
55
|
stats(): Promise<TokenCacheStats>;
|
|
54
56
|
close(): Promise<void>;
|
|
57
|
+
/** Detach before destruction so late socket errors cannot affect its successor. */
|
|
58
|
+
private invalidate;
|
|
55
59
|
private getConnectedClient;
|
|
56
|
-
private
|
|
60
|
+
private connectClient;
|
|
57
61
|
}
|
|
58
62
|
//# sourceMappingURL=redis-cache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis-cache.d.ts","sourceRoot":"","sources":["../../../../src/extensions/ext-cache-redis/src/redis-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAgDpG,sDAAsD;AACtD,MAAM,WAAW,2BAA2B;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AACD,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACjD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAClD;AAkBD,kEAAkE;AAClE,MAAM,MAAM,kBAAkB,GAAG,CAE/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KACtB,eAAe,CAAC;AAErB,qBAAa,oBAAqB,YAAW,eAAe;IAC1D,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAU;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,SAAS,CAAS;
|
|
1
|
+
{"version":3,"file":"redis-cache.d.ts","sourceRoot":"","sources":["../../../../src/extensions/ext-cache-redis/src/redis-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAgDpG,sDAAsD;AACtD,MAAM,WAAW,2BAA2B;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AACD,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACjD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAClD;AAkBD,kEAAkE;AAClE,MAAM,MAAM,kBAAkB,GAAG,CAE/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KACtB,eAAe,CAAC;AAErB,qBAAa,oBAAqB,YAAW,eAAe;IAC1D,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAU;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAyC;IAC3D,OAAO,CAAC,MAAM,CAAS;gBAGrB,OAAO,EAAE,2BAA2B,EACpC,IAAI,GAAE;QACJ,MAAM,CAAC,EAAE,gBAAgB,CAAC;QAC1B,aAAa,CAAC,EAAE,kBAAkB,CAAC;KAC/B;IAYR,OAAO,CAAC,GAAG;IAIL,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IA+BjD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BvD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAclC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAuCtB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAclC,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC;IAiBjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B,mFAAmF;IACnF,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,kBAAkB;YAYZ,aAAa;CA8C5B"}
|
|
@@ -69,6 +69,8 @@ export class RedisTokenCacheStore {
|
|
|
69
69
|
hits = 0;
|
|
70
70
|
misses = 0;
|
|
71
71
|
connected = false;
|
|
72
|
+
connecting = null;
|
|
73
|
+
closed = false;
|
|
72
74
|
constructor(options, deps = {}) {
|
|
73
75
|
this.url = options.url;
|
|
74
76
|
this.prefix = validatePrefix(options.prefix ?? DEFAULT_PREFIX);
|
|
@@ -83,8 +85,9 @@ export class RedisTokenCacheStore {
|
|
|
83
85
|
return `${this.prefix}${k}`;
|
|
84
86
|
}
|
|
85
87
|
async get(key) {
|
|
88
|
+
let client = null;
|
|
86
89
|
try {
|
|
87
|
-
|
|
90
|
+
client = await this.getConnectedClient();
|
|
88
91
|
const data = await client.get(this.key(key));
|
|
89
92
|
if (!data) {
|
|
90
93
|
this.misses++;
|
|
@@ -103,42 +106,56 @@ export class RedisTokenCacheStore {
|
|
|
103
106
|
this.logger.error("[RedisCache] Get error", {
|
|
104
107
|
error: error instanceof Error ? error.message : String(error),
|
|
105
108
|
});
|
|
106
|
-
this.
|
|
109
|
+
this.invalidate(client);
|
|
107
110
|
this.misses++;
|
|
108
111
|
throw error;
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
async set(key, entry) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
115
|
+
// Reuse the already minted token: only an explicit replica refusal proves
|
|
116
|
+
// that retrying this SET cannot duplicate an ambiguously accepted write.
|
|
117
|
+
const value = JSON.stringify(entry);
|
|
118
|
+
const expiresAt = entry.expiresAt;
|
|
119
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
120
|
+
let client = null;
|
|
121
|
+
try {
|
|
122
|
+
client = await this.getConnectedClient();
|
|
123
|
+
const ttlMs = expiresAt - Date.now();
|
|
124
|
+
if (attempt > 0 && ttlMs <= 0) {
|
|
125
|
+
throw new Error("Redis token-cache entry expired during replica recovery");
|
|
126
|
+
}
|
|
127
|
+
const ttlSeconds = Math.max(1, Math.floor(ttlMs / 1000));
|
|
128
|
+
await client.setEx(this.key(key), ttlSeconds, value);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
this.logger.error("[RedisCache] Set error", { error: getErrorMessage(error) });
|
|
133
|
+
this.invalidate(client);
|
|
134
|
+
if (attempt === 0 && client !== null && error instanceof Error &&
|
|
135
|
+
error.message === "READONLY You can't write against a read only replica.")
|
|
136
|
+
continue;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
124
139
|
}
|
|
125
140
|
}
|
|
126
141
|
async delete(key) {
|
|
142
|
+
let client = null;
|
|
127
143
|
try {
|
|
128
|
-
|
|
144
|
+
client = await this.getConnectedClient();
|
|
129
145
|
await client.del(this.key(key));
|
|
130
146
|
}
|
|
131
147
|
catch (error) {
|
|
132
148
|
this.logger.error("[RedisCache] Delete error", {
|
|
133
149
|
error: error instanceof Error ? error.message : String(error),
|
|
134
150
|
});
|
|
135
|
-
this.
|
|
151
|
+
this.invalidate(client);
|
|
136
152
|
throw error;
|
|
137
153
|
}
|
|
138
154
|
}
|
|
139
155
|
async clear() {
|
|
156
|
+
let client = null;
|
|
140
157
|
try {
|
|
141
|
-
|
|
158
|
+
client = await this.getConnectedClient();
|
|
142
159
|
const pattern = `${this.prefix}*`;
|
|
143
160
|
// redis v5: scan cursor is string-based to prevent Number.MAX_SAFE_INTEGER overflow
|
|
144
161
|
let cursor = "0";
|
|
@@ -164,31 +181,33 @@ export class RedisTokenCacheStore {
|
|
|
164
181
|
this.logger.error("[RedisCache] Clear error", {
|
|
165
182
|
error: error instanceof Error ? error.message : String(error),
|
|
166
183
|
});
|
|
167
|
-
this.
|
|
184
|
+
this.invalidate(client);
|
|
168
185
|
throw error;
|
|
169
186
|
}
|
|
170
187
|
}
|
|
171
188
|
async has(key) {
|
|
189
|
+
let client = null;
|
|
172
190
|
try {
|
|
173
|
-
|
|
191
|
+
client = await this.getConnectedClient();
|
|
174
192
|
return (await client.exists(this.key(key))) === 1;
|
|
175
193
|
}
|
|
176
194
|
catch (error) {
|
|
177
195
|
this.logger.error("[RedisCache] Has error", {
|
|
178
196
|
error: error instanceof Error ? error.message : String(error),
|
|
179
197
|
});
|
|
180
|
-
this.
|
|
198
|
+
this.invalidate(client);
|
|
181
199
|
throw error;
|
|
182
200
|
}
|
|
183
201
|
}
|
|
184
202
|
async stats() {
|
|
185
203
|
let size = 0;
|
|
204
|
+
let client = null;
|
|
186
205
|
try {
|
|
187
|
-
|
|
206
|
+
client = await this.getConnectedClient();
|
|
188
207
|
size = await client.dbSize();
|
|
189
208
|
}
|
|
190
209
|
catch (error) {
|
|
191
|
-
this.
|
|
210
|
+
this.invalidate(client);
|
|
192
211
|
this.logger.error("[RedisCache] Stats error", {
|
|
193
212
|
error: error instanceof Error ? error.message : String(error),
|
|
194
213
|
});
|
|
@@ -196,32 +215,38 @@ export class RedisTokenCacheStore {
|
|
|
196
215
|
return { hits: this.hits, misses: this.misses, size, type: "extension" };
|
|
197
216
|
}
|
|
198
217
|
async close() {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
218
|
+
this.closed = true;
|
|
219
|
+
this.invalidate(this.client);
|
|
220
|
+
}
|
|
221
|
+
/** Detach before destruction so late socket errors cannot affect its successor. */
|
|
222
|
+
invalidate(client) {
|
|
223
|
+
if (!client || this.client !== client)
|
|
202
224
|
return;
|
|
203
|
-
|
|
225
|
+
this.client = null;
|
|
226
|
+
this.connected = false;
|
|
204
227
|
try {
|
|
205
|
-
|
|
228
|
+
client.destroy();
|
|
206
229
|
}
|
|
207
|
-
catch
|
|
208
|
-
//
|
|
230
|
+
catch {
|
|
231
|
+
// Already-closed clients need no further cleanup.
|
|
209
232
|
}
|
|
210
|
-
finally {
|
|
211
|
-
this.client = null;
|
|
212
|
-
this.connected = false;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
async getConnectedClient() {
|
|
216
|
-
await this.ensureConnected();
|
|
217
|
-
if (!this.client) {
|
|
218
|
-
throw new Error("Redis client not available after connect");
|
|
219
|
-
}
|
|
220
|
-
return this.client;
|
|
221
233
|
}
|
|
222
|
-
|
|
234
|
+
getConnectedClient() {
|
|
235
|
+
if (this.closed)
|
|
236
|
+
return Promise.reject(new Error("Redis token-cache store is closed"));
|
|
223
237
|
if (this.connected && this.client)
|
|
224
|
-
return;
|
|
238
|
+
return Promise.resolve(this.client);
|
|
239
|
+
if (this.connecting)
|
|
240
|
+
return this.connecting;
|
|
241
|
+
const connecting = this.connectClient();
|
|
242
|
+
this.connecting = connecting;
|
|
243
|
+
void connecting.finally(() => {
|
|
244
|
+
if (this.connecting === connecting)
|
|
245
|
+
this.connecting = null;
|
|
246
|
+
}).catch(() => { });
|
|
247
|
+
return connecting;
|
|
248
|
+
}
|
|
249
|
+
async connectClient() {
|
|
225
250
|
// deno-lint-ignore no-explicit-any
|
|
226
251
|
const clientOpts = {
|
|
227
252
|
url: this.url,
|
|
@@ -241,9 +266,13 @@ export class RedisTokenCacheStore {
|
|
|
241
266
|
if (this.username)
|
|
242
267
|
clientOpts.username = this.username;
|
|
243
268
|
const client = this.clientFactory(clientOpts);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
269
|
+
client.on("error", (err) => {
|
|
270
|
+
if (this.client !== client)
|
|
271
|
+
return;
|
|
272
|
+
// Let connect() own its bounded startup reconnect policy. Once ready,
|
|
273
|
+
// discard a failed socket so the next operation resolves the writer anew.
|
|
274
|
+
if (this.connected)
|
|
275
|
+
this.invalidate(client);
|
|
247
276
|
const error = getErrorMessage(err);
|
|
248
277
|
if (isExpectedDisconnect(err)) {
|
|
249
278
|
this.logger.info("[RedisCache] Client disconnected", { error });
|
|
@@ -252,8 +281,19 @@ export class RedisTokenCacheStore {
|
|
|
252
281
|
this.logger.error("[RedisCache] Client error", { error });
|
|
253
282
|
});
|
|
254
283
|
this.client = client;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
284
|
+
try {
|
|
285
|
+
await client.connect();
|
|
286
|
+
if (this.closed)
|
|
287
|
+
throw new Error("Redis token-cache store is closed");
|
|
288
|
+
if (this.client !== client)
|
|
289
|
+
throw new Error("Redis client disconnected during connect");
|
|
290
|
+
this.connected = true;
|
|
291
|
+
this.logger.info("[RedisCache] Connected");
|
|
292
|
+
return client;
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
this.invalidate(client);
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
258
298
|
}
|
|
259
299
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veryfront/ext-cache-redis",
|
|
3
|
-
"version": "0.1.1288-rc.
|
|
3
|
+
"version": "0.1.1288-rc.25610",
|
|
4
4
|
"description": "Veryfront first-party extension package for ext-cache-redis",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"veryfront",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"redis": "5.11.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"veryfront": "^0.1.1288-rc.
|
|
66
|
+
"veryfront": "^0.1.1288-rc.25610"
|
|
67
67
|
},
|
|
68
68
|
"type": "module",
|
|
69
69
|
"types": "./esm/extensions/ext-cache-redis/src/index.d.ts",
|
|
@@ -73,5 +73,5 @@
|
|
|
73
73
|
"NOTICE",
|
|
74
74
|
"README.md"
|
|
75
75
|
],
|
|
76
|
-
"gitHead": "
|
|
76
|
+
"gitHead": "046bd33ed82875bbc85596bf561ac131e7e9913b"
|
|
77
77
|
}
|