@chainfuse/helpers 4.4.8 → 4.5.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/dist/db.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { CacheStorageLike } from '@chainfuse/types';
2
2
  import { Cache as DrizzleCache, type MutationOption } from 'drizzle-orm/cache/core';
3
3
  import type { CacheConfig } from 'drizzle-orm/cache/core/types';
4
- import * as z from 'zod/mini';
4
+ import * as zm from 'zod/mini';
5
5
  /**
6
6
  * SQLCache is a cache implementation for SQL query results, using Web CacheStorage (supports drop in replacements).
7
7
  * It supports caching strategies for explicit or global query caching, and provides mechanisms for cache invalidation based on affected tables or tags.
@@ -22,17 +22,16 @@ export declare class SQLCache<C extends CacheStorageLike> extends DrizzleCache {
22
22
  private _strategy;
23
23
  private logging;
24
24
  private usedTablesPerKey;
25
- static constructorArgs: z.ZodMiniObject<{
26
- dbName: z.ZodMiniPipe<z.ZodMiniString<string>, z.ZodMiniTransform<string, string>>;
27
- dbType: z.ZodMiniPipe<z.ZodMiniString<string>, z.ZodMiniTransform<string, string>>;
28
- cacheTTL: z.ZodMiniDefault<z.ZodMiniNumberFormat>;
29
- cachePurge: z.ZodMiniDefault<z.ZodMiniUnion<readonly [z.ZodMiniBoolean<boolean>, z.ZodMiniDate<Date>]>>;
30
- strategy: z.ZodMiniDefault<z.ZodMiniEnum<{
25
+ static constructorArgs: zm.ZodMiniObject<{
26
+ dbName: zm.ZodMiniPipe<zm.ZodMiniString<string>, zm.ZodMiniTransform<string, string>>;
27
+ dbType: zm.ZodMiniPipe<zm.ZodMiniString<string>, zm.ZodMiniTransform<string, string>>;
28
+ cacheTTL: zm.ZodMiniDefault<zm.ZodMiniNumberFormat>;
29
+ cachePurge: zm.ZodMiniDefault<zm.ZodMiniUnion<readonly [zm.ZodMiniBoolean<boolean>, zm.ZodMiniDate<Date>, zm.ZodMiniCodec<zm.z.iso.ZodMiniISODateTime, zm.ZodMiniDate<Date>>]>>;
30
+ strategy: zm.ZodMiniDefault<zm.ZodMiniEnum<{
31
31
  all: "all";
32
32
  explicit: "explicit";
33
33
  }>>;
34
- logging: z.ZodMiniDefault<z.ZodMiniBoolean<boolean>>;
35
- }, z.core.$strip>;
34
+ }, zm.z.core.$strip>;
36
35
  /**
37
36
  * Creates an instance of the class with the specified database name, type, and cache TTL.
38
37
  *
@@ -44,7 +43,7 @@ export declare class SQLCache<C extends CacheStorageLike> extends DrizzleCache {
44
43
  * - `all`: All queries are cached globally.
45
44
  * @param cacheStore - The cache store to use. Can be a CacheStorage or CacheStorage-like object that atleast contains the `open()` function
46
45
  */
47
- constructor(args: z.input<(typeof SQLCache)['constructorArgs']>, cacheStore?: C);
46
+ constructor(args: zm.input<(typeof SQLCache)['constructorArgs']>, cacheStore?: C);
48
47
  /**
49
48
  * For the strategy, we have two options:
50
49
  * - `explicit`: The cache is used only when .$withCache() is added to a query.
@@ -52,6 +51,7 @@ export declare class SQLCache<C extends CacheStorageLike> extends DrizzleCache {
52
51
  * @default 'explicit'
53
52
  */
54
53
  strategy(): "all" | "explicit";
54
+ private log;
55
55
  /**
56
56
  * Generates a cache key as a `Request` object based on the provided tag or key.
57
57
  *
package/dist/db.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Cache as DrizzleCache } from 'drizzle-orm/cache/core';
2
2
  import { is } from 'drizzle-orm/entity';
3
3
  import { getTableName, Table } from 'drizzle-orm/table';
4
- import * as z from 'zod/mini';
4
+ import { channel } from 'node:diagnostics_channel';
5
+ import * as zm from 'zod/mini';
5
6
  import { CryptoHelpers } from "./crypto.mjs";
6
7
  /**
7
8
  * SQLCache is a cache implementation for SQL query results, using Web CacheStorage (supports drop in replacements).
@@ -21,16 +22,26 @@ export class SQLCache extends DrizzleCache {
21
22
  globalTtl;
22
23
  ttlCutoff;
23
24
  _strategy;
24
- logging;
25
+ logging = {
26
+ 'SQLCache:GET': channel('SQLCache:GET'),
27
+ 'SQLCache:PUT': channel('SQLCache:PUT'),
28
+ 'SQLCache:DELETE': channel('SQLCache:DELETE'),
29
+ };
25
30
  // This object will be used to store which query keys were used for a specific table, so we can later use it for invalidation.
26
31
  usedTablesPerKey = {};
27
- static constructorArgs = z.object({
28
- dbName: z.pipe(z.string().check(z.trim(), z.minLength(1)), z.transform((val) => encodeURIComponent(val))),
29
- dbType: z.pipe(z.string().check(z.trim(), z.minLength(1)), z.transform((val) => encodeURIComponent(val))),
30
- cacheTTL: z._default(z.int().check(z.nonnegative()), 5 * 60),
31
- cachePurge: z._default(z.union([z.boolean(), z.date()]), false),
32
- strategy: z._default(z.enum(['explicit', 'all']), 'explicit'),
33
- logging: z._default(z.boolean(), false),
32
+ static constructorArgs = zm.object({
33
+ dbName: zm.pipe(zm.string().check(zm.trim(), zm.minLength(1)), zm.transform((val) => encodeURIComponent(val))),
34
+ dbType: zm.pipe(zm.string().check(zm.trim(), zm.minLength(1)), zm.transform((val) => encodeURIComponent(val))),
35
+ cacheTTL: zm._default(zm.int().check(zm.nonnegative()), 5 * 60),
36
+ cachePurge: zm._default(zm.union([
37
+ zm.boolean(),
38
+ zm.date(),
39
+ zm.codec(zm.iso.datetime({ precision: 3, local: false, offset: false }), zm.date(), {
40
+ decode: (isoString) => new Date(isoString),
41
+ encode: (date) => date.toISOString(),
42
+ }),
43
+ ]), false),
44
+ strategy: zm._default(zm.enum(['explicit', 'all']), 'explicit'),
34
45
  });
35
46
  /**
36
47
  * Creates an instance of the class with the specified database name, type, and cache TTL.
@@ -45,7 +56,7 @@ export class SQLCache extends DrizzleCache {
45
56
  */
46
57
  constructor(args, cacheStore) {
47
58
  super();
48
- const { dbName, dbType, cacheTTL, cachePurge, strategy, logging } = SQLCache.constructorArgs.parse(args);
59
+ const { dbName, dbType, cacheTTL, cachePurge, strategy } = SQLCache.constructorArgs.parse(args);
49
60
  this.dbName = dbName;
50
61
  this.dbType = dbType;
51
62
  cacheStore ??= globalThis.caches;
@@ -58,7 +69,10 @@ export class SQLCache extends DrizzleCache {
58
69
  this.globalTtl = cacheTTL;
59
70
  this.ttlCutoff = cachePurge;
60
71
  this._strategy = strategy;
61
- this.logging = logging;
72
+ this.logging[`SQLCache:${this.dbName}.${this.dbType}:GET`] = channel(`SQLCache:${this.dbName}.${this.dbType}:GET`);
73
+ this.logging[`SQLCache:${this.dbName}.${this.dbType}:PUT`] = channel(`SQLCache:${this.dbName}.${this.dbType}:PUT`);
74
+ this.logging[`SQLCache:${this.dbName}.${this.dbType}:DELETE`] = channel(`SQLCache:${this.dbName}.${this.dbType}:DELETE`);
75
+ console.debug('SQLCache available logging channels', Object.keys(this.logging));
62
76
  }
63
77
  /**
64
78
  * For the strategy, we have two options:
@@ -69,6 +83,10 @@ export class SQLCache extends DrizzleCache {
69
83
  strategy() {
70
84
  return this._strategy;
71
85
  }
86
+ log(type, message) {
87
+ this.logging[`SQLCache:${this.dbName}.${this.dbType}:${type}`]?.publish(message);
88
+ this.logging[`SQLCache:${type}`].publish({ db: `${this.dbName}.${this.dbType}`, ...message });
89
+ }
72
90
  /**
73
91
  * Generates a cache key as a `Request` object based on the provided tag or key.
74
92
  *
@@ -89,37 +107,39 @@ export class SQLCache extends DrizzleCache {
89
107
  async get(key, _tables, isTag) {
90
108
  const cacheKey = this.getCacheKey(isTag ? { tag: key } : { key });
91
109
  const response = await this.cache.then(async (cache) => cache.match(cacheKey));
92
- if (this.logging)
93
- console.debug('SQLCache.get', isTag ? 'tag' : 'key', key, response?.ok ? 'HIT' : 'MISS');
94
110
  if (response) {
95
111
  // Check if cache should be purged
96
112
  if (this.ttlCutoff === true) {
97
- if (this.logging)
98
- console.debug('SQLCache.get', 'cache purged', this.ttlCutoff);
99
- await this.cache.then((cache) => cache.delete(cacheKey));
113
+ this.log('GET', { keyType: isTag ? 'tag' : 'key', key, status: 'EXPIRED', expires: new Date().toISOString() });
114
+ const deleted = await this.cache.then((cache) => cache.delete(cacheKey));
115
+ this.log('DELETE', { keyType: isTag ? 'tag' : 'key', key, status: deleted ? 'DELETED' : 'NOT_FOUND', reason: 'TTL Cutoff parameter is true' });
116
+ return undefined;
117
+ }
118
+ // If the response doesn't have a Date header, we can't check its age, so we will consider it malformed and remove it from the cache to avoid future issues.
119
+ if (!response.headers.has('Date')) {
120
+ this.log('GET', { keyType: isTag ? 'tag' : 'key', key, status: 'UNKNOWN' });
121
+ const deleted = await this.cache.then((cache) => cache.delete(cacheKey));
122
+ this.log('DELETE', { keyType: isTag ? 'tag' : 'key', key, status: deleted ? 'DELETED' : 'NOT_FOUND', reason: 'Malformed cached response' });
100
123
  return undefined;
101
124
  }
125
+ const cachedDate = new Date(response.headers.get('Date'));
102
126
  if (this.ttlCutoff instanceof Date) {
103
- const responseDate = response.headers.get('Date');
104
- if (responseDate) {
105
- const cachedDate = new Date(responseDate);
106
- if (cachedDate < this.ttlCutoff) {
107
- if (this.logging)
108
- console.debug('SQLCache.get', 'cache purged', { cachedDate, cutoff: this.ttlCutoff });
109
- await this.cache.then((cache) => cache.delete(cacheKey));
110
- return undefined;
111
- }
112
- }
113
- else {
114
- if (this.logging)
115
- console.debug('SQLCache.get', 'cache purged', { responseDate });
116
- await this.cache.then((cache) => cache.delete(cacheKey));
127
+ if (cachedDate < this.ttlCutoff) {
128
+ this.log('GET', { keyType: isTag ? 'tag' : 'key', key, status: 'EXPIRED', expires: this.ttlCutoff.toISOString() });
129
+ const deleted = await this.cache.then((cache) => cache.delete(cacheKey));
130
+ this.log('DELETE', { keyType: isTag ? 'tag' : 'key', key, status: deleted ? 'DELETED' : 'NOT_FOUND', reason: `Cached date ${cachedDate.toISOString()} is older than TTL cutoff ${this.ttlCutoff.toISOString()} by ${(this.ttlCutoff.getTime() - cachedDate.getTime()) / 1000} seconds` });
117
131
  return undefined;
118
132
  }
119
133
  }
134
+ const cacheControl = response.headers.get('Cache-Control');
135
+ const sMaxAge = /(?:^|,)\s*s-maxage=(\d+)\b/i.exec(cacheControl)?.at(1);
136
+ const maxAge = /(?:^|,)\s*max-age=(\d+)\b/i.exec(cacheControl)?.at(1);
137
+ const maxAgeSeconds = sMaxAge !== undefined ? Number.parseInt(sMaxAge, 10) : maxAge !== undefined ? Number.parseInt(maxAge, 10) : undefined;
138
+ this.log('GET', { keyType: isTag ? 'tag' : 'key', key, status: 'HIT', ...(maxAgeSeconds !== undefined && { expires: new Date(cachedDate.getTime() + maxAgeSeconds * 1000).toISOString() }) });
120
139
  return response.json();
121
140
  }
122
141
  else {
142
+ this.log('GET', { keyType: isTag ? 'tag' : 'key', key, status: 'MISS' });
123
143
  return undefined;
124
144
  }
125
145
  }
@@ -153,12 +173,8 @@ export class SQLCache extends DrizzleCache {
153
173
  },
154
174
  });
155
175
  cacheResponse.headers.set('ETag', await CryptoHelpers.generateETag(cacheResponse));
156
- await this.cache
157
- .then(async (cache) => cache.put(this.getCacheKey(isTag ? { tag: hashedQuery } : { key: hashedQuery }), cacheResponse))
158
- .then(() => {
159
- if (this.logging)
160
- console.debug('SQLCache.put', isTag ? 'tag' : 'key', hashedQuery, 'SUCCESS');
161
- });
176
+ await this.cache.then(async (cache) => cache.put(this.getCacheKey(isTag ? { tag: hashedQuery } : { key: hashedQuery }), cacheResponse));
177
+ this.log('PUT', { keyType: isTag ? 'tag' : 'key', key: hashedQuery, status: 'SAVED', expires: new Date(Date.now() + ttl * 1000).toISOString() });
162
178
  for (const table of tables) {
163
179
  const keys = this.usedTablesPerKey[table];
164
180
  if (keys === undefined) {
@@ -188,20 +204,12 @@ export class SQLCache extends DrizzleCache {
188
204
  }
189
205
  if (keysToDelete.size > 0 || tagsArray.length > 0) {
190
206
  for (const tag of tagsArray) {
191
- await this.cache
192
- .then(async (cache) => cache.delete(this.getCacheKey({ tag })))
193
- .then(() => {
194
- if (this.logging)
195
- console.debug('SQLCache.delete', 'tag', tag, 'SUCCESS');
196
- });
207
+ const deleted = await this.cache.then(async (cache) => cache.delete(this.getCacheKey({ tag })));
208
+ this.log('DELETE', { keyType: 'tag', key: tag, status: deleted ? 'DELETED' : 'NOT_FOUND', reason: 'Invalidated by tag on mutation' });
197
209
  }
198
210
  for (const key of keysToDelete) {
199
- await this.cache
200
- .then(async (cache) => cache.delete(this.getCacheKey({ key })))
201
- .then(() => {
202
- if (this.logging)
203
- console.debug('SQLCache.delete', 'key', key, 'SUCCESS');
204
- });
211
+ const deleted = await this.cache.then(async (cache) => cache.delete(this.getCacheKey({ key })));
212
+ this.log('DELETE', { keyType: 'key', key, status: deleted ? 'DELETED' : 'NOT_FOUND', reason: 'Invalidated by affected table on mutation' });
205
213
  for (const table of tablesArray) {
206
214
  const tableName = is(table, Table) ? getTableName(table) : table;
207
215
  this.usedTablesPerKey[tableName] = [];
package/dist/index.d.mts CHANGED
@@ -1,7 +1,6 @@
1
1
  export * from './buffers.mjs';
2
2
  export * from './common.mjs';
3
3
  export * from './crypto.mjs';
4
- export * from './db.mjs';
5
4
  export * from './discord.mjs';
6
5
  export * from './dns.mjs';
7
6
  export * from './net.mjs';
package/dist/index.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  export * from './buffers.mjs';
2
2
  export * from './common.mjs';
3
3
  export * from './crypto.mjs';
4
- export * from './db.mjs';
5
4
  export * from './discord.mjs';
6
5
  export * from './dns.mjs';
7
6
  export * from './net.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chainfuse/helpers",
3
- "version": "4.4.8",
3
+ "version": "4.5.0",
4
4
  "description": "",
5
5
  "author": "ChainFuse",
6
6
  "homepage": "https://github.com/ChainFuse/packages/tree/main/packages/helpers#readme",
@@ -80,7 +80,7 @@
80
80
  },
81
81
  "prettier": "@demosjarco/prettier-config",
82
82
  "dependencies": {
83
- "@chainfuse/types": "^4.2.22",
83
+ "@chainfuse/types": "^4.2.23",
84
84
  "@discordjs/rest": "^2.6.1",
85
85
  "chalk": "^5.6.2",
86
86
  "cloudflare": "^5.2.0",
@@ -91,8 +91,8 @@
91
91
  "zod": "^4.3.6"
92
92
  },
93
93
  "devDependencies": {
94
- "@cloudflare/workers-types": "^4.20260405.1",
94
+ "@cloudflare/workers-types": "^4.20260414.1",
95
95
  "@types/dns-packet": "^5.6.5"
96
96
  },
97
- "gitHead": "b0bcea1728f8c8e629219b670d3778dbe2cb4aae"
97
+ "gitHead": "87a45416de45d6efef0c4a107fec5de29329b45e"
98
98
  }