@zerotal/cache 1.4.0 → 1.5.0
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 +12 -0
- package/package.json +2 -2
- package/src/CacheManager.ts +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,18 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.5.0] — 2026-08-15
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Stampede protection survives a compute slower than 30 seconds.** `remember()`'s
|
|
16
|
+
recompute lock had a fixed 30s TTL and no way to extend it, so a factory that ran longer
|
|
17
|
+
silently dropped its lock and let a second node in — and an expensive query is the only
|
|
18
|
+
kind anyone bothers caching, so the protection failed precisely where it was needed. The
|
|
19
|
+
lock now refreshes for as long as the factory runs. A lock lost anyway falls back to
|
|
20
|
+
re-checking the cache and recomputing, which is what a timed-out waiter already did:
|
|
21
|
+
caching is best-effort here, not a correctness boundary.
|
|
22
|
+
|
|
11
23
|
## [1.0.3] — 2026-08-07
|
|
12
24
|
|
|
13
25
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/cache",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"typecheck": "tsc --noEmit"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@zerotal/core": "1.
|
|
33
|
+
"@zerotal/core": "1.5.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "^5.8.0"
|
package/src/CacheManager.ts
CHANGED
|
@@ -3,10 +3,16 @@ import { CacheSerializationError, CacheDeserializationError } from "./errors.ts"
|
|
|
3
3
|
import { FrameworkEvents, sha256Hex } from "@zerotal/core";
|
|
4
4
|
import { CacheQueried } from "./events.ts";
|
|
5
5
|
import type { LockManager } from "@zerotal/core/lock";
|
|
6
|
-
import { LockNotAcquiredError } from "@zerotal/core/lock";
|
|
6
|
+
import { LockNotAcquiredError, LockLostError } from "@zerotal/core/lock";
|
|
7
7
|
|
|
8
8
|
// Cross-process recompute lock for remember(): how long the lock is held while the
|
|
9
9
|
// factory runs, and how long a waiter blocks for the holder before recomputing anyway.
|
|
10
|
+
//
|
|
11
|
+
// The TTL is now a heartbeat interval rather than a deadline — the lock is
|
|
12
|
+
// refreshed for as long as the factory runs, so a compute slower than 30s no
|
|
13
|
+
// longer silently drops its lock and lets a second node in. That was the exact
|
|
14
|
+
// case where stampede protection stopped protecting: an expensive query, which
|
|
15
|
+
// is the only kind anyone bothers caching.
|
|
10
16
|
const STAMPEDE_LOCK_TTL = 30; // seconds
|
|
11
17
|
const STAMPEDE_LOCK_WAIT = 30; // seconds
|
|
12
18
|
|
|
@@ -137,9 +143,15 @@ export class CacheManager {
|
|
|
137
143
|
return await this._lock.block(lockKey, STAMPEDE_LOCK_TTL, recheckThenCompute, {
|
|
138
144
|
timeout: STAMPEDE_LOCK_WAIT,
|
|
139
145
|
retryDelay: 50,
|
|
146
|
+
refresh: true,
|
|
140
147
|
});
|
|
141
148
|
} catch (err) {
|
|
142
149
|
if (err instanceof LockNotAcquiredError) return recheckThenCompute();
|
|
150
|
+
// A lost lock is not a reason to fail the read. Caching is best-effort by
|
|
151
|
+
// design here — a waiter that times out already recomputes — so losing the
|
|
152
|
+
// lock mid-compute means at worst two nodes did the work, which is the
|
|
153
|
+
// thing we were optimising, not a correctness boundary.
|
|
154
|
+
if (err instanceof LockLostError) return recheckThenCompute();
|
|
143
155
|
throw err;
|
|
144
156
|
}
|
|
145
157
|
}
|