@ivogt/rsc-router 0.0.0-experimental.1
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/README.md +19 -0
- package/package.json +131 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +891 -0
- package/src/browser/navigation-client.ts +155 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +545 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +228 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +149 -0
- package/src/browser/rsc-router.tsx +310 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +443 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
- package/src/cache/cf/cf-cache-store.ts +274 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/index.ts +52 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +366 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +609 -0
- package/src/components/DefaultDocument.tsx +20 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +178 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href.ts +139 -0
- package/src/index.rsc.ts +69 -0
- package/src/index.ts +84 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1333 -0
- package/src/route-map-builder.ts +140 -0
- package/src/route-types.ts +148 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +60 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +116 -0
- package/src/router/match-context.ts +261 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +250 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +212 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +271 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3484 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +942 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +225 -0
- package/src/segment-system.tsx +405 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +340 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +470 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +126 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +215 -0
- package/src/types.ts +1473 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +608 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Lookup Middleware
|
|
3
|
+
*
|
|
4
|
+
* First middleware in the pipeline. Checks cache before segment resolution.
|
|
5
|
+
*
|
|
6
|
+
* FLOW DIAGRAM
|
|
7
|
+
* ============
|
|
8
|
+
*
|
|
9
|
+
* source (empty)
|
|
10
|
+
* |
|
|
11
|
+
* v
|
|
12
|
+
* +---------------------+
|
|
13
|
+
* | Is action request? |──yes──> yield* source (pass through)
|
|
14
|
+
* +---------------------+
|
|
15
|
+
* | no
|
|
16
|
+
* v
|
|
17
|
+
* +---------------------+
|
|
18
|
+
* | Cache enabled? |──no───> yield* source (pass through)
|
|
19
|
+
* +---------------------+
|
|
20
|
+
* | yes
|
|
21
|
+
* v
|
|
22
|
+
* +---------------------+
|
|
23
|
+
* | Lookup cache |
|
|
24
|
+
* | (pathname, params) |
|
|
25
|
+
* +---------------------+
|
|
26
|
+
* |
|
|
27
|
+
* +-----+-----+
|
|
28
|
+
* | |
|
|
29
|
+
* miss hit
|
|
30
|
+
* | |
|
|
31
|
+
* v v
|
|
32
|
+
* yield* Set state.cacheHit = true
|
|
33
|
+
* source Set state.shouldRevalidate
|
|
34
|
+
* | |
|
|
35
|
+
* | v
|
|
36
|
+
* | +---------------------------+
|
|
37
|
+
* | | For each cached segment: |
|
|
38
|
+
* | | - Apply revalidation |
|
|
39
|
+
* | | - Set component = null |
|
|
40
|
+
* | | if client has it |
|
|
41
|
+
* | +---------------------------+
|
|
42
|
+
* | |
|
|
43
|
+
* | v
|
|
44
|
+
* | +---------------------------+
|
|
45
|
+
* | | Resolve fresh loaders | <-- Loaders are NEVER cached
|
|
46
|
+
* | | (always fresh data) |
|
|
47
|
+
* | +---------------------------+
|
|
48
|
+
* | |
|
|
49
|
+
* | v
|
|
50
|
+
* | yield cached segments
|
|
51
|
+
* | yield fresh loader segments
|
|
52
|
+
* | |
|
|
53
|
+
* +-----------+
|
|
54
|
+
* |
|
|
55
|
+
* v
|
|
56
|
+
* next middleware
|
|
57
|
+
*
|
|
58
|
+
*
|
|
59
|
+
* CACHE BEHAVIOR
|
|
60
|
+
* ==============
|
|
61
|
+
*
|
|
62
|
+
* Cache HIT:
|
|
63
|
+
* - state.cacheHit = true signals downstream middleware to skip
|
|
64
|
+
* - Cached segments have their components nullified if client already has them
|
|
65
|
+
* - Loaders are always re-resolved for fresh data
|
|
66
|
+
* - state.shouldRevalidate triggers background SWR if cache was stale
|
|
67
|
+
*
|
|
68
|
+
* Cache MISS:
|
|
69
|
+
* - Passes through to segment-resolution middleware
|
|
70
|
+
* - No segments yielded from this middleware
|
|
71
|
+
*
|
|
72
|
+
* Loaders:
|
|
73
|
+
* - NEVER cached by design
|
|
74
|
+
* - Always resolved fresh on every request
|
|
75
|
+
* - Ensures data freshness even with cached UI components
|
|
76
|
+
*
|
|
77
|
+
*
|
|
78
|
+
* REVALIDATION RULES
|
|
79
|
+
* ==================
|
|
80
|
+
*
|
|
81
|
+
* Each cached segment is evaluated against its revalidation rules:
|
|
82
|
+
*
|
|
83
|
+
* 1. No rules defined -> use default (skip if client has segment)
|
|
84
|
+
* 2. Rules return false -> skip re-render (nullify component)
|
|
85
|
+
* 3. Rules return true -> re-render (keep component)
|
|
86
|
+
*
|
|
87
|
+
* Revalidation context includes:
|
|
88
|
+
* - Previous/next URL and params
|
|
89
|
+
* - Request object
|
|
90
|
+
* - Action context (if POST)
|
|
91
|
+
*/
|
|
92
|
+
import type { ResolvedSegment } from "../../types.js";
|
|
93
|
+
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
94
|
+
import { getRouterContext } from "../router-context.js";
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Async generator middleware type
|
|
98
|
+
*/
|
|
99
|
+
export type GeneratorMiddleware<T> = (
|
|
100
|
+
source: AsyncGenerator<T>
|
|
101
|
+
) => AsyncGenerator<T>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates cache lookup middleware
|
|
105
|
+
*
|
|
106
|
+
* Checks cache for segments. If cache hit:
|
|
107
|
+
* - Applies revalidation to determine which segments need re-rendering
|
|
108
|
+
* - Resolves loaders fresh (loaders are NOT cached by design)
|
|
109
|
+
* - Sets state.cacheHit = true
|
|
110
|
+
* - Sets state.shouldRevalidate if SWR needed
|
|
111
|
+
* - Yields cached segments + fresh loader segments
|
|
112
|
+
*
|
|
113
|
+
* If cache miss:
|
|
114
|
+
* - Passes through to next middleware
|
|
115
|
+
*/
|
|
116
|
+
export function withCacheLookup<TEnv>(
|
|
117
|
+
ctx: MatchContext<TEnv>,
|
|
118
|
+
state: MatchPipelineState
|
|
119
|
+
): GeneratorMiddleware<ResolvedSegment> {
|
|
120
|
+
return async function* (
|
|
121
|
+
source: AsyncGenerator<ResolvedSegment>
|
|
122
|
+
): AsyncGenerator<ResolvedSegment> {
|
|
123
|
+
const {
|
|
124
|
+
evaluateRevalidation,
|
|
125
|
+
buildEntryRevalidateMap,
|
|
126
|
+
resolveLoadersOnlyWithRevalidation,
|
|
127
|
+
resolveLoadersOnly,
|
|
128
|
+
} = getRouterContext<TEnv>();
|
|
129
|
+
|
|
130
|
+
// Skip cache during actions
|
|
131
|
+
if (ctx.isAction || !ctx.cacheScope?.enabled) {
|
|
132
|
+
// Cache miss - pass through to segment resolution
|
|
133
|
+
yield* source;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Lookup cache
|
|
138
|
+
const cacheResult = await ctx.cacheScope.lookupRoute(
|
|
139
|
+
ctx.pathname,
|
|
140
|
+
ctx.matched.params,
|
|
141
|
+
ctx.isIntercept
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (!cacheResult) {
|
|
145
|
+
// Cache miss - pass through to segment resolution
|
|
146
|
+
yield* source;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Cache HIT
|
|
151
|
+
state.cacheHit = true;
|
|
152
|
+
state.shouldRevalidate = cacheResult.shouldRevalidate;
|
|
153
|
+
state.cachedSegments = cacheResult.segments;
|
|
154
|
+
state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
|
|
155
|
+
|
|
156
|
+
// Apply revalidation to cached segments
|
|
157
|
+
const entryRevalidateMap = buildEntryRevalidateMap?.(ctx.entries);
|
|
158
|
+
|
|
159
|
+
for (const segment of cacheResult.segments) {
|
|
160
|
+
// Skip segments client doesn't have - they need their component
|
|
161
|
+
if (!ctx.clientSegmentSet.has(segment.id)) {
|
|
162
|
+
yield segment;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Skip intercept segments - they're handled separately
|
|
167
|
+
if (segment.namespace?.startsWith("intercept:")) {
|
|
168
|
+
yield segment;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Look up revalidation rules for this segment
|
|
173
|
+
const entryInfo = entryRevalidateMap?.get(segment.id);
|
|
174
|
+
if (!entryInfo || entryInfo.revalidate.length === 0) {
|
|
175
|
+
// No revalidation rules, use default behavior (skip if client has)
|
|
176
|
+
segment.component = null;
|
|
177
|
+
segment.loading = undefined;
|
|
178
|
+
yield segment;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Evaluate revalidation rules
|
|
183
|
+
const shouldRevalidate = await evaluateRevalidation({
|
|
184
|
+
segment,
|
|
185
|
+
prevParams: ctx.prevParams,
|
|
186
|
+
getPrevSegment: null,
|
|
187
|
+
request: ctx.request,
|
|
188
|
+
prevUrl: ctx.prevUrl,
|
|
189
|
+
nextUrl: ctx.url,
|
|
190
|
+
revalidations: entryInfo.revalidate.map((fn, i) => ({
|
|
191
|
+
name: `revalidate${i}`,
|
|
192
|
+
fn,
|
|
193
|
+
})),
|
|
194
|
+
routeKey: ctx.routeKey,
|
|
195
|
+
context: ctx.handlerContext,
|
|
196
|
+
actionContext: ctx.actionContext,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (!shouldRevalidate) {
|
|
200
|
+
// Client has it, no revalidation needed
|
|
201
|
+
segment.component = null;
|
|
202
|
+
segment.loading = undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
yield segment;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Resolve loaders fresh (loaders are NOT cached by default)
|
|
209
|
+
// This ensures fresh data even on cache hit
|
|
210
|
+
const Store = ctx.Store;
|
|
211
|
+
|
|
212
|
+
if (ctx.isFullMatch) {
|
|
213
|
+
// Full match (document request) - simple loader resolution without revalidation
|
|
214
|
+
if (resolveLoadersOnly) {
|
|
215
|
+
const loaderSegments = await Store.run(() =>
|
|
216
|
+
resolveLoadersOnly(ctx.entries, ctx.handlerContext)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// Update state - full match doesn't track matchedIds separately
|
|
220
|
+
state.matchedIds = state.cachedMatchedIds!;
|
|
221
|
+
|
|
222
|
+
// Yield fresh loader segments
|
|
223
|
+
for (const segment of loaderSegments) {
|
|
224
|
+
yield segment;
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
state.matchedIds = state.cachedMatchedIds!;
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
// Partial match (navigation) - loader resolution with revalidation
|
|
231
|
+
if (resolveLoadersOnlyWithRevalidation) {
|
|
232
|
+
const loaderResult = await Store.run(() =>
|
|
233
|
+
resolveLoadersOnlyWithRevalidation(
|
|
234
|
+
ctx.entries,
|
|
235
|
+
ctx.handlerContext,
|
|
236
|
+
ctx.clientSegmentSet,
|
|
237
|
+
ctx.prevParams,
|
|
238
|
+
ctx.request,
|
|
239
|
+
ctx.prevUrl,
|
|
240
|
+
ctx.url,
|
|
241
|
+
ctx.routeKey,
|
|
242
|
+
ctx.actionContext
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Update state with fresh loader matchedIds
|
|
247
|
+
state.matchedIds = [
|
|
248
|
+
...state.cachedMatchedIds!,
|
|
249
|
+
...loaderResult.matchedIds,
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
// Yield fresh loader segments
|
|
253
|
+
for (const segment of loaderResult.segments) {
|
|
254
|
+
yield segment;
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
state.matchedIds = state.cachedMatchedIds!;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Store Middleware
|
|
3
|
+
*
|
|
4
|
+
* Stores resolved segments in cache for future requests.
|
|
5
|
+
* Implements proactive caching for partial navigation scenarios.
|
|
6
|
+
*
|
|
7
|
+
* FLOW DIAGRAM
|
|
8
|
+
* ============
|
|
9
|
+
*
|
|
10
|
+
* source (from intercept-resolution)
|
|
11
|
+
* |
|
|
12
|
+
* v
|
|
13
|
+
* +---------------------------+
|
|
14
|
+
* | Collect + yield all | Observer pattern: pass through
|
|
15
|
+
* | allSegments[] |
|
|
16
|
+
* +---------------------------+
|
|
17
|
+
* |
|
|
18
|
+
* v
|
|
19
|
+
* +---------------------+
|
|
20
|
+
* | Should skip cache? |
|
|
21
|
+
* | - !cacheScope |──yes──> return
|
|
22
|
+
* | - isAction |
|
|
23
|
+
* | - cacheHit |
|
|
24
|
+
* | - method !== GET |
|
|
25
|
+
* +---------------------+
|
|
26
|
+
* | no
|
|
27
|
+
* v
|
|
28
|
+
* +-------------------------------+
|
|
29
|
+
* | Any null components? |
|
|
30
|
+
* | (client already has segment) |
|
|
31
|
+
* +-------------------------------+
|
|
32
|
+
* |
|
|
33
|
+
* +-----+-----+
|
|
34
|
+
* | |
|
|
35
|
+
* yes no
|
|
36
|
+
* | |
|
|
37
|
+
* v v
|
|
38
|
+
* PROACTIVE DIRECT
|
|
39
|
+
* CACHE CACHE
|
|
40
|
+
* | |
|
|
41
|
+
* v v
|
|
42
|
+
* waitUntil() cacheRoute()
|
|
43
|
+
* re-render immediately
|
|
44
|
+
* fresh |
|
|
45
|
+
* | |
|
|
46
|
+
* +-----------+
|
|
47
|
+
* |
|
|
48
|
+
* v
|
|
49
|
+
* next middleware
|
|
50
|
+
*
|
|
51
|
+
*
|
|
52
|
+
* CACHING STRATEGIES
|
|
53
|
+
* ==================
|
|
54
|
+
*
|
|
55
|
+
* 1. Direct Cache (all components present):
|
|
56
|
+
* - Immediate cacheRoute() call
|
|
57
|
+
* - All segments have valid components
|
|
58
|
+
* - Used for fresh full-page renders
|
|
59
|
+
*
|
|
60
|
+
* 2. Proactive Cache (null components present):
|
|
61
|
+
* - Background re-render via waitUntil()
|
|
62
|
+
* - Creates fresh context to avoid polluting response
|
|
63
|
+
* - Re-resolves ALL segments without revalidation
|
|
64
|
+
* - Ensures cache has complete components for future requests
|
|
65
|
+
*
|
|
66
|
+
*
|
|
67
|
+
* WHY PROACTIVE CACHING?
|
|
68
|
+
* ======================
|
|
69
|
+
*
|
|
70
|
+
* During partial navigation, some segments have null components:
|
|
71
|
+
*
|
|
72
|
+
* Request: /products/123 -> /products/456
|
|
73
|
+
* Segments: [ProductLayout(null), ProductPage(component)]
|
|
74
|
+
*
|
|
75
|
+
* The null means "client already has this, don't re-send."
|
|
76
|
+
* But if we cache these null components, future document requests
|
|
77
|
+
* would fail (no component to render).
|
|
78
|
+
*
|
|
79
|
+
* Solution: Background re-render all segments fresh, then cache.
|
|
80
|
+
* This ensures the cache always has complete, renderable segments.
|
|
81
|
+
*
|
|
82
|
+
*
|
|
83
|
+
* PROACTIVE CACHE FLOW
|
|
84
|
+
* ====================
|
|
85
|
+
*
|
|
86
|
+
* 1. Current request returns (fast, with nulls)
|
|
87
|
+
* 2. waitUntil() triggers background work
|
|
88
|
+
* 3. Create fresh handler context (silent, no stream pollution)
|
|
89
|
+
* 4. Re-resolve all entries without revalidation logic
|
|
90
|
+
* 5. Also resolve intercept segments if applicable
|
|
91
|
+
* 6. Store complete segments in cache
|
|
92
|
+
*
|
|
93
|
+
*
|
|
94
|
+
* SKIP CONDITIONS
|
|
95
|
+
* ===============
|
|
96
|
+
*
|
|
97
|
+
* Caching is skipped when:
|
|
98
|
+
* - Cache scope disabled (no caching configured)
|
|
99
|
+
* - This is an action request (mutations shouldn't cache)
|
|
100
|
+
* - Cache was already hit (no need to re-cache same data)
|
|
101
|
+
* - Non-GET request (only GET requests are cacheable)
|
|
102
|
+
*/
|
|
103
|
+
import type { ResolvedSegment } from "../../types.js";
|
|
104
|
+
import { getRequestContext } from "../../server/request-context.js";
|
|
105
|
+
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
106
|
+
import { getRouterContext } from "../router-context.js";
|
|
107
|
+
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Creates cache store middleware
|
|
111
|
+
*
|
|
112
|
+
* Observes all segments passing through and stores them in cache after pipeline completes.
|
|
113
|
+
* Handles proactive caching for null-component segments.
|
|
114
|
+
*/
|
|
115
|
+
export function withCacheStore<TEnv>(
|
|
116
|
+
ctx: MatchContext<TEnv>,
|
|
117
|
+
state: MatchPipelineState
|
|
118
|
+
): GeneratorMiddleware<ResolvedSegment> {
|
|
119
|
+
return async function* (
|
|
120
|
+
source: AsyncGenerator<ResolvedSegment>
|
|
121
|
+
): AsyncGenerator<ResolvedSegment> {
|
|
122
|
+
// Collect all segments while passing them through
|
|
123
|
+
const allSegments: ResolvedSegment[] = [];
|
|
124
|
+
for await (const segment of source) {
|
|
125
|
+
allSegments.push(segment);
|
|
126
|
+
yield segment;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Skip caching if:
|
|
130
|
+
// 1. Cache miss but cache scope is disabled
|
|
131
|
+
// 2. This is an action (actions don't cache)
|
|
132
|
+
// 3. Cache was already hit (no need to re-cache)
|
|
133
|
+
// 4. Non-GET request (only cache GET requests)
|
|
134
|
+
if (!ctx.cacheScope?.enabled || ctx.isAction || state.cacheHit || ctx.request.method !== "GET") {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const {
|
|
139
|
+
createHandlerContext,
|
|
140
|
+
setupLoaderAccessSilent,
|
|
141
|
+
resolveAllSegments,
|
|
142
|
+
resolveInterceptEntry,
|
|
143
|
+
} = getRouterContext<TEnv>();
|
|
144
|
+
|
|
145
|
+
// Combine main segments with intercept segments
|
|
146
|
+
const allSegmentsToCache = [...allSegments, ...state.interceptSegments];
|
|
147
|
+
|
|
148
|
+
// Check if any non-loader segments have null components
|
|
149
|
+
// This happens when client already had those segments (partial navigation)
|
|
150
|
+
const hasNullComponents = allSegmentsToCache.some(
|
|
151
|
+
(s) => s.component === null && s.type !== "loader"
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const requestCtx = getRequestContext();
|
|
155
|
+
if (!requestCtx) return;
|
|
156
|
+
|
|
157
|
+
const cacheScope = ctx.cacheScope;
|
|
158
|
+
|
|
159
|
+
// Register onResponse callback to skip caching for non-200 responses
|
|
160
|
+
// Note: error/notFound status codes are set elsewhere (not caching-specific)
|
|
161
|
+
requestCtx.onResponse((response) => {
|
|
162
|
+
// Only cache successful responses
|
|
163
|
+
if (response.status !== 200) {
|
|
164
|
+
console.log(
|
|
165
|
+
`[CacheStore] Skipping cache: non-200 status ${response.status} for ${ctx.pathname}`
|
|
166
|
+
);
|
|
167
|
+
return response;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (hasNullComponents) {
|
|
171
|
+
// Proactive caching: render all segments fresh in background
|
|
172
|
+
// This ensures cache has complete components for future requests
|
|
173
|
+
requestCtx.waitUntil(async () => {
|
|
174
|
+
console.log(
|
|
175
|
+
`[Router.matchPartial] Proactive caching: ${ctx.pathname} (rendering null-component segments)`
|
|
176
|
+
);
|
|
177
|
+
try {
|
|
178
|
+
// Create fresh context for proactive caching
|
|
179
|
+
// This prevents handle data from polluting the response stream
|
|
180
|
+
const proactiveHandlerContext = createHandlerContext(
|
|
181
|
+
ctx.matched.params,
|
|
182
|
+
ctx.request,
|
|
183
|
+
ctx.url.searchParams,
|
|
184
|
+
ctx.pathname,
|
|
185
|
+
ctx.url,
|
|
186
|
+
ctx.bindings
|
|
187
|
+
);
|
|
188
|
+
const proactiveLoaderPromises = new Map<string, Promise<any>>();
|
|
189
|
+
|
|
190
|
+
// Set up loader access that ignores handle pushes
|
|
191
|
+
setupLoaderAccessSilent(proactiveHandlerContext, proactiveLoaderPromises);
|
|
192
|
+
|
|
193
|
+
// Re-resolve ALL segments without revalidation
|
|
194
|
+
const Store = ctx.Store;
|
|
195
|
+
const freshSegments = await Store.run(() =>
|
|
196
|
+
resolveAllSegments(
|
|
197
|
+
ctx.entries,
|
|
198
|
+
ctx.routeKey,
|
|
199
|
+
ctx.matched.params,
|
|
200
|
+
proactiveHandlerContext,
|
|
201
|
+
proactiveLoaderPromises
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Also resolve intercept segments fresh if applicable
|
|
206
|
+
let freshInterceptSegments: ResolvedSegment[] = [];
|
|
207
|
+
if (ctx.interceptResult) {
|
|
208
|
+
freshInterceptSegments = await Store.run(() =>
|
|
209
|
+
resolveInterceptEntry(
|
|
210
|
+
ctx.interceptResult!.intercept,
|
|
211
|
+
ctx.interceptResult!.entry,
|
|
212
|
+
ctx.matched.params,
|
|
213
|
+
proactiveHandlerContext,
|
|
214
|
+
true // belongsToRoute
|
|
215
|
+
// No revalidationContext = render fresh
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const completeSegments = [...freshSegments, ...freshInterceptSegments];
|
|
221
|
+
await cacheScope.cacheRoute(
|
|
222
|
+
ctx.pathname,
|
|
223
|
+
ctx.matched.params,
|
|
224
|
+
completeSegments,
|
|
225
|
+
ctx.isIntercept
|
|
226
|
+
);
|
|
227
|
+
console.log(
|
|
228
|
+
`[Router.matchPartial] Proactive caching complete: ${ctx.pathname}`
|
|
229
|
+
);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error(`[Router.matchPartial] Proactive caching failed:`, error);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
} else {
|
|
235
|
+
// All segments have components - cache directly
|
|
236
|
+
// Schedule caching in waitUntil since cacheRoute is now async (key resolution)
|
|
237
|
+
requestCtx.waitUntil(async () => {
|
|
238
|
+
await cacheScope.cacheRoute(
|
|
239
|
+
ctx.pathname,
|
|
240
|
+
ctx.matched.params,
|
|
241
|
+
allSegmentsToCache,
|
|
242
|
+
ctx.isIntercept
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return response;
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match Middleware
|
|
3
|
+
*
|
|
4
|
+
* Async generator middleware for the match pipeline.
|
|
5
|
+
* Each middleware transforms or enriches the segment stream.
|
|
6
|
+
*
|
|
7
|
+
* MIDDLEWARE OVERVIEW
|
|
8
|
+
* ===================
|
|
9
|
+
*
|
|
10
|
+
* The pipeline consists of 5 middleware layers, each with a specific role:
|
|
11
|
+
*
|
|
12
|
+
* +-------------------------------------------------------------------------+
|
|
13
|
+
* | MIDDLEWARE PIPELINE |
|
|
14
|
+
* +-------------------------------------------------------------------------+
|
|
15
|
+
* | |
|
|
16
|
+
* | [1] CACHE LOOKUP (innermost) |
|
|
17
|
+
* | Purpose: Check cache before resolving |
|
|
18
|
+
* | On hit: Yield cached segments + fresh loaders |
|
|
19
|
+
* | On miss: Pass through to segment resolution |
|
|
20
|
+
* | Side effects: Sets state.cacheHit, state.shouldRevalidate |
|
|
21
|
+
* | |
|
|
22
|
+
* | [2] SEGMENT RESOLUTION |
|
|
23
|
+
* | Purpose: Resolve segments when cache misses |
|
|
24
|
+
* | Skips if: state.cacheHit === true |
|
|
25
|
+
* | Produces: All route segments (layouts, routes, loaders) |
|
|
26
|
+
* | Two modes: Full (simple) vs Partial (with revalidation) |
|
|
27
|
+
* | |
|
|
28
|
+
* | [3] INTERCEPT RESOLUTION |
|
|
29
|
+
* | Purpose: Resolve intercept segments (modal slots) |
|
|
30
|
+
* | Triggers: When ctx.interceptResult exists |
|
|
31
|
+
* | Produces: Additional segments for named slots |
|
|
32
|
+
* | Updates: state.slots[slotName] with intercept segments |
|
|
33
|
+
* | |
|
|
34
|
+
* | [4] CACHE STORE |
|
|
35
|
+
* | Purpose: Store segments in cache for future requests |
|
|
36
|
+
* | Skips if: Cache hit, actions, or cache disabled |
|
|
37
|
+
* | Strategy: Direct cache if all components present |
|
|
38
|
+
* | Proactive cache if null components (via waitUntil) |
|
|
39
|
+
* | |
|
|
40
|
+
* | [5] BACKGROUND REVALIDATION (outermost) |
|
|
41
|
+
* | Purpose: SWR - serve stale, revalidate in background |
|
|
42
|
+
* | Triggers: When state.shouldRevalidate === true |
|
|
43
|
+
* | Action: Async resolution via waitUntil(), updates cache |
|
|
44
|
+
* | |
|
|
45
|
+
* +-------------------------------------------------------------------------+
|
|
46
|
+
*
|
|
47
|
+
*
|
|
48
|
+
* MIDDLEWARE INTERACTION PATTERNS
|
|
49
|
+
* ===============================
|
|
50
|
+
*
|
|
51
|
+
* Pattern 1: Producer Middleware (cache-lookup, segment-resolution)
|
|
52
|
+
* - Yields segments into the stream
|
|
53
|
+
* - Creates new data for downstream middleware
|
|
54
|
+
*
|
|
55
|
+
* Pattern 2: Transformer Middleware (intercept-resolution)
|
|
56
|
+
* - Passes through existing segments
|
|
57
|
+
* - Adds additional segments (intercepts)
|
|
58
|
+
*
|
|
59
|
+
* Pattern 3: Observer Middleware (cache-store, background-revalidation)
|
|
60
|
+
* - Passes through all segments unchanged
|
|
61
|
+
* - Triggers side effects based on state
|
|
62
|
+
*
|
|
63
|
+
*
|
|
64
|
+
* STATE FLAGS
|
|
65
|
+
* ===========
|
|
66
|
+
*
|
|
67
|
+
* The middleware communicate through MatchPipelineState:
|
|
68
|
+
*
|
|
69
|
+
* state.cacheHit - Set by cache-lookup, read by others to skip work
|
|
70
|
+
* state.shouldRevalidate - Set by cache-lookup, triggers bg-revalidation
|
|
71
|
+
* state.segments - Accumulated segments from pipeline
|
|
72
|
+
* state.interceptSegments - Segments for intercept slots
|
|
73
|
+
* state.slots - Named slot data for client
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
export { withCacheLookup } from "./cache-lookup.js";
|
|
77
|
+
export { withSegmentResolution } from "./segment-resolution.js";
|
|
78
|
+
export { withInterceptResolution } from "./intercept-resolution.js";
|
|
79
|
+
export { withCacheStore } from "./cache-store.js";
|
|
80
|
+
export { withBackgroundRevalidation } from "./background-revalidation.js";
|
|
81
|
+
export type { GeneratorMiddleware } from "./cache-lookup.js";
|