@visualbravo/zenstack-cache 0.0.0 â 1.0.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/README.md +8 -8
- package/dist/providers/memory.cjs +9 -5
- package/dist/providers/memory.mjs +9 -5
- package/dist/providers/redis.cjs +12 -12
- package/dist/providers/redis.mjs +12 -12
- package/dist/utils.cjs +4 -2
- package/dist/utils.mjs +4 -2
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1>
|
|
3
3
|
ZenStack Cache
|
|
4
|
-
<small>(beta)</small>
|
|
5
4
|
</h1>
|
|
6
5
|
|
|
7
6
|
Reduce response times and database load with query-level caching integrated with the ZenStack ORM.
|
|
@@ -11,13 +10,13 @@
|
|
|
11
10
|
<a href="https://www.npmjs.com/package/@visualbravo/zenstack-cache?activeTab=versions">
|
|
12
11
|
<img alt="NPM Version" src="https://img.shields.io/npm/v/%40visualbravo%2Fzenstack-cache/latest">
|
|
13
12
|
</a>
|
|
14
|
-
<a>
|
|
15
|
-
<img alt="
|
|
13
|
+
<a href="https://github.com/visualbravo/zenstack-cache/actions/workflows/build-and-test.yaml?query=branch%3Adev++">
|
|
14
|
+
<img alt="Build Status" src="https://img.shields.io/github/actions/workflow/status/visualbravo/zenstack-cache/build-and-test.yaml">
|
|
16
15
|
</a>
|
|
17
16
|
<a href="https://discord.gg/Ykhr738dUe">
|
|
18
17
|
<img alt="Join the ZenStack server" src="https://img.shields.io/discord/1035538056146595961">
|
|
19
18
|
</a>
|
|
20
|
-
<a href="https://github.com/visualbravo/zenstack-cache/blob/
|
|
19
|
+
<a href="https://github.com/visualbravo/zenstack-cache/blob/76a2de03245c26841b04525dd8b424a8799d654c/LICENSE">
|
|
21
20
|
<img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-green">
|
|
22
21
|
</a>
|
|
23
22
|
|
|
@@ -27,13 +26,14 @@
|
|
|
27
26
|
</div>
|
|
28
27
|
|
|
29
28
|
## Features
|
|
30
|
-
* đ **Redis
|
|
31
|
-
* đĨī¸ **Memory
|
|
32
|
-
* đ **Type-
|
|
29
|
+
* đ **Redis Cache:** A central cache to scale across different systems.
|
|
30
|
+
* đĨī¸ **Memory Cache:** A simple cache when scale is not a concern.
|
|
31
|
+
* đ **Type-safety:** The caching options appear in the intellisense for all read queries.
|
|
32
|
+
* đˇī¸ **Tag-based Invalidation:** Easily invalidate multiple related cache entries.
|
|
33
33
|
|
|
34
34
|
## Requirements
|
|
35
35
|
|
|
36
|
-
* ZenStack (version >= `
|
|
36
|
+
* ZenStack (version >= `3.3.0`)
|
|
37
37
|
* Node.js (version >= `20.0.0`)
|
|
38
38
|
* Redis (version >= `7.0.0`)
|
|
39
39
|
* âšī¸ Only if you intend to use the `RedisCacheProvider`
|
|
@@ -16,10 +16,12 @@ var MemoryCacheProvider = class {
|
|
|
16
16
|
for (const [key, entry] of this.entryStore) if (require_utils.entryIsExpired(entry)) {
|
|
17
17
|
this.entryStore.delete(key);
|
|
18
18
|
this.options?.onIntervalExpiration?.(entry);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
if (entry.options.tags) for (const tag of entry.options.tags) {
|
|
20
|
+
const keys = this.tagStore.get(tag);
|
|
21
|
+
if (!keys) continue;
|
|
22
|
+
keys.delete(key);
|
|
23
|
+
if (keys.size === 0) this.tagStore.delete(tag);
|
|
24
|
+
}
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
get(key) {
|
|
@@ -40,7 +42,9 @@ var MemoryCacheProvider = class {
|
|
|
40
42
|
invalidate(options) {
|
|
41
43
|
if (options.tags) for (const tag of options.tags) {
|
|
42
44
|
const keys = this.tagStore.get(tag);
|
|
43
|
-
if (keys)
|
|
45
|
+
if (!keys) continue;
|
|
46
|
+
for (const key of keys) this.entryStore.delete(key);
|
|
47
|
+
this.tagStore.delete(tag);
|
|
44
48
|
}
|
|
45
49
|
return Promise.resolve();
|
|
46
50
|
}
|
|
@@ -16,10 +16,12 @@ var MemoryCacheProvider = class {
|
|
|
16
16
|
for (const [key, entry] of this.entryStore) if (entryIsExpired(entry)) {
|
|
17
17
|
this.entryStore.delete(key);
|
|
18
18
|
this.options?.onIntervalExpiration?.(entry);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
if (entry.options.tags) for (const tag of entry.options.tags) {
|
|
20
|
+
const keys = this.tagStore.get(tag);
|
|
21
|
+
if (!keys) continue;
|
|
22
|
+
keys.delete(key);
|
|
23
|
+
if (keys.size === 0) this.tagStore.delete(tag);
|
|
24
|
+
}
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
get(key) {
|
|
@@ -40,7 +42,9 @@ var MemoryCacheProvider = class {
|
|
|
40
42
|
invalidate(options) {
|
|
41
43
|
if (options.tags) for (const tag of options.tags) {
|
|
42
44
|
const keys = this.tagStore.get(tag);
|
|
43
|
-
if (keys)
|
|
45
|
+
if (!keys) continue;
|
|
46
|
+
for (const key of keys) this.entryStore.delete(key);
|
|
47
|
+
this.tagStore.delete(tag);
|
|
44
48
|
}
|
|
45
49
|
return Promise.resolve();
|
|
46
50
|
}
|
package/dist/providers/redis.cjs
CHANGED
|
@@ -11,30 +11,30 @@ var RedisCacheProvider = class {
|
|
|
11
11
|
this.redis = new ioredis.Redis(options.url);
|
|
12
12
|
}
|
|
13
13
|
async get(key) {
|
|
14
|
-
const entryJson = await this.redis.get(
|
|
14
|
+
const entryJson = await this.redis.get(makeQueryKey(key));
|
|
15
15
|
if (!entryJson) return;
|
|
16
16
|
return superjson.default.parse(entryJson);
|
|
17
17
|
}
|
|
18
18
|
async set(key, entry) {
|
|
19
19
|
const multi = this.redis.multi();
|
|
20
|
-
const
|
|
21
|
-
multi.set(
|
|
20
|
+
const queryKey = makeQueryKey(key);
|
|
21
|
+
multi.set(queryKey, superjson.default.stringify(entry));
|
|
22
22
|
const totalTtl = require_utils.getTotalTtl(entry);
|
|
23
|
-
if (totalTtl > 0) multi.expire(
|
|
23
|
+
if (totalTtl > 0) multi.expire(queryKey, totalTtl);
|
|
24
24
|
if (entry.options.tags) for (const tag of entry.options.tags) {
|
|
25
|
-
const
|
|
26
|
-
multi.sadd(
|
|
25
|
+
const tagKey = makeTagKey(tag);
|
|
26
|
+
multi.sadd(tagKey, queryKey);
|
|
27
27
|
if (totalTtl > 0) {
|
|
28
|
-
multi.expire(
|
|
29
|
-
multi.expire(
|
|
30
|
-
}
|
|
28
|
+
multi.expire(tagKey, totalTtl, "GT");
|
|
29
|
+
multi.expire(tagKey, totalTtl, "NX");
|
|
30
|
+
} else multi.persist(tagKey);
|
|
31
31
|
}
|
|
32
32
|
await multi.exec();
|
|
33
33
|
}
|
|
34
34
|
async invalidate(options) {
|
|
35
35
|
if (options.tags && options.tags.length > 0) await Promise.all(options.tags.map((tag) => {
|
|
36
36
|
return new Promise((resolve, reject) => {
|
|
37
|
-
const stream = this.redis.sscanStream(
|
|
37
|
+
const stream = this.redis.sscanStream(makeTagKey(tag), { count: 100 });
|
|
38
38
|
stream.on("data", async (keys) => {
|
|
39
39
|
if (keys.length > 1) await this.redis.del(...keys);
|
|
40
40
|
});
|
|
@@ -57,10 +57,10 @@ var RedisCacheProvider = class {
|
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
};
|
|
60
|
-
function
|
|
60
|
+
function makeQueryKey(key) {
|
|
61
61
|
return `zenstack:cache:query:${key}`;
|
|
62
62
|
}
|
|
63
|
-
function
|
|
63
|
+
function makeTagKey(key) {
|
|
64
64
|
return `zenstack:cache:tag:${key}`;
|
|
65
65
|
}
|
|
66
66
|
|
package/dist/providers/redis.mjs
CHANGED
|
@@ -9,30 +9,30 @@ var RedisCacheProvider = class {
|
|
|
9
9
|
this.redis = new Redis(options.url);
|
|
10
10
|
}
|
|
11
11
|
async get(key) {
|
|
12
|
-
const entryJson = await this.redis.get(
|
|
12
|
+
const entryJson = await this.redis.get(makeQueryKey(key));
|
|
13
13
|
if (!entryJson) return;
|
|
14
14
|
return superjson.parse(entryJson);
|
|
15
15
|
}
|
|
16
16
|
async set(key, entry) {
|
|
17
17
|
const multi = this.redis.multi();
|
|
18
|
-
const
|
|
19
|
-
multi.set(
|
|
18
|
+
const queryKey = makeQueryKey(key);
|
|
19
|
+
multi.set(queryKey, superjson.stringify(entry));
|
|
20
20
|
const totalTtl = getTotalTtl(entry);
|
|
21
|
-
if (totalTtl > 0) multi.expire(
|
|
21
|
+
if (totalTtl > 0) multi.expire(queryKey, totalTtl);
|
|
22
22
|
if (entry.options.tags) for (const tag of entry.options.tags) {
|
|
23
|
-
const
|
|
24
|
-
multi.sadd(
|
|
23
|
+
const tagKey = makeTagKey(tag);
|
|
24
|
+
multi.sadd(tagKey, queryKey);
|
|
25
25
|
if (totalTtl > 0) {
|
|
26
|
-
multi.expire(
|
|
27
|
-
multi.expire(
|
|
28
|
-
}
|
|
26
|
+
multi.expire(tagKey, totalTtl, "GT");
|
|
27
|
+
multi.expire(tagKey, totalTtl, "NX");
|
|
28
|
+
} else multi.persist(tagKey);
|
|
29
29
|
}
|
|
30
30
|
await multi.exec();
|
|
31
31
|
}
|
|
32
32
|
async invalidate(options) {
|
|
33
33
|
if (options.tags && options.tags.length > 0) await Promise.all(options.tags.map((tag) => {
|
|
34
34
|
return new Promise((resolve, reject) => {
|
|
35
|
-
const stream = this.redis.sscanStream(
|
|
35
|
+
const stream = this.redis.sscanStream(makeTagKey(tag), { count: 100 });
|
|
36
36
|
stream.on("data", async (keys) => {
|
|
37
37
|
if (keys.length > 1) await this.redis.del(...keys);
|
|
38
38
|
});
|
|
@@ -55,10 +55,10 @@ var RedisCacheProvider = class {
|
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
|
-
function
|
|
58
|
+
function makeQueryKey(key) {
|
|
59
59
|
return `zenstack:cache:query:${key}`;
|
|
60
60
|
}
|
|
61
|
-
function
|
|
61
|
+
function makeTagKey(key) {
|
|
62
62
|
return `zenstack:cache:tag:${key}`;
|
|
63
63
|
}
|
|
64
64
|
|
package/dist/utils.cjs
CHANGED
|
@@ -4,13 +4,15 @@ function getTotalTtl(entry) {
|
|
|
4
4
|
return (entry.options.ttl ?? 0) + (entry.options.swr ?? 0);
|
|
5
5
|
}
|
|
6
6
|
function entryIsFresh(entry) {
|
|
7
|
-
return entry.options.ttl ? Date.now() <= entry.createdAt + (entry.options.ttl ?? 0) * 1e3 :
|
|
7
|
+
return entry.options.ttl ? Date.now() <= entry.createdAt + (entry.options.ttl ?? 0) * 1e3 : !entry.options.swr;
|
|
8
8
|
}
|
|
9
9
|
function entryIsStale(entry) {
|
|
10
10
|
return entry.options.swr ? Date.now() <= entry.createdAt + getTotalTtl(entry) * 1e3 : false;
|
|
11
11
|
}
|
|
12
12
|
function entryIsExpired(entry) {
|
|
13
|
-
|
|
13
|
+
const totalTtl = getTotalTtl(entry);
|
|
14
|
+
if (totalTtl === 0) return false;
|
|
15
|
+
return Date.now() > entry.createdAt + totalTtl * 1e3;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
//#endregion
|
package/dist/utils.mjs
CHANGED
|
@@ -3,13 +3,15 @@ function getTotalTtl(entry) {
|
|
|
3
3
|
return (entry.options.ttl ?? 0) + (entry.options.swr ?? 0);
|
|
4
4
|
}
|
|
5
5
|
function entryIsFresh(entry) {
|
|
6
|
-
return entry.options.ttl ? Date.now() <= entry.createdAt + (entry.options.ttl ?? 0) * 1e3 :
|
|
6
|
+
return entry.options.ttl ? Date.now() <= entry.createdAt + (entry.options.ttl ?? 0) * 1e3 : !entry.options.swr;
|
|
7
7
|
}
|
|
8
8
|
function entryIsStale(entry) {
|
|
9
9
|
return entry.options.swr ? Date.now() <= entry.createdAt + getTotalTtl(entry) * 1e3 : false;
|
|
10
10
|
}
|
|
11
11
|
function entryIsExpired(entry) {
|
|
12
|
-
|
|
12
|
+
const totalTtl = getTotalTtl(entry);
|
|
13
|
+
if (totalTtl === 0) return false;
|
|
14
|
+
return Date.now() > entry.createdAt + totalTtl * 1e3;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@visualbravo/zenstack-cache",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": "github:visualbravo/zenstack-cache",
|
|
4
6
|
"type": "module",
|
|
5
7
|
"main": "./dist/index.cjs",
|
|
6
8
|
"module": "./dist/index.mjs",
|
|
@@ -103,13 +105,13 @@
|
|
|
103
105
|
"zod": "^4.1.0"
|
|
104
106
|
},
|
|
105
107
|
"devDependencies": {
|
|
108
|
+
"@types/better-sqlite3": "7.6.13",
|
|
106
109
|
"@zenstack-cache/config": "0.0.0",
|
|
107
|
-
"kysely": "~0.28.8",
|
|
108
110
|
"better-sqlite3": "12.6.2",
|
|
109
|
-
"
|
|
111
|
+
"kysely": "~0.28.8"
|
|
110
112
|
},
|
|
111
113
|
"peerDependencies": {
|
|
112
114
|
"@zenstackhq/orm": "canary"
|
|
113
115
|
},
|
|
114
|
-
"packageManager": "bun@1.3.
|
|
116
|
+
"packageManager": "bun@1.3.8"
|
|
115
117
|
}
|