@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 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="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/visualbravo/zenstack-cache/build-and-test.yaml">
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/main/LICENSE">
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 Caching:** Centralizes your caching to scale across different systems.
31
- * đŸ–Ĩī¸ **Memory Caching:** Simplifies caching when scale is not a concern.
32
- * 🛟 **Type-safe:** The caching options appear in the intellisense for all read queries.
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 >= `canary`)
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
- for (const [tag, keys] of this.tagStore) {
21
- for (const key of keys) if (!this.entryStore.has(key)) keys.delete(key);
22
- if (keys.size === 0) this.tagStore.delete(tag);
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) for (const key of keys) this.entryStore.delete(key);
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
- for (const [tag, keys] of this.tagStore) {
21
- for (const key of keys) if (!this.entryStore.has(key)) keys.delete(key);
22
- if (keys.size === 0) this.tagStore.delete(tag);
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) for (const key of keys) this.entryStore.delete(key);
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
  }
@@ -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(formatQueryKey(key));
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 formattedKey = formatQueryKey(key);
21
- multi.set(formattedKey, superjson.default.stringify(entry));
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(formattedKey, totalTtl);
23
+ if (totalTtl > 0) multi.expire(queryKey, totalTtl);
24
24
  if (entry.options.tags) for (const tag of entry.options.tags) {
25
- const formattedTagKey = formatTagKey(tag);
26
- multi.sadd(formattedTagKey, formattedKey);
25
+ const tagKey = makeTagKey(tag);
26
+ multi.sadd(tagKey, queryKey);
27
27
  if (totalTtl > 0) {
28
- multi.expire(formattedTagKey, totalTtl, "GT");
29
- multi.expire(formattedTagKey, totalTtl, "NX");
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(formatTagKey(tag), { count: 100 });
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 formatQueryKey(key) {
60
+ function makeQueryKey(key) {
61
61
  return `zenstack:cache:query:${key}`;
62
62
  }
63
- function formatTagKey(key) {
63
+ function makeTagKey(key) {
64
64
  return `zenstack:cache:tag:${key}`;
65
65
  }
66
66
 
@@ -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(formatQueryKey(key));
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 formattedKey = formatQueryKey(key);
19
- multi.set(formattedKey, superjson.stringify(entry));
18
+ const queryKey = makeQueryKey(key);
19
+ multi.set(queryKey, superjson.stringify(entry));
20
20
  const totalTtl = getTotalTtl(entry);
21
- if (totalTtl > 0) multi.expire(formattedKey, totalTtl);
21
+ if (totalTtl > 0) multi.expire(queryKey, totalTtl);
22
22
  if (entry.options.tags) for (const tag of entry.options.tags) {
23
- const formattedTagKey = formatTagKey(tag);
24
- multi.sadd(formattedTagKey, formattedKey);
23
+ const tagKey = makeTagKey(tag);
24
+ multi.sadd(tagKey, queryKey);
25
25
  if (totalTtl > 0) {
26
- multi.expire(formattedTagKey, totalTtl, "GT");
27
- multi.expire(formattedTagKey, totalTtl, "NX");
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(formatTagKey(tag), { count: 100 });
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 formatQueryKey(key) {
58
+ function makeQueryKey(key) {
59
59
  return `zenstack:cache:query:${key}`;
60
60
  }
61
- function formatTagKey(key) {
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 : false;
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
- return Date.now() > entry.createdAt + getTotalTtl(entry) * 1e3;
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 : false;
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
- return Date.now() > entry.createdAt + getTotalTtl(entry) * 1e3;
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": "0.0.0",
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
- "@types/better-sqlite3": "7.6.13"
111
+ "kysely": "~0.28.8"
110
112
  },
111
113
  "peerDependencies": {
112
114
  "@zenstackhq/orm": "canary"
113
115
  },
114
- "packageManager": "bun@1.3.6"
116
+ "packageManager": "bun@1.3.8"
115
117
  }