@chainfuse/helpers 4.4.7 → 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.
@@ -20,17 +20,18 @@ export declare class SQLCache<C extends CacheStorageLike> extends DrizzleCache {
20
20
  private globalTtl;
21
21
  private ttlCutoff;
22
22
  private _strategy;
23
+ private logging;
23
24
  private usedTablesPerKey;
24
- static constructorArgs: z.ZodMiniObject<{
25
- dbName: z.ZodMiniPipe<z.ZodMiniString<string>, z.ZodMiniTransform<string, string>>;
26
- dbType: z.ZodMiniPipe<z.ZodMiniString<string>, z.ZodMiniTransform<string, string>>;
27
- cacheTTL: z.ZodMiniDefault<z.ZodMiniNumberFormat>;
28
- cachePurge: z.ZodMiniDefault<z.ZodMiniUnion<readonly [z.ZodMiniBoolean<boolean>, z.ZodMiniDate<Date>]>>;
29
- 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<{
30
31
  all: "all";
31
32
  explicit: "explicit";
32
33
  }>>;
33
- }, z.core.$strip>;
34
+ }, zm.z.core.$strip>;
34
35
  /**
35
36
  * Creates an instance of the class with the specified database name, type, and cache TTL.
36
37
  *
@@ -42,7 +43,7 @@ export declare class SQLCache<C extends CacheStorageLike> extends DrizzleCache {
42
43
  * - `all`: All queries are cached globally.
43
44
  * @param cacheStore - The cache store to use. Can be a CacheStorage or CacheStorage-like object that atleast contains the `open()` function
44
45
  */
45
- constructor(args: z.input<(typeof SQLCache)['constructorArgs']>, cacheStore?: C);
46
+ constructor(args: zm.input<(typeof SQLCache)['constructorArgs']>, cacheStore?: C);
46
47
  /**
47
48
  * For the strategy, we have two options:
48
49
  * - `explicit`: The cache is used only when .$withCache() is added to a query.
@@ -50,6 +51,7 @@ export declare class SQLCache<C extends CacheStorageLike> extends DrizzleCache {
50
51
  * @default 'explicit'
51
52
  */
52
53
  strategy(): "all" | "explicit";
54
+ private log;
53
55
  /**
54
56
  * Generates a cache key as a `Request` object based on the provided tag or key.
55
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,14 +22,26 @@ export class SQLCache extends DrizzleCache {
21
22
  globalTtl;
22
23
  ttlCutoff;
23
24
  _strategy;
25
+ logging = {
26
+ 'SQLCache:GET': channel('SQLCache:GET'),
27
+ 'SQLCache:PUT': channel('SQLCache:PUT'),
28
+ 'SQLCache:DELETE': channel('SQLCache:DELETE'),
29
+ };
24
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.
25
31
  usedTablesPerKey = {};
26
- static constructorArgs = z.object({
27
- dbName: z.pipe(z.string().check(z.minLength(1)), z.transform((val) => encodeURIComponent(val))),
28
- dbType: z.pipe(z.string().check(z.minLength(1)), z.transform((val) => encodeURIComponent(val))),
29
- cacheTTL: z._default(z.int().check(z.nonnegative()), 5 * 60),
30
- cachePurge: z._default(z.union([z.boolean(), z.date()]), false),
31
- strategy: z._default(z.enum(['explicit', 'all']), 'explicit'),
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'),
32
45
  });
33
46
  /**
34
47
  * Creates an instance of the class with the specified database name, type, and cache TTL.
@@ -56,6 +69,10 @@ export class SQLCache extends DrizzleCache {
56
69
  this.globalTtl = cacheTTL;
57
70
  this.ttlCutoff = cachePurge;
58
71
  this._strategy = strategy;
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));
59
76
  }
60
77
  /**
61
78
  * For the strategy, we have two options:
@@ -66,6 +83,10 @@ export class SQLCache extends DrizzleCache {
66
83
  strategy() {
67
84
  return this._strategy;
68
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
+ }
69
90
  /**
70
91
  * Generates a cache key as a `Request` object based on the provided tag or key.
71
92
  *
@@ -86,34 +107,39 @@ export class SQLCache extends DrizzleCache {
86
107
  async get(key, _tables, isTag) {
87
108
  const cacheKey = this.getCacheKey(isTag ? { tag: key } : { key });
88
109
  const response = await this.cache.then(async (cache) => cache.match(cacheKey));
89
- console.debug('SQLCache.get', isTag ? 'tag' : 'key', key, response?.ok ? 'HIT' : 'MISS');
90
110
  if (response) {
91
111
  // Check if cache should be purged
92
112
  if (this.ttlCutoff === true) {
93
- console.debug('SQLCache.get', 'cache purged', this.ttlCutoff);
94
- 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' });
95
123
  return undefined;
96
124
  }
125
+ const cachedDate = new Date(response.headers.get('Date'));
97
126
  if (this.ttlCutoff instanceof Date) {
98
- const responseDate = response.headers.get('Date');
99
- if (responseDate) {
100
- const cachedDate = new Date(responseDate);
101
- if (cachedDate < this.ttlCutoff) {
102
- console.debug('SQLCache.get', 'cache purged', { cachedDate, cutoff: this.ttlCutoff });
103
- await this.cache.then((cache) => cache.delete(cacheKey));
104
- return undefined;
105
- }
106
- }
107
- else {
108
- console.debug('SQLCache.get', 'cache purged', { responseDate });
109
- 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` });
110
131
  return undefined;
111
132
  }
112
133
  }
113
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
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() }) });
114
139
  return response.json();
115
140
  }
116
141
  else {
142
+ this.log('GET', { keyType: isTag ? 'tag' : 'key', key, status: 'MISS' });
117
143
  return undefined;
118
144
  }
119
145
  }
@@ -147,7 +173,8 @@ export class SQLCache extends DrizzleCache {
147
173
  },
148
174
  });
149
175
  cacheResponse.headers.set('ETag', await CryptoHelpers.generateETag(cacheResponse));
150
- await this.cache.then(async (cache) => cache.put(this.getCacheKey(isTag ? { tag: hashedQuery } : { key: hashedQuery }), cacheResponse)).then(() => console.debug('SQLCache.put', isTag ? 'tag' : 'key', hashedQuery, 'SUCCESS'));
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() });
151
178
  for (const table of tables) {
152
179
  const keys = this.usedTablesPerKey[table];
153
180
  if (keys === undefined) {
@@ -177,10 +204,12 @@ export class SQLCache extends DrizzleCache {
177
204
  }
178
205
  if (keysToDelete.size > 0 || tagsArray.length > 0) {
179
206
  for (const tag of tagsArray) {
180
- await this.cache.then(async (cache) => cache.delete(this.getCacheKey({ tag }))).then(() => console.debug('SQLCache.delete', 'tag', tag, 'SUCCESS'));
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' });
181
209
  }
182
210
  for (const key of keysToDelete) {
183
- await this.cache.then(async (cache) => cache.delete(this.getCacheKey({ key }))).then(() => console.debug('SQLCache.delete', 'key', key, 'SUCCESS'));
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' });
184
213
  for (const table of tablesArray) {
185
214
  const tableName = is(table, Table) ? getTableName(table) : table;
186
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.7",
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,19 +80,19 @@
80
80
  },
81
81
  "prettier": "@demosjarco/prettier-config",
82
82
  "dependencies": {
83
- "@chainfuse/types": "^4.2.21",
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",
87
87
  "dns-packet": "^5.6.1",
88
- "drizzle-orm": "^0.45.1",
88
+ "drizzle-orm": "^0.45.2",
89
89
  "strip-ansi": "^7.2.0",
90
90
  "uuid": "^13.0.0",
91
91
  "zod": "^4.3.6"
92
92
  },
93
93
  "devDependencies": {
94
- "@cloudflare/workers-types": "^4.20260317.1",
94
+ "@cloudflare/workers-types": "^4.20260414.1",
95
95
  "@types/dns-packet": "^5.6.5"
96
96
  },
97
- "gitHead": "dbc6f594a818b5c982269fe8271161972cce4b03"
97
+ "gitHead": "87a45416de45d6efef0c4a107fec5de29329b45e"
98
98
  }