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.
Files changed (62) hide show
  1. package/dist/cache/fs-layer.d.ts.map +1 -1
  2. package/dist/cache/fs-layer.js +25 -2
  3. package/dist/cache/fs-layer.js.map +1 -1
  4. package/dist/cache/index.d.ts +5 -5
  5. package/dist/cache/index.d.ts.map +1 -1
  6. package/dist/cache/index.js +2 -2
  7. package/dist/cache/index.js.map +1 -1
  8. package/dist/cache/key.d.ts +28 -7
  9. package/dist/cache/key.d.ts.map +1 -1
  10. package/dist/cache/key.js +191 -18
  11. package/dist/cache/key.js.map +1 -1
  12. package/dist/cache/redis-layer.d.ts +6 -0
  13. package/dist/cache/redis-layer.d.ts.map +1 -1
  14. package/dist/cache/redis-layer.js +150 -33
  15. package/dist/cache/redis-layer.js.map +1 -1
  16. package/dist/cache/server-runtime.d.ts +22 -9
  17. package/dist/cache/server-runtime.d.ts.map +1 -1
  18. package/dist/cache/server-runtime.js +190 -22
  19. package/dist/cache/server-runtime.js.map +1 -1
  20. package/dist/cache/tiered.d.ts +43 -2
  21. package/dist/cache/tiered.d.ts.map +1 -1
  22. package/dist/cache/tiered.js +457 -136
  23. package/dist/cache/tiered.js.map +1 -1
  24. package/dist/http/cache.d.ts +10 -1
  25. package/dist/http/cache.d.ts.map +1 -1
  26. package/dist/http/cache.js +105 -37
  27. package/dist/http/cache.js.map +1 -1
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/middleware/auth/oauth2-client-credentials.d.ts.map +1 -1
  33. package/dist/middleware/auth/oauth2-client-credentials.js +3 -0
  34. package/dist/middleware/auth/oauth2-client-credentials.js.map +1 -1
  35. package/dist/middleware/auth/oauth2.d.ts.map +1 -1
  36. package/dist/middleware/auth/oauth2.js +8 -0
  37. package/dist/middleware/auth/oauth2.js.map +1 -1
  38. package/dist/middleware/auth.d.ts +2 -0
  39. package/dist/middleware/auth.d.ts.map +1 -1
  40. package/dist/middleware/auth.js +62 -0
  41. package/dist/middleware/auth.js.map +1 -1
  42. package/dist/server/builder.d.ts.map +1 -1
  43. package/dist/server/builder.js +1 -0
  44. package/dist/server/builder.js.map +1 -1
  45. package/dist/server/fs-routes/middleware-processor.d.ts.map +1 -1
  46. package/dist/server/fs-routes/middleware-processor.js +15 -0
  47. package/dist/server/fs-routes/middleware-processor.js.map +1 -1
  48. package/dist/types/context.d.ts +2 -0
  49. package/dist/types/context.d.ts.map +1 -1
  50. package/dist/types/context.js +3 -0
  51. package/dist/types/context.js.map +1 -1
  52. package/dist/ui/cache/fs-layer.d.ts.map +1 -1
  53. package/dist/ui/cache/key.d.ts +28 -7
  54. package/dist/ui/cache/key.d.ts.map +1 -1
  55. package/dist/ui/cache/server-runtime.d.ts +22 -9
  56. package/dist/ui/cache/server-runtime.d.ts.map +1 -1
  57. package/dist/ui/cache/tiered.d.ts +43 -2
  58. package/dist/ui/cache/tiered.d.ts.map +1 -1
  59. package/dist/ui/server/fs-routes/middleware-processor.d.ts.map +1 -1
  60. package/dist/ui/types/context.d.ts +2 -0
  61. package/dist/ui/types/context.d.ts.map +1 -1
  62. 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
- const prefix = options.prefix ?? 'raffel:cache:';
6
- const fullKey = (key) => prefix + key;
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 expiresAt = Date.now() + ttlMs;
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 pattern = `${prefix}${namespace}:*`;
75
- if (!options.client.scan) {
76
- if (options.allowBlockingClear && options.client.keys) {
77
- const keys = await options.client.keys(pattern);
78
- if (keys.length > 0)
79
- await options.client.del(keys);
80
- return;
81
- }
82
- throw new Error('Redis/Valkey namespace invalidation requires SCAN support');
83
- }
84
- let cursor = '0';
85
- do {
86
- const result = options.clientStyle === 'node-redis' || options.client.pSetEx
87
- ? await options.client.scan(cursor, {
88
- MATCH: pattern,
89
- COUNT: options.scanCount ?? 100,
90
- })
91
- : await options.client.scan(cursor, 'MATCH', pattern, 'COUNT', options.scanCount ?? 100);
92
- const next = Array.isArray(result) ? result[0] : result.cursor;
93
- const keys = Array.isArray(result) ? result[1] : result.keys;
94
- cursor = String(next);
95
- if (keys.length > 0)
96
- await options.client.del(keys);
97
- } while (cursor !== '0');
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":"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"}
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 CacheIdentityScope } from './key.js';
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 interface RouteCacheConfig extends CacheWriteOptions {
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
- scope?: CacheIdentityScope;
8
- version?: string;
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?: Pick<RouteCacheConfig, 'scope' | 'version'>): string | undefined;
54
- invalidateProcedure(procedure: string, input: unknown, ctx: Context, options?: Pick<RouteCacheConfig, 'scope' | 'version'>): Promise<boolean>;
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>, config: RouteCacheConfig): Promise<unknown>;
68
- revalidate(key: string, next: () => Promise<unknown>, config: RouteCacheConfig): void;
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":"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"}
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 { procedureCacheKey, procedureCacheKeyFor } from './key.js';
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 createProcedureInterceptor(runtime, config) {
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 = procedureCacheKey(envelope, ctx, config);
123
+ const key = keyFor(envelope.payload, ctx);
55
124
  if (!key)
56
125
  return next();
57
- const hit = await runtime.cache.get(key);
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, config);
139
+ runtime.revalidate(key, next, writeOptions, cache, planId);
62
140
  return hit.value;
63
141
  }
64
- return runtime.executeOnce(key, next, config);
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 = { enabled: false };
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
- return resolved.enabled ? createProcedureInterceptor(this, resolved) : undefined;
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, config) {
112
- const existing = this.pending.get(key);
113
- if (existing)
114
- return existing.then(cloneResponse);
115
- const execution = next()
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
- await this.cache.set(key, value, config);
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(() => this.pending.delete(key));
122
- this.pending.set(key, execution);
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, config) {
126
- if (this.pending.has(key))
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
- this.current = createTieredCache({
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
  }