@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 ensureConnected;
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;gBAGxB,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;IA8BjD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAevD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAalC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAsCtB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAalC,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC;IAgBjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAiBd,kBAAkB;YAQlB,eAAe;CAsC9B"}
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
- const client = await this.getConnectedClient();
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.connected = false;
109
+ this.invalidate(client);
107
110
  this.misses++;
108
111
  throw error;
109
112
  }
110
113
  }
111
114
  async set(key, entry) {
112
- try {
113
- const client = await this.getConnectedClient();
114
- const ttlMs = entry.expiresAt - Date.now();
115
- const ttlSeconds = Math.max(1, Math.floor(ttlMs / 1000));
116
- await client.setEx(this.key(key), ttlSeconds, JSON.stringify(entry));
117
- }
118
- catch (error) {
119
- this.logger.error("[RedisCache] Set error", {
120
- error: error instanceof Error ? error.message : String(error),
121
- });
122
- this.connected = false;
123
- throw error;
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
- const client = await this.getConnectedClient();
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.connected = false;
151
+ this.invalidate(client);
136
152
  throw error;
137
153
  }
138
154
  }
139
155
  async clear() {
156
+ let client = null;
140
157
  try {
141
- const client = await this.getConnectedClient();
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.connected = false;
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
- const client = await this.getConnectedClient();
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.connected = false;
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
- const client = await this.getConnectedClient();
206
+ client = await this.getConnectedClient();
188
207
  size = await client.dbSize();
189
208
  }
190
209
  catch (error) {
191
- this.connected = false;
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
- const client = this.client;
200
- if (!client) {
201
- this.connected = false;
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
- await client.close();
228
+ client.destroy();
206
229
  }
207
- catch (_) {
208
- // expected: close errors are non-critical
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
- async ensureConnected() {
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
- // deno-lint-ignore no-explicit-any
245
- client.on?.("error", (err) => {
246
- this.connected = false;
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
- await client.connect();
256
- this.connected = true;
257
- this.logger.info("[RedisCache] Connected");
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.25587",
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.25587"
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": "37961872ad0b80ec56ec17c75e33c8c00b187f8f"
76
+ "gitHead": "046bd33ed82875bbc85596bf561ac131e7e9913b"
77
77
  }