@tachybase/cache 1.6.9-alpha.2 → 1.6.10-alpha.1
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/lib/cache.d.ts +20 -0
- package/lib/cache.js +90 -1
- package/package.json +1 -1
package/lib/cache.d.ts
CHANGED
|
@@ -3,12 +3,22 @@ export declare class Cache {
|
|
|
3
3
|
name: string;
|
|
4
4
|
prefix?: string;
|
|
5
5
|
store: BasicCache;
|
|
6
|
+
/**
|
|
7
|
+
* Per prefixed key: serializes memory `store.set` / `store.get`+`store.set` so `set` and
|
|
8
|
+
* `setIfNotExists` cannot interleave for the same key (single-process).
|
|
9
|
+
*/
|
|
10
|
+
private memoryWriteQueues;
|
|
6
11
|
constructor({ name, prefix, store }: {
|
|
7
12
|
name: string;
|
|
8
13
|
store: BasicCache;
|
|
9
14
|
prefix?: string;
|
|
10
15
|
});
|
|
11
16
|
key(key: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* For memory store, waits on the same per-key queue as `setIfNotExists` so another writer
|
|
19
|
+
* cannot run `this.store.set` between that method's `get` and `set`.
|
|
20
|
+
*/
|
|
21
|
+
private runWithMemoryWriteLock;
|
|
12
22
|
set(key: string, value: unknown, ttl?: Milliseconds): Promise<void>;
|
|
13
23
|
get<T>(key: string): Promise<T>;
|
|
14
24
|
del(key: string): Promise<void>;
|
|
@@ -27,4 +37,14 @@ export declare class Cache {
|
|
|
27
37
|
setValueInObject(key: string, objectKey: string, value: unknown): Promise<void>;
|
|
28
38
|
getValueInObject(key: string, objectKey: string): Promise<any>;
|
|
29
39
|
delValueInObject(key: string, objectKey: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Atomically set `key` to `value` with expiry `ttl` (ms) only if `key` is absent.
|
|
42
|
+
* Returns `true` if the key was set, `false` if it already existed.
|
|
43
|
+
*
|
|
44
|
+
* - **Redis**: `SET` with `NX` and `PX` (safe across processes/instances).
|
|
45
|
+
* - **Memory**: `this.store.get` / `this.store.set` run under the same per-key `memoryWriteQueues`
|
|
46
|
+
* lock as `set`, so another `cache.set` cannot interleave between get and set (single-process;
|
|
47
|
+
* multi-instance locks still require Redis).
|
|
48
|
+
*/
|
|
49
|
+
setIfNotExists(key: string, value: unknown, ttl: number): Promise<boolean>;
|
|
30
50
|
}
|
package/lib/cache.js
CHANGED
|
@@ -21,8 +21,37 @@ __export(cache_exports, {
|
|
|
21
21
|
Cache: () => Cache
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(cache_exports);
|
|
24
|
+
var import_cache_manager_redis_yet = require("cache-manager-redis-yet");
|
|
25
|
+
function serializeRedisValue(value) {
|
|
26
|
+
try {
|
|
27
|
+
const serialized = JSON.stringify(value);
|
|
28
|
+
if (serialized === void 0) {
|
|
29
|
+
throw new import_cache_manager_redis_yet.NoCacheableError(`"${String(value)}" is not a cacheable value`);
|
|
30
|
+
}
|
|
31
|
+
return serialized;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
if (e instanceof import_cache_manager_redis_yet.NoCacheableError) {
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
37
|
+
throw new import_cache_manager_redis_yet.NoCacheableError(`"${String(value)}" is not a cacheable value: ${msg}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
__name(serializeRedisValue, "serializeRedisValue");
|
|
41
|
+
function isRedisLikeStore(store) {
|
|
42
|
+
var _a;
|
|
43
|
+
if (!store || typeof store !== "object") return false;
|
|
44
|
+
const s = store;
|
|
45
|
+
return typeof ((_a = s.client) == null ? void 0 : _a.set) === "function";
|
|
46
|
+
}
|
|
47
|
+
__name(isRedisLikeStore, "isRedisLikeStore");
|
|
24
48
|
const _Cache = class _Cache {
|
|
25
49
|
constructor({ name, prefix, store }) {
|
|
50
|
+
/**
|
|
51
|
+
* Per prefixed key: serializes memory `store.set` / `store.get`+`store.set` so `set` and
|
|
52
|
+
* `setIfNotExists` cannot interleave for the same key (single-process).
|
|
53
|
+
*/
|
|
54
|
+
this.memoryWriteQueues = /* @__PURE__ */ new Map();
|
|
26
55
|
this.name = name;
|
|
27
56
|
this.prefix = prefix;
|
|
28
57
|
this.store = store;
|
|
@@ -30,8 +59,38 @@ const _Cache = class _Cache {
|
|
|
30
59
|
key(key) {
|
|
31
60
|
return this.prefix ? `${this.prefix}:${key}` : key;
|
|
32
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* For memory store, waits on the same per-key queue as `setIfNotExists` so another writer
|
|
64
|
+
* cannot run `this.store.set` between that method's `get` and `set`.
|
|
65
|
+
*/
|
|
66
|
+
async runWithMemoryWriteLock(k, fn) {
|
|
67
|
+
const previous = this.memoryWriteQueues.get(k) ?? Promise.resolve();
|
|
68
|
+
let release;
|
|
69
|
+
const next = new Promise((res) => {
|
|
70
|
+
release = res;
|
|
71
|
+
});
|
|
72
|
+
const current = previous.then(() => next);
|
|
73
|
+
this.memoryWriteQueues.set(k, current);
|
|
74
|
+
await previous;
|
|
75
|
+
try {
|
|
76
|
+
return await fn();
|
|
77
|
+
} finally {
|
|
78
|
+
release();
|
|
79
|
+
if (this.memoryWriteQueues.get(k) === current) {
|
|
80
|
+
this.memoryWriteQueues.delete(k);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
33
84
|
async set(key, value, ttl) {
|
|
34
|
-
|
|
85
|
+
const k = this.key(key);
|
|
86
|
+
const raw = this.store.store;
|
|
87
|
+
if (isRedisLikeStore(raw)) {
|
|
88
|
+
await this.store.set(k, value, ttl);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
await this.runWithMemoryWriteLock(k, async () => {
|
|
92
|
+
await this.store.set(k, value, ttl);
|
|
93
|
+
});
|
|
35
94
|
}
|
|
36
95
|
async get(key) {
|
|
37
96
|
return await this.store.get(this.key(key));
|
|
@@ -97,6 +156,36 @@ const _Cache = class _Cache {
|
|
|
97
156
|
delete object[objectKey];
|
|
98
157
|
await this.set(key, object);
|
|
99
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Atomically set `key` to `value` with expiry `ttl` (ms) only if `key` is absent.
|
|
161
|
+
* Returns `true` if the key was set, `false` if it already existed.
|
|
162
|
+
*
|
|
163
|
+
* - **Redis**: `SET` with `NX` and `PX` (safe across processes/instances).
|
|
164
|
+
* - **Memory**: `this.store.get` / `this.store.set` run under the same per-key `memoryWriteQueues`
|
|
165
|
+
* lock as `set`, so another `cache.set` cannot interleave between get and set (single-process;
|
|
166
|
+
* multi-instance locks still require Redis).
|
|
167
|
+
*/
|
|
168
|
+
async setIfNotExists(key, value, ttl) {
|
|
169
|
+
if (value === void 0 || value === null) {
|
|
170
|
+
throw new import_cache_manager_redis_yet.NoCacheableError(`"${value}" is not a cacheable value`);
|
|
171
|
+
}
|
|
172
|
+
const k = this.key(key);
|
|
173
|
+
const raw = this.store.store;
|
|
174
|
+
if (isRedisLikeStore(raw)) {
|
|
175
|
+
const payload = serializeRedisValue(value);
|
|
176
|
+
const opts = ttl !== void 0 && ttl !== 0 ? { NX: true, PX: ttl } : { NX: true };
|
|
177
|
+
const reply = await raw.client.set(k, payload, opts);
|
|
178
|
+
return reply === "OK";
|
|
179
|
+
}
|
|
180
|
+
return this.runWithMemoryWriteLock(k, async () => {
|
|
181
|
+
const existing = await this.store.get(k);
|
|
182
|
+
if (existing !== void 0) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
await this.store.set(k, value, ttl);
|
|
186
|
+
return true;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
100
189
|
};
|
|
101
190
|
__name(_Cache, "Cache");
|
|
102
191
|
let Cache = _Cache;
|