@visualbravo/zenstack-cache 0.0.0 â 1.0.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/README.md +13 -10
- 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/schemas.cjs +2 -2
- package/dist/schemas.d.cts +4 -4
- package/dist/schemas.d.mts +4 -4
- package/dist/schemas.mjs +2 -2
- package/dist/utils.cjs +4 -2
- package/dist/utils.mjs +4 -2
- package/package.json +14 -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,16 @@
|
|
|
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://www.npmjs.com/package/@visualbravo/zenstack-cache">
|
|
14
|
+
<img alt="NPM Downloads" src="https://img.shields.io/npm/dm/%40visualbravo%2Fzenstack-cache">
|
|
16
15
|
</a>
|
|
17
|
-
<a href="https://
|
|
18
|
-
<img alt="
|
|
16
|
+
<a href="https://github.com/visualbravo/zenstack-cache/actions/workflows/build-and-test.yaml?query=branch%3Adev++">
|
|
17
|
+
<img alt="Build Status" src="https://img.shields.io/github/actions/workflow/status/visualbravo/zenstack-cache/build-and-test.yaml">
|
|
19
18
|
</a>
|
|
20
|
-
<a href="https://
|
|
19
|
+
<a href="https://discord.gg/2PaRSu7X">
|
|
20
|
+
<img alt="Join the ZenStack Cache channel" src="https://img.shields.io/discord/1035538056146595961">
|
|
21
|
+
</a>
|
|
22
|
+
<a href="https://github.com/visualbravo/zenstack-cache/blob/76a2de03245c26841b04525dd8b424a8799d654c/LICENSE">
|
|
21
23
|
<img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-green">
|
|
22
24
|
</a>
|
|
23
25
|
|
|
@@ -27,13 +29,14 @@
|
|
|
27
29
|
</div>
|
|
28
30
|
|
|
29
31
|
## Features
|
|
30
|
-
* đ **Redis
|
|
31
|
-
* đĨī¸ **Memory
|
|
32
|
-
* đ **Type-
|
|
32
|
+
* đ **Redis Cache:** A central cache to scale across different systems.
|
|
33
|
+
* đĨī¸ **Memory Cache:** A simple cache when scale is not a concern.
|
|
34
|
+
* đ **Type-safety:** The caching options appear in the intellisense for all read queries.
|
|
35
|
+
* đˇī¸ **Tag-based Invalidation:** Easily invalidate multiple related cache entries.
|
|
33
36
|
|
|
34
37
|
## Requirements
|
|
35
38
|
|
|
36
|
-
* ZenStack (version >= `
|
|
39
|
+
* ZenStack (version >= `3.3.0`)
|
|
37
40
|
* Node.js (version >= `20.0.0`)
|
|
38
41
|
* Redis (version >= `7.0.0`)
|
|
39
42
|
* âšī¸ 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/schemas.cjs
CHANGED
|
@@ -4,8 +4,8 @@ zod = require_rolldown_runtime.__toESM(zod);
|
|
|
4
4
|
|
|
5
5
|
//#region src/schemas.ts
|
|
6
6
|
const cacheOptionsSchema = zod.default.strictObject({
|
|
7
|
-
ttl: zod.default.
|
|
8
|
-
swr: zod.default.
|
|
7
|
+
ttl: zod.default.int().positive().optional(),
|
|
8
|
+
swr: zod.default.int().positive().optional(),
|
|
9
9
|
tags: zod.default.string().array().optional()
|
|
10
10
|
});
|
|
11
11
|
const cacheEnvelopeSchema = zod.default.object({ cache: cacheOptionsSchema.optional() });
|
package/dist/schemas.d.cts
CHANGED
|
@@ -2,14 +2,14 @@ import z from "zod";
|
|
|
2
2
|
|
|
3
3
|
//#region src/schemas.d.ts
|
|
4
4
|
declare const cacheOptionsSchema: z.ZodObject<{
|
|
5
|
-
ttl: z.ZodOptional<z.
|
|
6
|
-
swr: z.ZodOptional<z.
|
|
5
|
+
ttl: z.ZodOptional<z.ZodInt>;
|
|
6
|
+
swr: z.ZodOptional<z.ZodInt>;
|
|
7
7
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
8
8
|
}, z.core.$strict>;
|
|
9
9
|
declare const cacheEnvelopeSchema: z.ZodObject<{
|
|
10
10
|
cache: z.ZodOptional<z.ZodObject<{
|
|
11
|
-
ttl: z.ZodOptional<z.
|
|
12
|
-
swr: z.ZodOptional<z.
|
|
11
|
+
ttl: z.ZodOptional<z.ZodInt>;
|
|
12
|
+
swr: z.ZodOptional<z.ZodInt>;
|
|
13
13
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
14
14
|
}, z.core.$strict>>;
|
|
15
15
|
}, z.core.$strip>;
|
package/dist/schemas.d.mts
CHANGED
|
@@ -2,14 +2,14 @@ import z from "zod";
|
|
|
2
2
|
|
|
3
3
|
//#region src/schemas.d.ts
|
|
4
4
|
declare const cacheOptionsSchema: z.ZodObject<{
|
|
5
|
-
ttl: z.ZodOptional<z.
|
|
6
|
-
swr: z.ZodOptional<z.
|
|
5
|
+
ttl: z.ZodOptional<z.ZodInt>;
|
|
6
|
+
swr: z.ZodOptional<z.ZodInt>;
|
|
7
7
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
8
8
|
}, z.core.$strict>;
|
|
9
9
|
declare const cacheEnvelopeSchema: z.ZodObject<{
|
|
10
10
|
cache: z.ZodOptional<z.ZodObject<{
|
|
11
|
-
ttl: z.ZodOptional<z.
|
|
12
|
-
swr: z.ZodOptional<z.
|
|
11
|
+
ttl: z.ZodOptional<z.ZodInt>;
|
|
12
|
+
swr: z.ZodOptional<z.ZodInt>;
|
|
13
13
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
14
14
|
}, z.core.$strict>>;
|
|
15
15
|
}, z.core.$strip>;
|
package/dist/schemas.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import z from "zod";
|
|
|
2
2
|
|
|
3
3
|
//#region src/schemas.ts
|
|
4
4
|
const cacheOptionsSchema = z.strictObject({
|
|
5
|
-
ttl: z.
|
|
6
|
-
swr: z.
|
|
5
|
+
ttl: z.int().positive().optional(),
|
|
6
|
+
swr: z.int().positive().optional(),
|
|
7
7
|
tags: z.string().array().optional()
|
|
8
8
|
});
|
|
9
9
|
const cacheEnvelopeSchema = z.object({ cache: cacheOptionsSchema.optional() });
|
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,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@visualbravo/zenstack-cache",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|
|
7
9
|
"types": "./dist/index.d.cts",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"zenstack",
|
|
12
|
+
"cache",
|
|
13
|
+
"caching",
|
|
14
|
+
"prisma",
|
|
15
|
+
"accelerate",
|
|
16
|
+
"orm"
|
|
17
|
+
],
|
|
8
18
|
"exports": {
|
|
9
19
|
".": {
|
|
10
20
|
"@zenstack-cache/source": "./src/index.ts",
|
|
@@ -103,13 +113,13 @@
|
|
|
103
113
|
"zod": "^4.1.0"
|
|
104
114
|
},
|
|
105
115
|
"devDependencies": {
|
|
116
|
+
"@types/better-sqlite3": "7.6.13",
|
|
106
117
|
"@zenstack-cache/config": "0.0.0",
|
|
107
|
-
"kysely": "~0.28.8",
|
|
108
118
|
"better-sqlite3": "12.6.2",
|
|
109
|
-
"
|
|
119
|
+
"kysely": "~0.28.8"
|
|
110
120
|
},
|
|
111
121
|
"peerDependencies": {
|
|
112
122
|
"@zenstackhq/orm": "canary"
|
|
113
123
|
},
|
|
114
|
-
"packageManager": "bun@1.3.
|
|
124
|
+
"packageManager": "bun@1.3.8"
|
|
115
125
|
}
|