@nage-api/cache 1.0.0-beta.2
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/LICENSE +202 -0
- package/README.md +127 -0
- package/dist/cache.module.d.ts +34 -0
- package/dist/cache.module.js +100 -0
- package/dist/cache.service.d.ts +73 -0
- package/dist/cache.service.js +147 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +27 -0
- package/dist/memory.store.d.ts +44 -0
- package/dist/memory.store.js +128 -0
- package/dist/ports.d.ts +51 -0
- package/dist/ports.js +14 -0
- package/dist/redis.store.d.ts +48 -0
- package/dist/redis.store.js +101 -0
- package/dist/tokens.d.ts +14 -0
- package/dist/tokens.js +15 -0
- package/dist/two-tier.store.d.ts +41 -0
- package/dist/two-tier.store.js +102 -0
- package/package.json +58 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* L1 in front of L2 (PLAN.md §8: "two-tier Keyv (memory+redis), typed").
|
|
4
|
+
*
|
|
5
|
+
* The point is latency, not capacity: a shared Redis is one network round trip
|
|
6
|
+
* away, and a request that reads the same key five times should pay for it
|
|
7
|
+
* once. L1 is a small in-process cache with a **shorter** TTL than L2, because
|
|
8
|
+
* an in-process copy is the one nothing can invalidate remotely.
|
|
9
|
+
*
|
|
10
|
+
* That shorter TTL is the whole safety argument. Another instance deleting a
|
|
11
|
+
* key clears L2 immediately and every L1 within its own TTL — so staleness is
|
|
12
|
+
* bounded by a number you choose, rather than by when a process happens to
|
|
13
|
+
* restart. Deleting locally also clears the local copy at once, so the instance
|
|
14
|
+
* that made a change never reads its own stale value.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.TwoTierCacheStore = void 0;
|
|
18
|
+
const DEFAULT_L1_TTL_MS = 5_000;
|
|
19
|
+
class TwoTierCacheStore {
|
|
20
|
+
#l1;
|
|
21
|
+
#l2;
|
|
22
|
+
#l1TtlMs;
|
|
23
|
+
#clock;
|
|
24
|
+
#onRemoteError;
|
|
25
|
+
#promotions = 0;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.#l1 = options.l1;
|
|
28
|
+
this.#l2 = options.l2;
|
|
29
|
+
this.#l1TtlMs = options.l1TtlMs ?? DEFAULT_L1_TTL_MS;
|
|
30
|
+
this.#clock = options.clock ?? { now: () => Date.now() };
|
|
31
|
+
this.#onRemoteError = options.onRemoteError;
|
|
32
|
+
}
|
|
33
|
+
async get(key) {
|
|
34
|
+
const local = await this.#l1.get(key);
|
|
35
|
+
if (local !== undefined)
|
|
36
|
+
return local;
|
|
37
|
+
const remote = await this.#tryRemote('get', () => this.#l2.get(key));
|
|
38
|
+
if (remote === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
await this.#l1.set(key, this.#clampForL1(remote));
|
|
41
|
+
this.#promotions += 1;
|
|
42
|
+
return remote;
|
|
43
|
+
}
|
|
44
|
+
async set(key, entry) {
|
|
45
|
+
// L2 first: if the shared write fails, an L1 copy nobody else can see would
|
|
46
|
+
// be the worst of both worlds.
|
|
47
|
+
await this.#tryRemote('set', async () => {
|
|
48
|
+
await this.#l2.set(key, entry);
|
|
49
|
+
return undefined;
|
|
50
|
+
});
|
|
51
|
+
await this.#l1.set(key, this.#clampForL1(entry));
|
|
52
|
+
}
|
|
53
|
+
async delete(key) {
|
|
54
|
+
await this.#l1.delete(key);
|
|
55
|
+
await this.#tryRemote('delete', async () => {
|
|
56
|
+
await this.#l2.delete(key);
|
|
57
|
+
return undefined;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async deletePrefix(prefix) {
|
|
61
|
+
await this.#l1.deletePrefix(prefix);
|
|
62
|
+
return (await this.#tryRemote('deletePrefix', () => this.#l2.deletePrefix(prefix))) ?? 0;
|
|
63
|
+
}
|
|
64
|
+
async deleteTag(tag) {
|
|
65
|
+
await this.#l1.deleteTag(tag);
|
|
66
|
+
return (await this.#tryRemote('deleteTag', () => this.#l2.deleteTag(tag))) ?? 0;
|
|
67
|
+
}
|
|
68
|
+
async clear() {
|
|
69
|
+
await this.#l1.clear();
|
|
70
|
+
await this.#tryRemote('clear', async () => {
|
|
71
|
+
await this.#l2.clear();
|
|
72
|
+
return undefined;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** How many reads were served by L2 and copied into L1. */
|
|
76
|
+
get promotions() {
|
|
77
|
+
return this.#promotions;
|
|
78
|
+
}
|
|
79
|
+
#clampForL1(entry) {
|
|
80
|
+
const ceiling = this.#clock.now() + this.#l1TtlMs;
|
|
81
|
+
const expiresAt = entry.expiresAt === undefined ? ceiling : Math.min(entry.expiresAt, ceiling);
|
|
82
|
+
return { ...entry, expiresAt };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Run an L2 operation, degrading to `undefined` on failure.
|
|
86
|
+
*
|
|
87
|
+
* A cache is an optimisation. If Redis is down, the application should be
|
|
88
|
+
* slower, not broken — so a remote failure is reported and swallowed rather
|
|
89
|
+
* than propagated to a request that could have been served from the source.
|
|
90
|
+
*/
|
|
91
|
+
async #tryRemote(operation, run) {
|
|
92
|
+
try {
|
|
93
|
+
return await run();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
this.#onRemoteError?.(operation, error);
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.TwoTierCacheStore = TwoTierCacheStore;
|
|
102
|
+
//# sourceMappingURL=two-tier.store.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nage-api/cache",
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
|
+
"description": "Typed two-tier cache for @nage-api — memory L1, pluggable remote L2, namespaced invalidation",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"!dist/.tsbuildinfo",
|
|
20
|
+
"!dist/**/*.map",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@nage-api/contracts": "1.0.0-beta.2",
|
|
28
|
+
"@nage-api/core": "1.0.0-beta.2"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@nestjs/common": "^11.0.0",
|
|
32
|
+
"@nestjs/core": "^11.0.0",
|
|
33
|
+
"reflect-metadata": "^0.2.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@nestjs/common": "11.1.29",
|
|
37
|
+
"@nestjs/core": "11.1.29",
|
|
38
|
+
"@nestjs/testing": "11.1.29",
|
|
39
|
+
"@swc/core": "1.15.47",
|
|
40
|
+
"@types/node": "22.20.1",
|
|
41
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
42
|
+
"reflect-metadata": "0.2.2",
|
|
43
|
+
"rimraf": "6.1.3",
|
|
44
|
+
"rxjs": "7.8.2",
|
|
45
|
+
"typescript": "5.9.3",
|
|
46
|
+
"unplugin-swc": "1.5.11",
|
|
47
|
+
"vitest": "4.1.10"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=22.0.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc -b tsconfig.build.json",
|
|
54
|
+
"clean": "rimraf dist .turbo",
|
|
55
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
56
|
+
"test": "vitest run"
|
|
57
|
+
}
|
|
58
|
+
}
|