@getstrata/core 2.2.23 → 2.2.24
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.24
|
|
4
|
+
|
|
5
|
+
Fix Redis cache recovery after transport failures and command deadlines: retire failed connections, abort active fills, and lazily reconnect for later operations without replaying uncertain commands; preserve permanent explicit closure and shared invalidation fencing.
|
|
6
|
+
|
|
3
7
|
## 2.2.23
|
|
4
8
|
|
|
5
9
|
Fix native SMTP hangs with bounded whole-send deadlines, cancellation, buffered multiline replies, complete writes and verified implicit TLS. Preserve known final DATA acceptance through cleanup failures. Correct DATA transparency, CRLF, UTF-8 MIME/header encoding and long-line framing; reject incomplete AUTH and unsupported SMTPUTF8 envelopes explicitly. Generate documented SMTP timeout and TLS defaults. Real socket/TLS/protocol regressions cover outages, cancellation, accepted sends and framing; SMTP delivery before final acknowledgement remains uncertain.
|
|
@@ -9,14 +9,16 @@ type RedisCacheOptions = {
|
|
|
9
9
|
metadataLimit?: number;
|
|
10
10
|
};
|
|
11
11
|
declare class RedisCacheStore implements CacheStore {
|
|
12
|
+
private readonly redisUrl;
|
|
12
13
|
private readonly ttlMs;
|
|
13
14
|
private readonly maxEntries;
|
|
14
|
-
private
|
|
15
|
+
private client;
|
|
15
16
|
private readonly keys;
|
|
16
17
|
private readonly options;
|
|
17
18
|
private readonly fills;
|
|
18
19
|
private closed;
|
|
19
20
|
constructor(redisUrl: string, ttlMs: number, maxEntries: number, options?: RedisCacheOptions);
|
|
21
|
+
private createClient;
|
|
20
22
|
close(): void;
|
|
21
23
|
get<T>(key: string): Promise<T | undefined>;
|
|
22
24
|
set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
|
|
@@ -289,6 +289,7 @@ return redis.error_reply('Unknown cache operation')
|
|
|
289
289
|
`;
|
|
290
290
|
|
|
291
291
|
class RedisCacheStore {
|
|
292
|
+
redisUrl;
|
|
292
293
|
ttlMs;
|
|
293
294
|
maxEntries;
|
|
294
295
|
client;
|
|
@@ -297,6 +298,7 @@ class RedisCacheStore {
|
|
|
297
298
|
fills = new Set;
|
|
298
299
|
closed = false;
|
|
299
300
|
constructor(redisUrl, ttlMs, maxEntries, options = {}) {
|
|
301
|
+
this.redisUrl = redisUrl;
|
|
300
302
|
this.ttlMs = ttlMs;
|
|
301
303
|
this.maxEntries = maxEntries;
|
|
302
304
|
if (!Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
@@ -321,7 +323,10 @@ class RedisCacheStore {
|
|
|
321
323
|
const namespace = namespacedRedisKey("cache:v3:");
|
|
322
324
|
const slot = new Bun.CryptoHasher("sha256").update(namespace).digest("hex");
|
|
323
325
|
this.keys = ["entries", "expiry", "order", "metadata", "metadata-expiry"].map((name) => `${namespace}{${slot}}:${name}`);
|
|
324
|
-
this.client =
|
|
326
|
+
this.client = this.createClient();
|
|
327
|
+
}
|
|
328
|
+
createClient() {
|
|
329
|
+
return new RedisClient(this.redisUrl, {
|
|
325
330
|
connectionTimeout: this.options.commandTimeoutMs,
|
|
326
331
|
maxRetries: 1
|
|
327
332
|
});
|
|
@@ -330,7 +335,8 @@ class RedisCacheStore {
|
|
|
330
335
|
this.closed = true;
|
|
331
336
|
for (const fill of this.fills)
|
|
332
337
|
fill.abort(new Error("Redis cache store is closed."));
|
|
333
|
-
this.client
|
|
338
|
+
this.client?.close();
|
|
339
|
+
this.client = undefined;
|
|
334
340
|
}
|
|
335
341
|
async get(key) {
|
|
336
342
|
const result = await this.execute("get", key);
|
|
@@ -409,6 +415,7 @@ class RedisCacheStore {
|
|
|
409
415
|
Promise.resolve().then(() => loader(controller.signal)),
|
|
410
416
|
aborted
|
|
411
417
|
]);
|
|
418
|
+
controller.signal.throwIfAborted();
|
|
412
419
|
done = true;
|
|
413
420
|
clearInterval(renew);
|
|
414
421
|
await this.execute("publish", key, {
|
|
@@ -450,16 +457,28 @@ class RedisCacheStore {
|
|
|
450
457
|
const ttlMs = extra.ttlMs ?? this.ttlMs;
|
|
451
458
|
if (typeof ttlMs !== "number" || !Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
452
459
|
throw new RangeError("ttlMs must be a non-negative safe integer.");
|
|
460
|
+
if (!this.client)
|
|
461
|
+
this.client = this.createClient();
|
|
462
|
+
const client = this.client;
|
|
463
|
+
const retire = (reason) => {
|
|
464
|
+
if (this.client !== client)
|
|
465
|
+
return;
|
|
466
|
+
this.client = undefined;
|
|
467
|
+
for (const fill of this.fills)
|
|
468
|
+
fill.abort(reason);
|
|
469
|
+
client.close();
|
|
470
|
+
};
|
|
453
471
|
let timer;
|
|
454
472
|
const timeout = new Promise((_, reject) => {
|
|
455
473
|
timer = setTimeout(() => {
|
|
456
|
-
|
|
457
|
-
|
|
474
|
+
const error = new Error("Redis cache command deadline exceeded.");
|
|
475
|
+
reject(error);
|
|
476
|
+
retire(error);
|
|
458
477
|
}, this.options.commandTimeoutMs);
|
|
459
478
|
});
|
|
460
479
|
try {
|
|
461
480
|
const raw = await Promise.race([
|
|
462
|
-
|
|
481
|
+
client.send("EVAL", [
|
|
463
482
|
CACHE_SCRIPT,
|
|
464
483
|
String(this.keys.length),
|
|
465
484
|
...this.keys,
|
|
@@ -477,7 +496,13 @@ class RedisCacheStore {
|
|
|
477
496
|
]),
|
|
478
497
|
timeout
|
|
479
498
|
]);
|
|
499
|
+
if (this.closed || this.client !== client)
|
|
500
|
+
throw new Error("Redis cache connection was retired.");
|
|
480
501
|
return JSON.parse(String(raw));
|
|
502
|
+
} catch (error) {
|
|
503
|
+
if (error && typeof error === "object" && "code" in error && typeof error.code === "string" && error.code.startsWith("ERR_REDIS_CONNECTION"))
|
|
504
|
+
retire(error);
|
|
505
|
+
throw error;
|
|
481
506
|
} finally {
|
|
482
507
|
clearTimeout(timer);
|
|
483
508
|
}
|
package/dist/index.js
CHANGED
|
@@ -3893,6 +3893,7 @@ return redis.error_reply('Unknown cache operation')
|
|
|
3893
3893
|
`;
|
|
3894
3894
|
|
|
3895
3895
|
class RedisCacheStore {
|
|
3896
|
+
redisUrl;
|
|
3896
3897
|
ttlMs;
|
|
3897
3898
|
maxEntries;
|
|
3898
3899
|
client;
|
|
@@ -3901,6 +3902,7 @@ class RedisCacheStore {
|
|
|
3901
3902
|
fills = new Set;
|
|
3902
3903
|
closed = false;
|
|
3903
3904
|
constructor(redisUrl, ttlMs, maxEntries, options = {}) {
|
|
3905
|
+
this.redisUrl = redisUrl;
|
|
3904
3906
|
this.ttlMs = ttlMs;
|
|
3905
3907
|
this.maxEntries = maxEntries;
|
|
3906
3908
|
if (!Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
@@ -3925,7 +3927,10 @@ class RedisCacheStore {
|
|
|
3925
3927
|
const namespace = namespacedRedisKey("cache:v3:");
|
|
3926
3928
|
const slot = new Bun.CryptoHasher("sha256").update(namespace).digest("hex");
|
|
3927
3929
|
this.keys = ["entries", "expiry", "order", "metadata", "metadata-expiry"].map((name) => `${namespace}{${slot}}:${name}`);
|
|
3928
|
-
this.client =
|
|
3930
|
+
this.client = this.createClient();
|
|
3931
|
+
}
|
|
3932
|
+
createClient() {
|
|
3933
|
+
return new RedisClient(this.redisUrl, {
|
|
3929
3934
|
connectionTimeout: this.options.commandTimeoutMs,
|
|
3930
3935
|
maxRetries: 1
|
|
3931
3936
|
});
|
|
@@ -3934,7 +3939,8 @@ class RedisCacheStore {
|
|
|
3934
3939
|
this.closed = true;
|
|
3935
3940
|
for (const fill of this.fills)
|
|
3936
3941
|
fill.abort(new Error("Redis cache store is closed."));
|
|
3937
|
-
this.client
|
|
3942
|
+
this.client?.close();
|
|
3943
|
+
this.client = undefined;
|
|
3938
3944
|
}
|
|
3939
3945
|
async get(key) {
|
|
3940
3946
|
const result = await this.execute("get", key);
|
|
@@ -4013,6 +4019,7 @@ class RedisCacheStore {
|
|
|
4013
4019
|
Promise.resolve().then(() => loader(controller.signal)),
|
|
4014
4020
|
aborted
|
|
4015
4021
|
]);
|
|
4022
|
+
controller.signal.throwIfAborted();
|
|
4016
4023
|
done = true;
|
|
4017
4024
|
clearInterval(renew);
|
|
4018
4025
|
await this.execute("publish", key, {
|
|
@@ -4054,16 +4061,28 @@ class RedisCacheStore {
|
|
|
4054
4061
|
const ttlMs = extra.ttlMs ?? this.ttlMs;
|
|
4055
4062
|
if (typeof ttlMs !== "number" || !Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
4056
4063
|
throw new RangeError("ttlMs must be a non-negative safe integer.");
|
|
4064
|
+
if (!this.client)
|
|
4065
|
+
this.client = this.createClient();
|
|
4066
|
+
const client = this.client;
|
|
4067
|
+
const retire = (reason) => {
|
|
4068
|
+
if (this.client !== client)
|
|
4069
|
+
return;
|
|
4070
|
+
this.client = undefined;
|
|
4071
|
+
for (const fill of this.fills)
|
|
4072
|
+
fill.abort(reason);
|
|
4073
|
+
client.close();
|
|
4074
|
+
};
|
|
4057
4075
|
let timer;
|
|
4058
4076
|
const timeout = new Promise((_, reject) => {
|
|
4059
4077
|
timer = setTimeout(() => {
|
|
4060
|
-
|
|
4061
|
-
|
|
4078
|
+
const error = new Error("Redis cache command deadline exceeded.");
|
|
4079
|
+
reject(error);
|
|
4080
|
+
retire(error);
|
|
4062
4081
|
}, this.options.commandTimeoutMs);
|
|
4063
4082
|
});
|
|
4064
4083
|
try {
|
|
4065
4084
|
const raw = await Promise.race([
|
|
4066
|
-
|
|
4085
|
+
client.send("EVAL", [
|
|
4067
4086
|
CACHE_SCRIPT,
|
|
4068
4087
|
String(this.keys.length),
|
|
4069
4088
|
...this.keys,
|
|
@@ -4081,7 +4100,13 @@ class RedisCacheStore {
|
|
|
4081
4100
|
]),
|
|
4082
4101
|
timeout
|
|
4083
4102
|
]);
|
|
4103
|
+
if (this.closed || this.client !== client)
|
|
4104
|
+
throw new Error("Redis cache connection was retired.");
|
|
4084
4105
|
return JSON.parse(String(raw));
|
|
4106
|
+
} catch (error) {
|
|
4107
|
+
if (error && typeof error === "object" && "code" in error && typeof error.code === "string" && error.code.startsWith("ERR_REDIS_CONNECTION"))
|
|
4108
|
+
retire(error);
|
|
4109
|
+
throw error;
|
|
4085
4110
|
} finally {
|
|
4086
4111
|
clearTimeout(timer);
|
|
4087
4112
|
}
|