@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 +10 -10
- package/dist/db.mjs +56 -48
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +0 -1
- package/package.json +4 -4
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
|
|
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:
|
|
26
|
-
dbName:
|
|
27
|
-
dbType:
|
|
28
|
-
cacheTTL:
|
|
29
|
-
cachePurge:
|
|
30
|
-
strategy:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
28
|
-
dbName:
|
|
29
|
-
dbType:
|
|
30
|
-
cacheTTL:
|
|
31
|
-
cachePurge:
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chainfuse/helpers",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
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.
|
|
94
|
+
"@cloudflare/workers-types": "^4.20260414.1",
|
|
95
95
|
"@types/dns-packet": "^5.6.5"
|
|
96
96
|
},
|
|
97
|
-
"gitHead": "
|
|
97
|
+
"gitHead": "87a45416de45d6efef0c4a107fec5de29329b45e"
|
|
98
98
|
}
|