raffel 1.1.73 → 1.1.74
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/cache/fs-layer.d.ts.map +1 -1
- package/dist/cache/fs-layer.js +25 -2
- package/dist/cache/fs-layer.js.map +1 -1
- package/dist/cache/index.d.ts +5 -5
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +2 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/key.d.ts +28 -7
- package/dist/cache/key.d.ts.map +1 -1
- package/dist/cache/key.js +191 -18
- package/dist/cache/key.js.map +1 -1
- package/dist/cache/redis-layer.d.ts +6 -0
- package/dist/cache/redis-layer.d.ts.map +1 -1
- package/dist/cache/redis-layer.js +150 -33
- package/dist/cache/redis-layer.js.map +1 -1
- package/dist/cache/server-runtime.d.ts +22 -9
- package/dist/cache/server-runtime.d.ts.map +1 -1
- package/dist/cache/server-runtime.js +190 -22
- package/dist/cache/server-runtime.js.map +1 -1
- package/dist/cache/tiered.d.ts +43 -2
- package/dist/cache/tiered.d.ts.map +1 -1
- package/dist/cache/tiered.js +457 -136
- package/dist/cache/tiered.js.map +1 -1
- package/dist/http/cache.d.ts +10 -1
- package/dist/http/cache.d.ts.map +1 -1
- package/dist/http/cache.js +105 -37
- package/dist/http/cache.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth/oauth2-client-credentials.d.ts.map +1 -1
- package/dist/middleware/auth/oauth2-client-credentials.js +3 -0
- package/dist/middleware/auth/oauth2-client-credentials.js.map +1 -1
- package/dist/middleware/auth/oauth2.d.ts.map +1 -1
- package/dist/middleware/auth/oauth2.js +8 -0
- package/dist/middleware/auth/oauth2.js.map +1 -1
- package/dist/middleware/auth.d.ts +2 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +62 -0
- package/dist/middleware/auth.js.map +1 -1
- package/dist/server/builder.d.ts.map +1 -1
- package/dist/server/builder.js +1 -0
- package/dist/server/builder.js.map +1 -1
- package/dist/server/fs-routes/middleware-processor.d.ts.map +1 -1
- package/dist/server/fs-routes/middleware-processor.js +15 -0
- package/dist/server/fs-routes/middleware-processor.js.map +1 -1
- package/dist/types/context.d.ts +2 -0
- package/dist/types/context.d.ts.map +1 -1
- package/dist/types/context.js +3 -0
- package/dist/types/context.js.map +1 -1
- package/dist/ui/cache/fs-layer.d.ts.map +1 -1
- package/dist/ui/cache/key.d.ts +28 -7
- package/dist/ui/cache/key.d.ts.map +1 -1
- package/dist/ui/cache/server-runtime.d.ts +22 -9
- package/dist/ui/cache/server-runtime.d.ts.map +1 -1
- package/dist/ui/cache/tiered.d.ts +43 -2
- package/dist/ui/cache/tiered.d.ts.map +1 -1
- package/dist/ui/server/fs-routes/middleware-processor.d.ts.map +1 -1
- package/dist/ui/types/context.d.ts +2 -0
- package/dist/ui/types/context.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,42 @@
|
|
|
1
|
-
import { isCacheRecord, } from './tiered.js';
|
|
1
|
+
import { cacheNamespacePrefix, isCacheRecord, } from './tiered.js';
|
|
2
2
|
import { DEFAULT_CACHE_CIRCUIT_COOLDOWN_MS, DEFAULT_CACHE_CIRCUIT_FAILURE_THRESHOLD, DEFAULT_CACHE_OPERATION_TIMEOUT_MS, } from './defaults.js';
|
|
3
|
+
function escapeRedisGlob(value) {
|
|
4
|
+
return value.replace(/[\\*?\[\]]/g, '\\$&');
|
|
5
|
+
}
|
|
6
|
+
const COMMIT_FILL_SCRIPT = `
|
|
7
|
+
local current = redis.call('GET', KEYS[1]) or '0'
|
|
8
|
+
if current ~= ARGV[1] then return 0 end
|
|
9
|
+
redis.call('SET', KEYS[2], ARGV[2], 'PX', ARGV[3])
|
|
10
|
+
return 1
|
|
11
|
+
`;
|
|
3
12
|
/** Redis/Valkey layer that delegates capacity and eviction to the provider. */
|
|
4
13
|
export function createRedisCacheLayer(options) {
|
|
5
|
-
|
|
6
|
-
|
|
14
|
+
let prefix = 'raffel:cache:';
|
|
15
|
+
if (options.prefix !== undefined) {
|
|
16
|
+
try {
|
|
17
|
+
encodeURIComponent(options.prefix);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error('Redis/Valkey prefix must contain valid Unicode');
|
|
21
|
+
}
|
|
22
|
+
prefix = `p${Buffer.byteLength(options.prefix)}:${options.prefix}`;
|
|
23
|
+
}
|
|
24
|
+
if (options.clusterHashTag?.includes('{') || options.clusterHashTag?.includes('}')) {
|
|
25
|
+
throw new Error('Redis/Valkey clusterHashTag cannot contain braces');
|
|
26
|
+
}
|
|
27
|
+
if (Boolean(options.clusterHashTag) !== Boolean(options.scanClients)) {
|
|
28
|
+
throw new Error('Redis/Valkey Cluster requires both clusterHashTag and scanClients');
|
|
29
|
+
}
|
|
30
|
+
const storagePrefix = options.clusterHashTag
|
|
31
|
+
? `${prefix}{${options.clusterHashTag}}:`
|
|
32
|
+
: prefix;
|
|
33
|
+
const dataPrefix = `${storagePrefix}d:`;
|
|
34
|
+
const fullKey = (key) => dataPrefix + key;
|
|
35
|
+
const generationKey = (namespace) => (`${storagePrefix}m:g:${Buffer.from(namespace).toString('base64url')}`);
|
|
36
|
+
const supportsDistributedFillFencing = Boolean(options.client.incr && options.client.eval);
|
|
37
|
+
const supportsInvalidation = Boolean(options.scanClients ||
|
|
38
|
+
options.client.scan ||
|
|
39
|
+
(options.allowBlockingClear && options.client.keys));
|
|
7
40
|
let hits = 0;
|
|
8
41
|
let misses = 0;
|
|
9
42
|
async function setWithNativeTtl(key, value, ttlMs) {
|
|
@@ -23,8 +56,78 @@ export function createRedisCacheLayer(options) {
|
|
|
23
56
|
}
|
|
24
57
|
await options.client.set(key, value, 'PX', ttlMs);
|
|
25
58
|
}
|
|
59
|
+
async function scanBatches(pattern, visit) {
|
|
60
|
+
const clients = options.scanClients?.() ?? [options.client];
|
|
61
|
+
if (clients.length === 0) {
|
|
62
|
+
throw new Error('Redis/Valkey scanClients returned no cluster masters');
|
|
63
|
+
}
|
|
64
|
+
for (const client of clients) {
|
|
65
|
+
if (!client.scan) {
|
|
66
|
+
if (options.allowBlockingClear && client.keys) {
|
|
67
|
+
const keys = await client.keys(pattern);
|
|
68
|
+
if (keys.length > 0)
|
|
69
|
+
await visit(client, keys);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
throw new Error('Redis/Valkey namespace invalidation requires SCAN support');
|
|
73
|
+
}
|
|
74
|
+
let cursor = '0';
|
|
75
|
+
do {
|
|
76
|
+
const result = options.clientStyle === 'node-redis' || client.pSetEx
|
|
77
|
+
? await client.scan(cursor, {
|
|
78
|
+
MATCH: pattern,
|
|
79
|
+
COUNT: options.scanCount ?? 100,
|
|
80
|
+
})
|
|
81
|
+
: await client.scan(cursor, 'MATCH', pattern, 'COUNT', options.scanCount ?? 100);
|
|
82
|
+
const next = Array.isArray(result) ? result[0] : result.cursor;
|
|
83
|
+
const keys = Array.isArray(result) ? result[1] : result.keys;
|
|
84
|
+
cursor = String(next);
|
|
85
|
+
if (keys.length > 0)
|
|
86
|
+
await visit(client, keys);
|
|
87
|
+
} while (cursor !== '0');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function deleteScannedKeys(client, keys) {
|
|
91
|
+
if (!options.scanClients)
|
|
92
|
+
return client.del(keys);
|
|
93
|
+
const deleted = await Promise.all(keys.map((key) => client.del(key)));
|
|
94
|
+
return deleted.reduce((total, count) => total + count, 0);
|
|
95
|
+
}
|
|
96
|
+
async function deletePattern(pattern) {
|
|
97
|
+
let deleted = 0;
|
|
98
|
+
await scanBatches(pattern, async (client, keys) => {
|
|
99
|
+
deleted += await deleteScannedKeys(client, keys);
|
|
100
|
+
});
|
|
101
|
+
return deleted;
|
|
102
|
+
}
|
|
103
|
+
function storedRecord(record, ttlMs, staleMs) {
|
|
104
|
+
const expiresAt = Date.now() + ttlMs;
|
|
105
|
+
return {
|
|
106
|
+
...record,
|
|
107
|
+
expiresAt,
|
|
108
|
+
staleUntil: staleMs > 0 ? expiresAt + staleMs : undefined,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function commitFillAtomically(key, record, ttlMs, staleMs, token, namespace) {
|
|
112
|
+
const encoded = JSON.stringify(storedRecord(record, ttlMs, staleMs));
|
|
113
|
+
const providerKey = fullKey(key);
|
|
114
|
+
const generation = generationKey(namespace);
|
|
115
|
+
const duration = String(ttlMs + staleMs);
|
|
116
|
+
const result = options.clientStyle === 'node-redis' || options.client.pSetEx
|
|
117
|
+
? await options.client.eval(COMMIT_FILL_SCRIPT, {
|
|
118
|
+
keys: [generation, providerKey],
|
|
119
|
+
arguments: [String(token), encoded, duration],
|
|
120
|
+
})
|
|
121
|
+
: await options.client.eval(COMMIT_FILL_SCRIPT, 2, generation, providerKey, String(token), encoded, duration);
|
|
122
|
+
return Number(result) === 1;
|
|
123
|
+
}
|
|
26
124
|
return {
|
|
27
125
|
id: options.id,
|
|
126
|
+
capabilities: {
|
|
127
|
+
distributedFillFencing: supportsDistributedFillFencing,
|
|
128
|
+
prefixInvalidation: supportsInvalidation ? 'scan' : false,
|
|
129
|
+
tagInvalidation: supportsInvalidation ? 'scan' : false,
|
|
130
|
+
},
|
|
28
131
|
ttlMs: options.ttlMs,
|
|
29
132
|
readTimeoutMs: options.readTimeoutMs ?? 100,
|
|
30
133
|
operationTimeoutMs: options.operationTimeoutMs ?? DEFAULT_CACHE_OPERATION_TIMEOUT_MS,
|
|
@@ -59,42 +162,56 @@ export function createRedisCacheLayer(options) {
|
|
|
59
162
|
return record;
|
|
60
163
|
},
|
|
61
164
|
async set(key, record, ttlMs, staleMs = 0) {
|
|
62
|
-
const
|
|
63
|
-
const stored = {
|
|
64
|
-
...record,
|
|
65
|
-
expiresAt,
|
|
66
|
-
staleUntil: staleMs > 0 ? expiresAt + staleMs : undefined,
|
|
67
|
-
};
|
|
165
|
+
const stored = storedRecord(record, ttlMs, staleMs);
|
|
68
166
|
await setWithNativeTtl(fullKey(key), JSON.stringify(stored), ttlMs + staleMs);
|
|
69
167
|
},
|
|
168
|
+
...(supportsDistributedFillFencing ? {
|
|
169
|
+
async beginFill(_key, namespace) {
|
|
170
|
+
return await options.client.get(generationKey(namespace)) ?? '0';
|
|
171
|
+
},
|
|
172
|
+
async isFillCurrent(token, namespace) {
|
|
173
|
+
const current = await options.client.get(generationKey(namespace)) ?? '0';
|
|
174
|
+
return current === String(token);
|
|
175
|
+
},
|
|
176
|
+
commitFill: commitFillAtomically,
|
|
177
|
+
async bumpGeneration(namespace) {
|
|
178
|
+
await options.client.incr(generationKey(namespace));
|
|
179
|
+
},
|
|
180
|
+
} : {}),
|
|
70
181
|
async delete(key) {
|
|
71
182
|
await options.client.del(fullKey(key));
|
|
72
183
|
},
|
|
73
184
|
async clearNamespace(namespace) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
185
|
+
const namespacePrefix = dataPrefix + cacheNamespacePrefix(namespace);
|
|
186
|
+
await deletePattern(`${escapeRedisGlob(namespacePrefix)}*`);
|
|
187
|
+
},
|
|
188
|
+
async invalidatePrefix(logicalPrefix, namespace) {
|
|
189
|
+
const namespacePrefix = dataPrefix + cacheNamespacePrefix(namespace);
|
|
190
|
+
const pattern = `${escapeRedisGlob(namespacePrefix + logicalPrefix)}*`;
|
|
191
|
+
return { mode: 'physical', deleted: await deletePattern(pattern) };
|
|
192
|
+
},
|
|
193
|
+
async invalidateTag(tag, namespace) {
|
|
194
|
+
const namespacePrefix = dataPrefix + cacheNamespacePrefix(namespace);
|
|
195
|
+
const pattern = `${escapeRedisGlob(namespacePrefix)}*`;
|
|
196
|
+
let deleted = 0;
|
|
197
|
+
await scanBatches(pattern, async (client, keys) => {
|
|
198
|
+
const records = await Promise.all(keys.map(async (key) => {
|
|
199
|
+
const encoded = await client.get(key);
|
|
200
|
+
if (!encoded)
|
|
201
|
+
return undefined;
|
|
202
|
+
try {
|
|
203
|
+
const record = JSON.parse(encoded);
|
|
204
|
+
return isCacheRecord(record) ? record : undefined;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
}));
|
|
210
|
+
const matches = keys.filter((_key, index) => records[index]?.tags?.includes(tag));
|
|
211
|
+
if (matches.length > 0)
|
|
212
|
+
deleted += await deleteScannedKeys(client, matches);
|
|
213
|
+
});
|
|
214
|
+
return { mode: 'physical', deleted };
|
|
98
215
|
},
|
|
99
216
|
stats() {
|
|
100
217
|
return { totalItems: 0, hits, misses };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis-layer.js","sourceRoot":"","sources":["../../src/cache/redis-layer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,GAId,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,iCAAiC,EACjC,uCAAuC,EACvC,kCAAkC,GACnC,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"redis-layer.js","sourceRoot":"","sources":["../../src/cache/redis-layer.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { Context, Interceptor } from '../types/index.js';
|
|
2
|
-
import { type
|
|
3
|
-
import { type CacheCircuitBreakerOptions, type CacheWriteOptions, type TieredCache, type TieredCacheOptions } from './tiered.js';
|
|
2
|
+
import { type ProcedureCacheKeyOptions } from './key.js';
|
|
3
|
+
import { type CacheCircuitBreakerOptions, type CacheInvalidationResult, type TieredCacheAccess, type CacheWriteOptions, type TieredCache, type TieredCacheOptions } from './tiered.js';
|
|
4
4
|
import type { WriteBehindQueueOptions } from './write-behind-queue.js';
|
|
5
|
-
export
|
|
5
|
+
export type CacheTagResolver = readonly string[] | ((input: unknown, ctx: Context, result: unknown) => readonly string[]);
|
|
6
|
+
export interface RouteCacheConfig extends Omit<CacheWriteOptions, 'tags'>, ProcedureCacheKeyOptions {
|
|
6
7
|
enabled?: boolean;
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
profile?: string;
|
|
9
|
+
tags?: CacheTagResolver;
|
|
10
|
+
}
|
|
11
|
+
export interface CacheProfileConfig extends Omit<CacheWriteOptions, 'tags'> {
|
|
12
|
+
layers: string[];
|
|
13
|
+
/** Coherence guarantee promised by this profile's selected topology. */
|
|
14
|
+
coherence?: 'ttl' | 'shared-invalidation' | 'backplane';
|
|
9
15
|
}
|
|
10
16
|
export interface CacheRule extends RouteCacheConfig {
|
|
11
17
|
match: string;
|
|
@@ -43,29 +49,36 @@ export interface ServerCacheConfig {
|
|
|
43
49
|
enabled: boolean;
|
|
44
50
|
namespace?: string;
|
|
45
51
|
layers: ServerCacheLayerConfig[];
|
|
52
|
+
profiles?: Readonly<Record<string, CacheProfileConfig>>;
|
|
46
53
|
rules?: CacheRule[];
|
|
47
54
|
writeBehind?: WriteBehindQueueOptions;
|
|
48
55
|
onLayerError?: TieredCacheOptions['onLayerError'];
|
|
49
56
|
}
|
|
50
57
|
export interface ServerCacheController {
|
|
51
58
|
invalidate(key: string): Promise<void>;
|
|
59
|
+
invalidateTag(tag: string): Promise<CacheInvalidationResult>;
|
|
60
|
+
invalidatePrefix(prefix: string): Promise<CacheInvalidationResult>;
|
|
52
61
|
clear(): Promise<void>;
|
|
53
|
-
keyForProcedure(procedure: string, input: unknown, ctx: Context, options?:
|
|
54
|
-
|
|
62
|
+
keyForProcedure(procedure: string, input: unknown, ctx: Context, options?: ProcedureCacheKeyOptions): string | undefined;
|
|
63
|
+
/** @deprecated Prefer dependency tags or logical-prefix invalidation. */
|
|
64
|
+
invalidateProcedure(procedure: string, input: unknown, ctx: Context, options?: ProcedureCacheKeyOptions): Promise<boolean>;
|
|
55
65
|
stats(): ReturnType<TieredCache['stats']>;
|
|
56
66
|
}
|
|
57
67
|
export declare class ServerCacheRuntime {
|
|
58
68
|
private readonly config;
|
|
59
69
|
private current;
|
|
70
|
+
private readonly profileCaches;
|
|
71
|
+
private readonly layersById;
|
|
60
72
|
private binders;
|
|
73
|
+
private providersValidated;
|
|
61
74
|
private readonly pending;
|
|
62
75
|
readonly controller: ServerCacheController;
|
|
63
76
|
constructor(config: ServerCacheConfig);
|
|
64
77
|
get cache(): TieredCache;
|
|
65
78
|
interceptorFor(procedure: string, explicit?: RouteCacheConfig | false): Interceptor | undefined;
|
|
66
79
|
bind(services: Readonly<Record<string, unknown>>): void;
|
|
67
|
-
executeOnce(key: string, next: () => Promise<unknown>,
|
|
68
|
-
revalidate(key: string, next: () => Promise<unknown>,
|
|
80
|
+
executeOnce(key: string, next: () => Promise<unknown>, writeOptions: CacheWriteOptions | ((value: unknown) => CacheWriteOptions | undefined), cache?: TieredCacheAccess, planId?: string): Promise<unknown>;
|
|
81
|
+
revalidate(key: string, next: () => Promise<unknown>, writeOptions: CacheWriteOptions | ((value: unknown) => CacheWriteOptions | undefined), cache?: TieredCacheAccess, planId?: string): void;
|
|
69
82
|
start(): void;
|
|
70
83
|
stop(): Promise<void>;
|
|
71
84
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-runtime.d.ts","sourceRoot":"","sources":["../../src/cache/server-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAY,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAGvE,OAAO,
|
|
1
|
+
{"version":3,"file":"server-runtime.d.ts","sourceRoot":"","sources":["../../src/cache/server-runtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { matchProcedurePattern } from '../utils/pattern-match.js';
|
|
2
2
|
import { createFileSystemCacheLayer } from './fs-layer.js';
|
|
3
|
-
import {
|
|
3
|
+
import { compileProcedureCacheKey, procedureCacheKeyFor, } from './key.js';
|
|
4
4
|
import { createMemoryCacheLayer, createTieredCache, } from './tiered.js';
|
|
5
5
|
function providerLayer(config, id) {
|
|
6
6
|
let delegate;
|
|
7
|
+
let capabilities = { distributedFillFencing: false };
|
|
7
8
|
const requireDelegate = () => {
|
|
8
9
|
if (!delegate)
|
|
9
10
|
throw new Error(`Cache provider "${config.provider}" is not available`);
|
|
@@ -19,10 +20,26 @@ function providerLayer(config, id) {
|
|
|
19
20
|
'delete' in candidate &&
|
|
20
21
|
'clearNamespace' in candidate) {
|
|
21
22
|
delegate = candidate;
|
|
23
|
+
const hasCompleteFencing = Boolean(delegate.capabilities?.distributedFillFencing &&
|
|
24
|
+
delegate.beginFill &&
|
|
25
|
+
delegate.isFillCurrent &&
|
|
26
|
+
delegate.commitFill &&
|
|
27
|
+
delegate.bumpGeneration);
|
|
28
|
+
capabilities = {
|
|
29
|
+
...delegate.capabilities,
|
|
30
|
+
distributedFillFencing: hasCompleteFencing,
|
|
31
|
+
tagInvalidation: delegate.invalidateTag
|
|
32
|
+
? delegate.capabilities?.tagInvalidation ?? false
|
|
33
|
+
: false,
|
|
34
|
+
prefixInvalidation: delegate.invalidatePrefix
|
|
35
|
+
? delegate.capabilities?.prefixInvalidation ?? false
|
|
36
|
+
: false,
|
|
37
|
+
};
|
|
22
38
|
}
|
|
23
39
|
},
|
|
24
40
|
layer: {
|
|
25
41
|
id,
|
|
42
|
+
get capabilities() { return capabilities; },
|
|
26
43
|
ttlMs: config.ttlMs ?? 60 * 60_000,
|
|
27
44
|
writeBehind: config.writeBehind,
|
|
28
45
|
readTimeoutMs: config.timeoutMs ?? 100,
|
|
@@ -34,6 +51,47 @@ function providerLayer(config, id) {
|
|
|
34
51
|
set: (key, record, ttlMs, staleMs) => requireDelegate().set(key, record, ttlMs, staleMs),
|
|
35
52
|
delete: (key) => requireDelegate().delete(key),
|
|
36
53
|
clearNamespace: (namespace) => requireDelegate().clearNamespace(namespace),
|
|
54
|
+
async beginFill(key, namespace) {
|
|
55
|
+
const candidate = requireDelegate();
|
|
56
|
+
if (!candidate.beginFill || !candidate.commitFill)
|
|
57
|
+
return { fenced: false };
|
|
58
|
+
return { fenced: true, token: await candidate.beginFill(key, namespace) };
|
|
59
|
+
},
|
|
60
|
+
async isFillCurrent(token, namespace) {
|
|
61
|
+
const captured = token;
|
|
62
|
+
if (!captured.fenced)
|
|
63
|
+
return true;
|
|
64
|
+
const candidate = requireDelegate();
|
|
65
|
+
return candidate.isFillCurrent
|
|
66
|
+
? candidate.isFillCurrent(captured.token, namespace)
|
|
67
|
+
: true;
|
|
68
|
+
},
|
|
69
|
+
async commitFill(key, record, ttlMs, staleMs, token, namespace) {
|
|
70
|
+
const captured = token;
|
|
71
|
+
const candidate = requireDelegate();
|
|
72
|
+
if (!captured.fenced || !candidate.commitFill) {
|
|
73
|
+
await candidate.set(key, record, ttlMs, staleMs);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return candidate.commitFill(key, record, ttlMs, staleMs, captured.token, namespace);
|
|
77
|
+
},
|
|
78
|
+
async bumpGeneration(namespace) {
|
|
79
|
+
await requireDelegate().bumpGeneration?.(namespace);
|
|
80
|
+
},
|
|
81
|
+
invalidateTag: (tag, namespace) => {
|
|
82
|
+
const candidate = requireDelegate().invalidateTag;
|
|
83
|
+
if (!candidate) {
|
|
84
|
+
throw new Error(`Cache provider "${config.provider}" does not support tag invalidation`);
|
|
85
|
+
}
|
|
86
|
+
return candidate.call(requireDelegate(), tag, namespace);
|
|
87
|
+
},
|
|
88
|
+
invalidatePrefix: (prefix, namespace) => {
|
|
89
|
+
const candidate = requireDelegate().invalidatePrefix;
|
|
90
|
+
if (!candidate) {
|
|
91
|
+
throw new Error(`Cache provider "${config.provider}" does not support prefix invalidation`);
|
|
92
|
+
}
|
|
93
|
+
return candidate.call(requireDelegate(), prefix, namespace);
|
|
94
|
+
},
|
|
37
95
|
stats: () => delegate?.stats?.() ?? { totalItems: 0 },
|
|
38
96
|
shutdown: () => delegate?.shutdown?.(),
|
|
39
97
|
},
|
|
@@ -48,32 +106,57 @@ function isSuccessful(value) {
|
|
|
48
106
|
function cloneResponse(value) {
|
|
49
107
|
return value instanceof Response ? value.clone() : value;
|
|
50
108
|
}
|
|
51
|
-
function
|
|
109
|
+
function routeWriteOptions(config, envelope, ctx, result) {
|
|
110
|
+
const tags = typeof config.tags === 'function'
|
|
111
|
+
? config.tags(envelope.payload, ctx, result)
|
|
112
|
+
: config.tags;
|
|
113
|
+
return {
|
|
114
|
+
ttlMs: config.ttlMs,
|
|
115
|
+
staleMs: config.staleMs,
|
|
116
|
+
tags,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function createProcedureInterceptor(runtime, procedure, config, cache, planId) {
|
|
120
|
+
const keyFor = compileProcedureCacheKey(procedure, config);
|
|
52
121
|
return async (envelope, ctx, next) => {
|
|
53
122
|
runtime.bind(ctx.services);
|
|
54
|
-
const key =
|
|
123
|
+
const key = keyFor(envelope.payload, ctx);
|
|
55
124
|
if (!key)
|
|
56
125
|
return next();
|
|
57
|
-
const hit = await
|
|
126
|
+
const hit = await cache.get(key);
|
|
58
127
|
if (hit && !hit.stale)
|
|
59
128
|
return hit.value;
|
|
129
|
+
const writeOptions = (result) => {
|
|
130
|
+
try {
|
|
131
|
+
return routeWriteOptions(config, envelope, ctx, result);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
ctx.logger.warn({ error, procedure: envelope.procedure }, 'Cache tag resolver failed; the result was not cached');
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
60
138
|
if (hit?.stale) {
|
|
61
|
-
runtime.revalidate(key, next,
|
|
139
|
+
runtime.revalidate(key, next, writeOptions, cache, planId);
|
|
62
140
|
return hit.value;
|
|
63
141
|
}
|
|
64
|
-
return runtime.executeOnce(key, next,
|
|
142
|
+
return runtime.executeOnce(key, next, writeOptions, cache, planId);
|
|
65
143
|
};
|
|
66
144
|
}
|
|
67
145
|
export class ServerCacheRuntime {
|
|
68
146
|
config;
|
|
69
147
|
current;
|
|
148
|
+
profileCaches = new Map();
|
|
149
|
+
layersById = new Map();
|
|
70
150
|
binders = [];
|
|
151
|
+
providersValidated = false;
|
|
71
152
|
pending = new Map();
|
|
72
153
|
controller;
|
|
73
154
|
constructor(config) {
|
|
74
155
|
this.config = config;
|
|
75
156
|
this.controller = {
|
|
76
157
|
invalidate: (key) => this.cache.delete(key),
|
|
158
|
+
invalidateTag: (tag) => this.cache.invalidateTag(tag),
|
|
159
|
+
invalidatePrefix: (prefix) => this.cache.invalidatePrefix(prefix),
|
|
77
160
|
clear: () => this.cache.clearNamespace(),
|
|
78
161
|
keyForProcedure: (procedure, input, ctx, options) => procedureCacheKeyFor(procedure, input, ctx, options),
|
|
79
162
|
invalidateProcedure: async (procedure, input, ctx, options) => {
|
|
@@ -93,7 +176,7 @@ export class ServerCacheRuntime {
|
|
|
93
176
|
return this.current;
|
|
94
177
|
}
|
|
95
178
|
interceptorFor(procedure, explicit) {
|
|
96
|
-
const resolved = {
|
|
179
|
+
const resolved = {};
|
|
97
180
|
for (const rule of this.config.rules ?? []) {
|
|
98
181
|
if (matchProcedurePattern(rule.match, procedure))
|
|
99
182
|
Object.assign(resolved, rule);
|
|
@@ -102,36 +185,93 @@ export class ServerCacheRuntime {
|
|
|
102
185
|
resolved.enabled = false;
|
|
103
186
|
else if (explicit)
|
|
104
187
|
Object.assign(resolved, explicit);
|
|
105
|
-
|
|
188
|
+
if (resolved.enabled === undefined)
|
|
189
|
+
resolved.enabled = Boolean(resolved.profile);
|
|
190
|
+
if (!resolved.enabled)
|
|
191
|
+
return undefined;
|
|
192
|
+
const profileName = resolved.profile;
|
|
193
|
+
if (!profileName) {
|
|
194
|
+
return createProcedureInterceptor(this, procedure, resolved, this.cache, 'default');
|
|
195
|
+
}
|
|
196
|
+
const profile = this.config.profiles?.[profileName];
|
|
197
|
+
if (!profile)
|
|
198
|
+
throw new Error(`Cache profile "${profileName}" does not exist`);
|
|
199
|
+
let cache = this.profileCaches.get(profileName);
|
|
200
|
+
if (!cache) {
|
|
201
|
+
cache = this.cache.selectLayers(profile.layers);
|
|
202
|
+
this.profileCaches.set(profileName, cache);
|
|
203
|
+
}
|
|
204
|
+
const { layers: _layers, ...profileDefaults } = profile;
|
|
205
|
+
const effective = { ...profileDefaults, ...resolved };
|
|
206
|
+
return createProcedureInterceptor(this, procedure, effective, cache, profileName);
|
|
106
207
|
}
|
|
107
208
|
bind(services) {
|
|
209
|
+
if (this.providersValidated)
|
|
210
|
+
return;
|
|
108
211
|
for (const binder of this.binders)
|
|
109
212
|
binder.bind(services);
|
|
213
|
+
for (const [profileName, profile] of Object.entries(this.config.profiles ?? {})) {
|
|
214
|
+
if (profile.coherence !== 'shared-invalidation')
|
|
215
|
+
continue;
|
|
216
|
+
for (const id of profile.layers) {
|
|
217
|
+
const layer = this.layersById.get(id);
|
|
218
|
+
if (!layer.capabilities?.distributedFillFencing) {
|
|
219
|
+
throw new Error(`Cache profile "${profileName}" requires distributed fill fencing on layer "${id}"`);
|
|
220
|
+
}
|
|
221
|
+
if (!layer.capabilities.tagInvalidation || !layer.capabilities.prefixInvalidation) {
|
|
222
|
+
throw new Error(`Cache profile "${profileName}" requires tag and prefix invalidation on layer "${id}"`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
this.providersValidated = true;
|
|
110
227
|
}
|
|
111
|
-
executeOnce(key, next,
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
228
|
+
executeOnce(key, next, writeOptions, cache = this.cache, planId = 'default') {
|
|
229
|
+
const pendingKey = `${planId}\0${key}`;
|
|
230
|
+
const existing = this.pending.get(pendingKey);
|
|
231
|
+
if (existing) {
|
|
232
|
+
return existing.cache.isFillCurrent(existing.ticket).then((current) => {
|
|
233
|
+
if (current)
|
|
234
|
+
return existing.execution.then(cloneResponse);
|
|
235
|
+
if (this.pending.get(pendingKey) === existing)
|
|
236
|
+
this.pending.delete(pendingKey);
|
|
237
|
+
return this.executeOnce(key, next, writeOptions, cache, planId);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const ticket = cache.beginFill(key);
|
|
241
|
+
const execution = Promise.resolve()
|
|
242
|
+
.then(() => ticket.ready)
|
|
243
|
+
.then(next)
|
|
116
244
|
.then(async (value) => {
|
|
117
|
-
if (isSuccessful(value))
|
|
118
|
-
|
|
245
|
+
if (isSuccessful(value)) {
|
|
246
|
+
const resolved = typeof writeOptions === 'function'
|
|
247
|
+
? writeOptions(value)
|
|
248
|
+
: writeOptions;
|
|
249
|
+
if (resolved)
|
|
250
|
+
await cache.commitFill(ticket, value, resolved);
|
|
251
|
+
}
|
|
119
252
|
return value;
|
|
120
253
|
})
|
|
121
|
-
.finally(() =>
|
|
122
|
-
|
|
254
|
+
.finally(() => {
|
|
255
|
+
cache.cancelFill(ticket);
|
|
256
|
+
if (this.pending.get(pendingKey)?.execution === execution) {
|
|
257
|
+
this.pending.delete(pendingKey);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
this.pending.set(pendingKey, { cache, execution, ticket });
|
|
123
261
|
return execution.then(cloneResponse);
|
|
124
262
|
}
|
|
125
|
-
revalidate(key, next,
|
|
126
|
-
|
|
127
|
-
return;
|
|
128
|
-
void this.executeOnce(key, next, config).catch(() => undefined);
|
|
263
|
+
revalidate(key, next, writeOptions, cache = this.cache, planId = 'default') {
|
|
264
|
+
void this.executeOnce(key, next, writeOptions, cache, planId).catch(() => undefined);
|
|
129
265
|
}
|
|
130
266
|
start() {
|
|
131
267
|
if (this.current)
|
|
132
268
|
return;
|
|
133
269
|
this.binders = [];
|
|
270
|
+
this.providersValidated = false;
|
|
271
|
+
this.profileCaches.clear();
|
|
272
|
+
this.layersById.clear();
|
|
134
273
|
const layers = [];
|
|
274
|
+
const layerConfigsById = new Map();
|
|
135
275
|
const ids = new Set();
|
|
136
276
|
for (const [index, layer] of this.config.layers.entries()) {
|
|
137
277
|
if (layer.enabled === false)
|
|
@@ -140,6 +280,7 @@ export class ServerCacheRuntime {
|
|
|
140
280
|
if (ids.has(id))
|
|
141
281
|
throw new Error(`Cache layer id "${id}" is duplicated`);
|
|
142
282
|
ids.add(id);
|
|
283
|
+
layerConfigsById.set(id, layer);
|
|
143
284
|
if (layer.ttlMs !== undefined && layer.ttlMs <= 0) {
|
|
144
285
|
throw new Error(`Cache layer "${id}" ttlMs must be greater than zero`);
|
|
145
286
|
}
|
|
@@ -195,19 +336,46 @@ export class ServerCacheRuntime {
|
|
|
195
336
|
this.binders.push(bound);
|
|
196
337
|
layers.push(bound.layer);
|
|
197
338
|
}
|
|
339
|
+
this.layersById.set(id, layers.at(-1));
|
|
198
340
|
}
|
|
199
341
|
if (layers.length === 0)
|
|
200
342
|
throw new Error('At least one cache layer must be enabled');
|
|
201
|
-
|
|
343
|
+
const cache = createTieredCache({
|
|
202
344
|
namespace: this.config.namespace ?? 'raffel',
|
|
203
345
|
layers,
|
|
204
346
|
writeBehind: this.config.writeBehind,
|
|
205
347
|
onLayerError: this.config.onLayerError,
|
|
206
348
|
});
|
|
349
|
+
for (const [profileName, profile] of Object.entries(this.config.profiles ?? {})) {
|
|
350
|
+
try {
|
|
351
|
+
const selected = cache.selectLayers(profile.layers);
|
|
352
|
+
if (profile.coherence === 'backplane') {
|
|
353
|
+
throw new Error(`Cache profile "${profileName}" requires an invalidation backplane, which is not configured`);
|
|
354
|
+
}
|
|
355
|
+
if (profile.coherence === 'shared-invalidation') {
|
|
356
|
+
const configuredLayers = profile.layers.map((id) => layerConfigsById.get(id));
|
|
357
|
+
if (configuredLayers.some((layer) => layer?.driver !== 'provider')) {
|
|
358
|
+
throw new Error(`Cache profile "${profileName}" with shared-invalidation coherence can only use provider layers`);
|
|
359
|
+
}
|
|
360
|
+
if (configuredLayers.length !== 1) {
|
|
361
|
+
throw new Error(`Cache profile "${profileName}" with shared-invalidation coherence requires exactly one provider layer`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
this.profileCaches.set(profileName, selected);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
void cache.shutdown();
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
this.current = cache;
|
|
207
372
|
}
|
|
208
373
|
async stop() {
|
|
209
374
|
const cache = this.current;
|
|
210
375
|
this.current = undefined;
|
|
376
|
+
this.profileCaches.clear();
|
|
377
|
+
this.layersById.clear();
|
|
378
|
+
this.providersValidated = false;
|
|
211
379
|
if (cache)
|
|
212
380
|
await cache.shutdown();
|
|
213
381
|
}
|