Skip to content

Commit 4e51487

Browse files
committed
Add debug logging
1 parent 2a337c4 commit 4e51487

File tree

15 files changed

+325
-22
lines changed

15 files changed

+325
-22
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,4 @@ dist
130130
.pnp.*
131131
.tsup
132132
.DS_Store
133+
.wrangler

demos/cache-handlers/astro.config.mjs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@ export default defineConfig({
1010
enabled: true,
1111
persist: true,
1212
},
13+
workerEntryPoint: { path: "src/worker.ts" },
1314
}),
14-
vite: {
15-
build: {
16-
minify: false, // Better error messages during development
17-
},
18-
},
1915
});

demos/cache-handlers/src/pages/api/invalidate.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,20 @@ export async function POST({ request }: { request: Request }) {
1313
if (!value) {
1414
return Response.json({ success: false, error: "Tag value is required" }, { status: 400 });
1515
}
16-
const tagCount = await invalidateByTag(value);
16+
const tagCount = await invalidateByTag(value, { debug: true });
1717
result = { success: true, count: tagCount };
1818
break;
1919

2020
case "path":
2121
if (!value) {
2222
return Response.json({ success: false, error: "Path value is required" }, { status: 400 });
2323
}
24-
const pathCount = await invalidateByPath(value);
24+
const pathCount = await invalidateByPath(value, { debug: true });
2525
result = { success: true, count: pathCount };
2626
break;
2727

2828
case "all":
29-
const allCount = await invalidateAll();
29+
const allCount = await invalidateAll({ debug: true });
3030
result = { success: true, count: allCount };
3131
break;
3232

@@ -46,7 +46,7 @@ export async function POST({ request }: { request: Request }) {
4646

4747
export async function GET() {
4848
try {
49-
const stats = await getCacheStats();
49+
const stats = await getCacheStats({ debug: true });
5050
return Response.json({
5151
success: true,
5252
stats,

demos/cache-handlers/src/worker.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,25 @@ export function createExports(manifest: SSRManifest) {
3535
conditionalRequests: { etag: "generate" },
3636
cacheStatusHeader: "demo-cache",
3737
},
38+
debug: {
39+
enabled: true,
40+
logLevel: "verbose",
41+
},
3842
});
3943

4044
if (url.pathname.endsWith("/blocking")) {
4145
return cacheHandle(request, {
4246
swr: "blocking",
43-
runInBackground: ctx.waitUntil,
47+
runInBackground: ctx.waitUntil.bind(ctx),
4448
});
4549
}
4650
if (url.pathname.endsWith("/off")) {
4751
return cacheHandle(request, {
4852
swr: "off",
49-
runInBackground: ctx.waitUntil,
53+
runInBackground: ctx.waitUntil.bind(ctx),
5054
});
5155
}
52-
return cacheHandle(request, { runInBackground: ctx.waitUntil });
56+
return cacheHandle(request, { runInBackground: ctx.waitUntil.bind(ctx) });
5357
},
5458
},
5559
};

packages/cache-handlers/README.md

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ const handle = createCacheHandler({
100100
export default {
101101
async fetch(request, env, ctx) {
102102
return handle(request, {
103-
runInBackground: ctx.waitUntil,
103+
runInBackground: ctx.waitUntil.bind(ctx),
104104
});
105105
},
106106
};
@@ -133,7 +133,7 @@ import handler from "./handler.js";
133133
addEventListener("fetch", (event) => {
134134
const handle = createCacheHandler({
135135
handler: handleRequest,
136-
runInBackground: event.waitUntil,
136+
runInBackground: event.waitUntil.bind(event),
137137
});
138138
event.respondWith(handle(event.request));
139139
});
@@ -284,13 +284,97 @@ import type {
284284
} from "cache-handlers";
285285
```
286286

287+
## Important Caveats & behaviour
288+
289+
### Race Conditions
290+
291+
**Concurrent Cache Writes**: Multiple simultaneous requests for the same resource may result in duplicate cache writes. The last write wins, but all requests will complete successfully. This is generally harmless but may cause temporary inconsistency during high concurrency.
292+
293+
**Background Revalidation**: When using `stale-while-revalidate`, multiple concurrent requests during the SWR window will each trigger their own background revalidation. The library does not deduplicate these - each will run independently. Consider using request deduplication at the application level if this is a concern.
294+
295+
**Invalidation During Revalidation**: Cache invalidation operations may occur while background revalidation is in progress. The invalidation will complete immediately, but in-flight revalidations may still write back to the cache, potentially restoring stale data.
296+
297+
### Platform-Specific CacheStorage behaviour
298+
299+
Different platforms implement the Web Standard `CacheStorage` API with varying capabilities and limitations:
300+
301+
#### Cloudflare Workers
302+
```ts
303+
// ✅ Full support for all features
304+
// ✅ Persistent across requests within the same data center
305+
// ✅ Automatic geographic distribution
306+
// ⚠️ Cache keys limited to ~8KB total URL length
307+
// ⚠️ Cache entries expire after ~1 hour of inactivity
308+
```
309+
310+
#### Deno Deploy
311+
```ts
312+
// ✅ Full CacheStorage support
313+
// ✅ Persistent across deployments in same region
314+
// ⚠️ Regional caches - not globally distributed
315+
// ⚠️ Cache may not persist during deployment updates
316+
```
317+
318+
#### Node.js (with undici polyfill)
319+
```ts
320+
// ✅ Works via undici polyfill
321+
// ⚠️ In-memory only by default - not persistent across restarts
322+
// ⚠️ Limited to single process - no cross-process sharing
323+
// 💡 Consider using Redis or similar for production Node.js deployments
324+
```
325+
326+
#### Netlify Edge Functions
327+
```ts
328+
// ✅ CacheStorage available
329+
// ⚠️ Cache is per-edge location, not globally consistent
330+
// ⚠️ Cache may be cleared during deployments
331+
```
332+
333+
#### Vercel Edge Runtime
334+
```ts
335+
// ❌ CacheStorage not available
336+
// 💡 Use Vercel's built-in caching mechanisms instead
337+
```
338+
339+
### Memory and Performance Considerations
340+
341+
**Large Responses**: Caching large responses (>1MB) may impact performance and memory usage. Consider streaming or chunked responses for large payloads.
342+
343+
**Cache Key Generation**: Complex cache key generation (e.g., with many vary parameters) can impact performance. Keep cache keys simple when possible.
344+
345+
**Metadata Overhead**: Cache tag metadata is stored separately and may grow large with many tagged entries. Monitor cache statistics and clean up unused tags periodically.
346+
347+
### Debugging and Monitoring
348+
349+
Enable debug logging to understand cache behaviour:
350+
351+
```ts
352+
createCacheHandler({
353+
debug: {
354+
enabled: true,
355+
logLevel: 'verbose' // Use 'basic' in production
356+
}
357+
});
358+
```
359+
360+
Monitor cache statistics:
361+
362+
```ts
363+
import { getCacheStats } from 'cache-handlers';
364+
const stats = await getCacheStats();
365+
console.log(`Cache: ${stats.totalEntries} entries, ${Object.keys(stats.entriesByTag).length} tags`);
366+
```
367+
287368
## Best Practices
288369

289370
1. Always bound TTLs with `maxTtl`.
290371
2. Use `stale-while-revalidate` for latency-sensitive endpoints.
291372
3. Include cache tags for selective purge (`cache-tag: user:123, list:users`).
292373
4. Generate or preserve ETags to leverage client 304s.
293374
5. Keep cache keys stable & explicit if customizing via `getCacheKey`.
375+
6. Test cache behaviour thoroughly across your target platforms.
376+
7. Monitor cache hit rates and adjust TTLs based on usage patterns.
377+
8. Use debug logging during development to understand cache behaviour.
294378

295379
## License
296380

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { DebugConfig } from "./types.ts";
2+
3+
/**
4+
* Debug logger utility for cache operations
5+
*/
6+
export class DebugLogger {
7+
private config: DebugConfig;
8+
9+
constructor(config: DebugConfig = {}) {
10+
this.config = {
11+
enabled: false,
12+
logger: console.log,
13+
logLevel: 'basic',
14+
...config,
15+
};
16+
}
17+
18+
/**
19+
* Check if debug logging is enabled
20+
*/
21+
get isEnabled(): boolean {
22+
return this.config.enabled === true;
23+
}
24+
25+
/**
26+
* Check if verbose logging is enabled
27+
*/
28+
get isVerbose(): boolean {
29+
return this.isEnabled && this.config.logLevel === 'verbose';
30+
}
31+
32+
/**
33+
* Log a basic debug message
34+
*/
35+
log(operation: string, message: string, ...args: unknown[]): void {
36+
if (!this.isEnabled) return;
37+
38+
const timestamp = new Date().toISOString();
39+
const prefix = `[${timestamp}] [cache-handlers:${operation}]`;
40+
this.config.logger!(`${prefix} ${message}`, ...args);
41+
}
42+
43+
/**
44+
* Log a verbose debug message (only if verbose mode is enabled)
45+
*/
46+
verbose(operation: string, message: string, ...args: unknown[]): void {
47+
if (!this.isVerbose) return;
48+
this.log(operation, message, ...args);
49+
}
50+
51+
/**
52+
* Log cache read operation
53+
*/
54+
logCacheRead(url: string, result: 'hit' | 'miss' | 'stale', metadata?: unknown): void {
55+
this.log('read', `Cache ${result} for ${url}`);
56+
if (this.isVerbose && metadata) {
57+
this.verbose('read', 'Cache metadata:', metadata);
58+
}
59+
}
60+
61+
/**
62+
* Log cache write operation
63+
*/
64+
logCacheWrite(url: string, ttl?: number, tags?: string[]): void {
65+
this.log('write', `Writing to cache: ${url}${ttl ? ` (TTL: ${ttl}s)` : ''}`);
66+
if (this.isVerbose && tags?.length) {
67+
this.verbose('write', `Cache tags: ${tags.join(', ')}`);
68+
}
69+
}
70+
71+
/**
72+
* Log cache invalidation operation
73+
*/
74+
logInvalidation(type: 'tag' | 'path' | 'all', value: string, count: number): void {
75+
this.log('invalidation', `Invalidated ${count} entries by ${type}: ${value}`);
76+
}
77+
78+
/**
79+
* Log conditional request operation
80+
*/
81+
logConditionalRequest(url: string, type: 'etag' | 'last-modified', result: '304' | 'fresh'): void {
82+
this.log('conditional', `${type} check for ${url}: ${result}`);
83+
}
84+
85+
/**
86+
* Log background revalidation
87+
*/
88+
logBackgroundRevalidation(url: string, triggered: boolean): void {
89+
const status = triggered ? 'triggered' : 'skipped';
90+
this.log('background', `Background revalidation ${status} for ${url}`);
91+
}
92+
93+
/**
94+
* Log error
95+
*/
96+
logError(operation: string, error: Error, context?: string): void {
97+
this.log('error', `Error in ${operation}${context ? ` (${context})` : ''}: ${error.message}`);
98+
if (this.isVerbose) {
99+
this.verbose('error', 'Stack trace:', error.stack);
100+
}
101+
}
102+
}
103+
104+
/**
105+
* Create a debug logger instance from config
106+
*/
107+
export function createDebugLogger(config?: boolean | DebugConfig): DebugLogger {
108+
if (typeof config === 'boolean') {
109+
return new DebugLogger({ enabled: config });
110+
}
111+
return new DebugLogger(config);
112+
}

packages/cache-handlers/src/handlers.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
} from "./types.ts";
99
import { readFromCache } from "./read.ts";
1010
import { writeToCache } from "./write.ts";
11+
import { createDebugLogger } from "./debug.ts";
1112

1213
export function createCacheHandler<
1314
TRequest extends MinimalRequest = Request,
@@ -17,13 +18,15 @@ export function createCacheHandler<
1718
): CacheHandle<TRequest, TResponse> {
1819
const baseHandler: HandlerFunction<TRequest, TResponse> | undefined =
1920
options.handler;
21+
const debug = createDebugLogger(options.debug);
2022

2123
const handle: CacheHandle<TRequest, TResponse> = async (
2224
request,
2325
callOpts = {},
2426
): Promise<TResponse> => {
2527
// Only cache GET
2628
if (request.method !== "GET") {
29+
debug.log('handler', `Non-GET request (${request.method}), bypassing cache: ${request.url}`);
2730
const handler = callOpts.handler || baseHandler;
2831
if (!handler) {
2932
return new Response("No handler provided", {
@@ -37,6 +40,12 @@ export function createCacheHandler<
3740
request,
3841
options,
3942
);
43+
44+
if (cached) {
45+
debug.logCacheRead(request.url, needsBackgroundRevalidation ? 'stale' : 'hit');
46+
} else {
47+
debug.logCacheRead(request.url, 'miss');
48+
}
4049
const statusSetting = options.features?.cacheStatusHeader;
4150
const enableStatus = !!statusSetting;
4251
const cacheStatusName =
@@ -50,12 +59,14 @@ export function createCacheHandler<
5059
const handler = baseHandler || callOpts.handler;
5160
if (handler) {
5261
try {
62+
debug.log('handler', `Blocking revalidation for ${request.url}`);
5363
const fresh = await handler(request, {
5464
mode: "stale",
5565
background: false,
5666
});
5767
return await writeToCache(request, fresh, options);
5868
} catch (err) {
69+
debug.logError('handler', err as Error, 'SWR blocking revalidation');
5970
console.warn(
6071
"SWR blocking revalidation failed; serving stale",
6172
err,
@@ -65,16 +76,20 @@ export function createCacheHandler<
6576
} else if (policy === "background") {
6677
const handler = baseHandler || callOpts.handler;
6778
if (handler) {
79+
debug.logBackgroundRevalidation(request.url, true);
6880
const scheduler = callOpts.runInBackground ||
6981
options.runInBackground;
7082
const revalidatePromise = (async () => {
7183
try {
84+
debug.log('handler', `Background revalidation starting for ${request.url}`);
7285
const response = await handler(request, {
7386
mode: "stale",
7487
background: true,
7588
});
7689
await writeToCache(request, response, options);
90+
debug.log('handler', `Background revalidation completed for ${request.url}`);
7791
} catch (err) {
92+
debug.logError('handler', err as Error, 'SWR background revalidation');
7893
console.warn("SWR background revalidation failed", err);
7994
}
8095
})();
@@ -86,6 +101,7 @@ export function createCacheHandler<
86101
}
87102
} else if (policy === "off") {
88103
// Treat stale-while-revalidate as disabled: delete and proceed as miss
104+
debug.log('handler', `SWR disabled, deleting stale entry for ${request.url}`);
89105
try {
90106
await caches.open(options.cacheName || "cache-primitives-default")
91107
.then((c) => c.delete(request as unknown as Request));
@@ -143,6 +159,7 @@ export function createCacheHandler<
143159
}
144160

145161
// Cache miss
162+
debug.log('handler', `Cache miss, calling handler for ${request.url}`);
146163
const handler = callOpts.handler || baseHandler;
147164
if (!handler) {
148165
return new Response("Cache miss and no handler provided", {

0 commit comments

Comments
 (0)