@rangojs/router 0.0.0-experimental.2

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 (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,522 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
+ import { createDocumentCacheMiddleware } from "../document-cache.js";
3
+ import type { MiddlewareContext } from "../../router/middleware.js";
4
+
5
+ // ============================================================================
6
+ // Mock Cache Store
7
+ // ============================================================================
8
+
9
+ interface MockCacheEntry {
10
+ response: Response;
11
+ staleAt: number;
12
+ }
13
+
14
+ function createMockCacheStore() {
15
+ const cache = new Map<string, MockCacheEntry>();
16
+
17
+ return {
18
+ cache,
19
+ async getResponse(key: string) {
20
+ const entry = cache.get(key);
21
+ if (!entry) return null;
22
+
23
+ const isStale = Date.now() > entry.staleAt;
24
+ return {
25
+ response: entry.response.clone(),
26
+ shouldRevalidate: isStale,
27
+ };
28
+ },
29
+ async putResponse(
30
+ key: string,
31
+ response: Response,
32
+ ttl: number,
33
+ _swr?: number,
34
+ ) {
35
+ cache.set(key, {
36
+ response: response.clone(),
37
+ staleAt: Date.now() + ttl * 1000,
38
+ });
39
+ },
40
+ };
41
+ }
42
+
43
+ // ============================================================================
44
+ // Mock Request Context
45
+ // ============================================================================
46
+
47
+ function createMockRequestContext(store: ReturnType<typeof createMockCacheStore>) {
48
+ return {
49
+ _cacheStore: store,
50
+ _onResponseCallbacks: [] as Array<(r: Response) => Response>,
51
+ waitUntil: vi.fn((fn: () => Promise<void>) => {
52
+ fn().catch(() => {});
53
+ }),
54
+ };
55
+ }
56
+
57
+ // ============================================================================
58
+ // Mock Middleware Context
59
+ // ============================================================================
60
+
61
+ function createMockMiddlewareContext(
62
+ url: string,
63
+ options: { method?: string; headers?: Record<string, string> } = {},
64
+ ): MiddlewareContext<any> {
65
+ const parsedUrl = new URL(url, "http://localhost");
66
+ const request = new Request(parsedUrl.toString(), {
67
+ method: options.method ?? "GET",
68
+ headers: options.headers,
69
+ });
70
+
71
+ return {
72
+ request,
73
+ url: parsedUrl,
74
+ env: {},
75
+ var: {},
76
+ get: vi.fn(),
77
+ set: vi.fn(),
78
+ } as unknown as MiddlewareContext<any>;
79
+ }
80
+
81
+ // ============================================================================
82
+ // Tests
83
+ // ============================================================================
84
+
85
+ describe("createDocumentCacheMiddleware", () => {
86
+ let mockStore: ReturnType<typeof createMockCacheStore>;
87
+ let mockRequestCtx: ReturnType<typeof createMockRequestContext>;
88
+
89
+ beforeEach(() => {
90
+ vi.useFakeTimers();
91
+ vi.setSystemTime(new Date("2024-01-01T00:00:00Z"));
92
+
93
+ mockStore = createMockCacheStore();
94
+ mockRequestCtx = createMockRequestContext(mockStore);
95
+
96
+ // Mock getRequestContext to return our mock
97
+ vi.doMock("../../server/request-context.js", () => ({
98
+ getRequestContext: () => mockRequestCtx,
99
+ }));
100
+ });
101
+
102
+ afterEach(() => {
103
+ vi.useRealTimers();
104
+ vi.restoreAllMocks();
105
+ });
106
+
107
+ describe("cache miss", () => {
108
+ it("should pass through and cache response with s-maxage", async () => {
109
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
110
+
111
+ const middleware = createDocumentCacheMiddleware();
112
+ const ctx = createMockMiddlewareContext("http://localhost/page");
113
+
114
+ const responseBody = "Hello World";
115
+ const next = vi.fn().mockResolvedValue(
116
+ new Response(responseBody, {
117
+ headers: { "Cache-Control": "s-maxage=60" },
118
+ }),
119
+ );
120
+
121
+ // Mock getRequestContext inline
122
+ const originalModule = await import("../../server/request-context.js");
123
+ vi.spyOn(originalModule, "getRequestContext").mockReturnValue(
124
+ mockRequestCtx as any,
125
+ );
126
+
127
+ const response = await middleware(ctx, next);
128
+
129
+ expect(next).toHaveBeenCalledTimes(1);
130
+ expect(response.headers.get("x-document-cache-status")).toBe("MISS");
131
+ expect(await response.text()).toBe(responseBody);
132
+
133
+ // Wait for background cache write
134
+ await vi.runAllTimersAsync();
135
+
136
+ // Verify cached
137
+ expect(mockStore.cache.has("/page:html")).toBe(true);
138
+ });
139
+
140
+ it("should not cache response without s-maxage", async () => {
141
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
142
+
143
+ const middleware = createDocumentCacheMiddleware();
144
+ const ctx = createMockMiddlewareContext("http://localhost/page");
145
+
146
+ const next = vi.fn().mockResolvedValue(
147
+ new Response("No cache", {
148
+ headers: { "Cache-Control": "private" },
149
+ }),
150
+ );
151
+
152
+ const originalModule = await import("../../server/request-context.js");
153
+ vi.spyOn(originalModule, "getRequestContext").mockReturnValue(
154
+ mockRequestCtx as any,
155
+ );
156
+
157
+ const response = await middleware(ctx, next);
158
+
159
+ expect(next).toHaveBeenCalledTimes(1);
160
+ expect(response.headers.has("x-document-cache-status")).toBe(false);
161
+ expect(mockStore.cache.size).toBe(0);
162
+ });
163
+ });
164
+
165
+ describe("cache hit", () => {
166
+ it("should return cached response on second request", async () => {
167
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
168
+
169
+ // Pre-populate cache
170
+ const cachedResponse = new Response("Cached content", {
171
+ headers: { "Cache-Control": "s-maxage=60" },
172
+ });
173
+ mockStore.cache.set("/page:html", {
174
+ response: cachedResponse,
175
+ staleAt: Date.now() + 60 * 1000,
176
+ });
177
+
178
+ const middleware = createDocumentCacheMiddleware();
179
+ const ctx = createMockMiddlewareContext("http://localhost/page");
180
+ const next = vi.fn();
181
+
182
+ const originalModule = await import("../../server/request-context.js");
183
+ vi.spyOn(originalModule, "getRequestContext").mockReturnValue(
184
+ mockRequestCtx as any,
185
+ );
186
+
187
+ const response = await middleware(ctx, next);
188
+
189
+ expect(next).not.toHaveBeenCalled();
190
+ expect(response.headers.get("x-document-cache-status")).toBe("HIT");
191
+ expect(await response.text()).toBe("Cached content");
192
+ });
193
+ });
194
+
195
+ describe("stale-while-revalidate", () => {
196
+ it("should return stale response and revalidate in background", async () => {
197
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
198
+
199
+ // Pre-populate cache with stale entry
200
+ const staleResponse = new Response("Stale content", {
201
+ headers: { "Cache-Control": "s-maxage=60, stale-while-revalidate=300" },
202
+ });
203
+ mockStore.cache.set("/page:html", {
204
+ response: staleResponse,
205
+ staleAt: Date.now() - 1000, // Already stale
206
+ });
207
+
208
+ const middleware = createDocumentCacheMiddleware();
209
+ const ctx = createMockMiddlewareContext("http://localhost/page");
210
+
211
+ const freshResponse = new Response("Fresh content", {
212
+ headers: { "Cache-Control": "s-maxage=60, stale-while-revalidate=300" },
213
+ });
214
+ const next = vi.fn().mockResolvedValue(freshResponse);
215
+
216
+ const originalModule = await import("../../server/request-context.js");
217
+ vi.spyOn(originalModule, "getRequestContext").mockReturnValue(
218
+ mockRequestCtx as any,
219
+ );
220
+
221
+ const response = await middleware(ctx, next);
222
+
223
+ // Should return stale content immediately
224
+ expect(response.headers.get("x-document-cache-status")).toBe("STALE");
225
+ expect(await response.text()).toBe("Stale content");
226
+
227
+ // Background revalidation should be scheduled
228
+ expect(mockRequestCtx.waitUntil).toHaveBeenCalledTimes(1);
229
+
230
+ // Execute background task
231
+ await vi.runAllTimersAsync();
232
+
233
+ // next() should have been called for revalidation
234
+ expect(next).toHaveBeenCalledTimes(1);
235
+ });
236
+ });
237
+
238
+ describe("skip conditions", () => {
239
+ it("should skip RSC action requests", async () => {
240
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
241
+
242
+ const middleware = createDocumentCacheMiddleware();
243
+ const ctx = createMockMiddlewareContext(
244
+ "http://localhost/page?_rsc_action=true",
245
+ );
246
+
247
+ const next = vi.fn().mockResolvedValue(new Response("Action response"));
248
+
249
+ const response = await middleware(ctx, next);
250
+
251
+ expect(next).toHaveBeenCalledTimes(1);
252
+ expect(response.headers.has("x-document-cache-status")).toBe(false);
253
+ });
254
+
255
+ it("should skip loader requests", async () => {
256
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
257
+
258
+ const middleware = createDocumentCacheMiddleware();
259
+ const ctx = createMockMiddlewareContext(
260
+ "http://localhost/page?_rsc_loader=myLoader",
261
+ );
262
+
263
+ const next = vi.fn().mockResolvedValue(new Response("Loader response"));
264
+
265
+ const response = await middleware(ctx, next);
266
+
267
+ expect(next).toHaveBeenCalledTimes(1);
268
+ expect(response.headers.has("x-document-cache-status")).toBe(false);
269
+ });
270
+
271
+ it("should skip configured paths", async () => {
272
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
273
+
274
+ const middleware = createDocumentCacheMiddleware({
275
+ skipPaths: ["/api", "/admin"],
276
+ });
277
+ const ctx = createMockMiddlewareContext("http://localhost/api/users");
278
+
279
+ const next = vi.fn().mockResolvedValue(new Response("API response"));
280
+
281
+ const response = await middleware(ctx, next);
282
+
283
+ expect(next).toHaveBeenCalledTimes(1);
284
+ expect(response.headers.has("x-document-cache-status")).toBe(false);
285
+ });
286
+
287
+ it("should skip when isEnabled returns false", async () => {
288
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
289
+
290
+ const middleware = createDocumentCacheMiddleware({
291
+ isEnabled: (ctx) => !ctx.request.headers.has("x-no-cache"),
292
+ });
293
+ const ctx = createMockMiddlewareContext("http://localhost/page", {
294
+ headers: { "x-no-cache": "true" },
295
+ });
296
+
297
+ const next = vi.fn().mockResolvedValue(new Response("Response"));
298
+
299
+ const response = await middleware(ctx, next);
300
+
301
+ expect(next).toHaveBeenCalledTimes(1);
302
+ expect(response.headers.has("x-document-cache-status")).toBe(false);
303
+ });
304
+ });
305
+
306
+ describe("cache key generation", () => {
307
+ it("should use custom key generator", async () => {
308
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
309
+
310
+ const middleware = createDocumentCacheMiddleware({
311
+ keyGenerator: (url) => `custom:${url.pathname}`,
312
+ });
313
+
314
+ // Pre-populate cache with custom key
315
+ const cachedResponse = new Response("Cached", {
316
+ headers: { "Cache-Control": "s-maxage=60" },
317
+ });
318
+ mockStore.cache.set("custom:/page:html", {
319
+ response: cachedResponse,
320
+ staleAt: Date.now() + 60 * 1000,
321
+ });
322
+
323
+ const ctx = createMockMiddlewareContext("http://localhost/page");
324
+ const next = vi.fn();
325
+
326
+ const originalModule = await import("../../server/request-context.js");
327
+ vi.spyOn(originalModule, "getRequestContext").mockReturnValue(
328
+ mockRequestCtx as any,
329
+ );
330
+
331
+ const response = await middleware(ctx, next);
332
+
333
+ expect(next).not.toHaveBeenCalled();
334
+ expect(response.headers.get("x-document-cache-status")).toBe("HIT");
335
+ });
336
+
337
+ it("should differentiate RSC partial requests with :rsc suffix", async () => {
338
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
339
+
340
+ // Pre-populate HTML cache
341
+ const htmlResponse = new Response("HTML", {
342
+ headers: { "Cache-Control": "s-maxage=60" },
343
+ });
344
+ mockStore.cache.set("/page:html", {
345
+ response: htmlResponse,
346
+ staleAt: Date.now() + 60 * 1000,
347
+ });
348
+
349
+ const middleware = createDocumentCacheMiddleware();
350
+
351
+ // RSC partial request should miss (different key)
352
+ const ctx = createMockMiddlewareContext(
353
+ "http://localhost/page?_rsc_partial=true",
354
+ );
355
+ const next = vi.fn().mockResolvedValue(
356
+ new Response("RSC", {
357
+ headers: { "Cache-Control": "s-maxage=60" },
358
+ }),
359
+ );
360
+
361
+ const originalModule = await import("../../server/request-context.js");
362
+ vi.spyOn(originalModule, "getRequestContext").mockReturnValue(
363
+ mockRequestCtx as any,
364
+ );
365
+
366
+ const response = await middleware(ctx, next);
367
+
368
+ // Should be a MISS since RSC key is different
369
+ expect(next).toHaveBeenCalledTimes(1);
370
+ expect(response.headers.get("x-document-cache-status")).toBe("MISS");
371
+ });
372
+
373
+ it("should include segment hash in cache key for partial requests", async () => {
374
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
375
+
376
+ const middleware = createDocumentCacheMiddleware();
377
+
378
+ // First partial request with segments A,B
379
+ const ctx1 = createMockMiddlewareContext(
380
+ "http://localhost/page?_rsc_partial=true&_rsc_segments=root,blog-layout",
381
+ );
382
+ const next1 = vi.fn().mockResolvedValue(
383
+ new Response("Response for blog navigation", {
384
+ headers: { "Cache-Control": "s-maxage=60" },
385
+ }),
386
+ );
387
+
388
+ const originalModule = await import("../../server/request-context.js");
389
+ vi.spyOn(originalModule, "getRequestContext").mockReturnValue(
390
+ mockRequestCtx as any,
391
+ );
392
+
393
+ await middleware(ctx1, next1);
394
+ await vi.runAllTimersAsync();
395
+
396
+ // Second partial request with different segments
397
+ const ctx2 = createMockMiddlewareContext(
398
+ "http://localhost/page?_rsc_partial=true&_rsc_segments=root,shop-layout",
399
+ );
400
+ const next2 = vi.fn().mockResolvedValue(
401
+ new Response("Response for shop navigation", {
402
+ headers: { "Cache-Control": "s-maxage=60" },
403
+ }),
404
+ );
405
+
406
+ const response2 = await middleware(ctx2, next2);
407
+
408
+ // Should be a MISS because different segments = different cache key
409
+ expect(next2).toHaveBeenCalledTimes(1);
410
+ expect(response2.headers.get("x-document-cache-status")).toBe("MISS");
411
+ });
412
+ });
413
+
414
+ describe("debug logging", () => {
415
+ it("should not log when debug is false", async () => {
416
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
417
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
418
+
419
+ const middleware = createDocumentCacheMiddleware({ debug: false });
420
+ const ctx = createMockMiddlewareContext("http://localhost/page");
421
+
422
+ const next = vi.fn().mockResolvedValue(
423
+ new Response("Response", {
424
+ headers: { "Cache-Control": "s-maxage=60" },
425
+ }),
426
+ );
427
+
428
+ const originalModule = await import("../../server/request-context.js");
429
+ vi.spyOn(originalModule, "getRequestContext").mockReturnValue(
430
+ mockRequestCtx as any,
431
+ );
432
+
433
+ await middleware(ctx, next);
434
+
435
+ expect(consoleSpy).not.toHaveBeenCalled();
436
+ consoleSpy.mockRestore();
437
+ });
438
+
439
+ it("should log when debug is true", async () => {
440
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
441
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
442
+
443
+ const middleware = createDocumentCacheMiddleware({ debug: true });
444
+ const ctx = createMockMiddlewareContext("http://localhost/page");
445
+
446
+ const next = vi.fn().mockResolvedValue(
447
+ new Response("Response", {
448
+ headers: { "Cache-Control": "s-maxage=60" },
449
+ }),
450
+ );
451
+
452
+ const originalModule = await import("../../server/request-context.js");
453
+ vi.spyOn(originalModule, "getRequestContext").mockReturnValue(
454
+ mockRequestCtx as any,
455
+ );
456
+
457
+ await middleware(ctx, next);
458
+
459
+ expect(consoleSpy).toHaveBeenCalledWith(
460
+ expect.stringContaining("[DocumentCache] MISS"),
461
+ );
462
+ consoleSpy.mockRestore();
463
+ });
464
+ });
465
+
466
+ describe("error handling", () => {
467
+ it("should fall through to handler on cache error", async () => {
468
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
469
+
470
+ // Create a store that throws on getResponse
471
+ const brokenStore = {
472
+ async getResponse() {
473
+ throw new Error("Cache unavailable");
474
+ },
475
+ async putResponse() {},
476
+ };
477
+ const brokenCtx = createMockRequestContext(brokenStore as any);
478
+
479
+ const middleware = createDocumentCacheMiddleware();
480
+ const ctx = createMockMiddlewareContext("http://localhost/page");
481
+
482
+ const next = vi.fn().mockResolvedValue(new Response("Fallback"));
483
+
484
+ const originalModule = await import("../../server/request-context.js");
485
+ vi.spyOn(originalModule, "getRequestContext").mockReturnValue(
486
+ brokenCtx as any,
487
+ );
488
+
489
+ const response = await middleware(ctx, next);
490
+
491
+ expect(next).toHaveBeenCalledTimes(1);
492
+ expect(await response.text()).toBe("Fallback");
493
+ });
494
+ });
495
+
496
+ describe("no cache store", () => {
497
+ it("should pass through when no cache store is configured", async () => {
498
+ const { createDocumentCacheMiddleware } = await import("../document-cache.js");
499
+
500
+ const noCacheCtx = {
501
+ _cacheStore: undefined,
502
+ _onResponseCallbacks: [],
503
+ waitUntil: vi.fn(),
504
+ };
505
+
506
+ const middleware = createDocumentCacheMiddleware();
507
+ const ctx = createMockMiddlewareContext("http://localhost/page");
508
+
509
+ const next = vi.fn().mockResolvedValue(new Response("Response"));
510
+
511
+ const originalModule = await import("../../server/request-context.js");
512
+ vi.spyOn(originalModule, "getRequestContext").mockReturnValue(
513
+ noCacheCtx as any,
514
+ );
515
+
516
+ const response = await middleware(ctx, next);
517
+
518
+ expect(next).toHaveBeenCalledTimes(1);
519
+ expect(response.headers.has("x-document-cache-status")).toBe(false);
520
+ });
521
+ });
522
+ });