@rangojs/router 0.0.0-experimental.65 → 0.0.0-experimental.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/router/loader-resolution.ts +70 -47
- package/src/router/match-middleware/cache-lookup.ts +8 -4
- package/src/router/match-middleware/segment-resolution.ts +1 -1
- package/src/rsc/handler.ts +5 -1
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/progressive-enhancement.ts +10 -2
- package/src/rsc/rsc-rendering.ts +5 -1
- package/src/rsc/server-action.ts +6 -0
- package/src/rsc/types.ts +1 -0
- package/src/server/handle-store.ts +19 -0
- package/src/server/request-context.ts +30 -2
package/dist/vite/index.js
CHANGED
|
@@ -1733,7 +1733,7 @@ import { resolve } from "node:path";
|
|
|
1733
1733
|
// package.json
|
|
1734
1734
|
var package_default = {
|
|
1735
1735
|
name: "@rangojs/router",
|
|
1736
|
-
version: "0.0.0-experimental.
|
|
1736
|
+
version: "0.0.0-experimental.66",
|
|
1737
1737
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1738
1738
|
keywords: [
|
|
1739
1739
|
"react",
|
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
} from "../types";
|
|
22
22
|
import type { LoaderRevalidationResult, ActionContext } from "./types";
|
|
23
23
|
import { isHandle, collectHandleData, type Handle } from "../handle.js";
|
|
24
|
-
import
|
|
24
|
+
import { buildHandleSnapshot } from "../server/handle-store.js";
|
|
25
25
|
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
26
26
|
import { _getRequestContext } from "../server/request-context.js";
|
|
27
27
|
import { isInsideLoaderScope } from "../server/context.js";
|
|
@@ -284,10 +284,9 @@ function createLoaderExecutor<TEnv>(
|
|
|
284
284
|
);
|
|
285
285
|
}
|
|
286
286
|
const segmentOrder = reqCtx._renderBarrierSegmentOrder ?? [];
|
|
287
|
-
const snapshot =
|
|
288
|
-
reqCtx.
|
|
289
|
-
segmentOrder
|
|
290
|
-
);
|
|
287
|
+
const snapshot =
|
|
288
|
+
reqCtx._renderBarrierHandleSnapshot ??
|
|
289
|
+
buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
|
|
291
290
|
return collectHandleData(item, snapshot, segmentOrder);
|
|
292
291
|
}
|
|
293
292
|
|
|
@@ -324,6 +323,17 @@ function createLoaderExecutor<TEnv>(
|
|
|
324
323
|
);
|
|
325
324
|
}
|
|
326
325
|
|
|
326
|
+
// Bidirectional deadlock check: if a handler already started
|
|
327
|
+
// awaiting this loader, calling rendered() would deadlock.
|
|
328
|
+
if (reqCtx._handlerLoaderDeps?.has(currentLoaderId)) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`Deadlock: loader "${currentLoaderId}" called ctx.rendered() but a handler ` +
|
|
331
|
+
`is already awaiting this loader via ctx.use(). The handler blocks ` +
|
|
332
|
+
`segment resolution, which blocks the barrier, which blocks this loader. ` +
|
|
333
|
+
`Move the data dependency to a loader-to-loader pattern instead.`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
327
337
|
// Register this loader as waiting for the barrier so that
|
|
328
338
|
// setupLoaderAccess can detect deadlocks when a handler
|
|
329
339
|
// tries to await the same loader via ctx.use().
|
|
@@ -354,25 +364,6 @@ function createLoaderExecutor<TEnv>(
|
|
|
354
364
|
return useLoader;
|
|
355
365
|
}
|
|
356
366
|
|
|
357
|
-
/**
|
|
358
|
-
* Build a HandleData snapshot from the HandleStore using segment ordering.
|
|
359
|
-
* Reads data directly from the store for each segment in order.
|
|
360
|
-
*/
|
|
361
|
-
function buildHandleSnapshot(
|
|
362
|
-
handleStore: HandleStore,
|
|
363
|
-
segmentOrder: string[],
|
|
364
|
-
): HandleData {
|
|
365
|
-
const data: HandleData = {};
|
|
366
|
-
for (const segmentId of segmentOrder) {
|
|
367
|
-
const segData = handleStore.getDataForSegment(segmentId);
|
|
368
|
-
for (const handleName in segData) {
|
|
369
|
-
if (!data[handleName]) data[handleName] = {};
|
|
370
|
-
data[handleName][segmentId] = segData[handleName];
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return data;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
367
|
/**
|
|
377
368
|
* Set up the use() method on handler context to access loaders and handles.
|
|
378
369
|
*
|
|
@@ -386,15 +377,22 @@ export function setupLoaderAccess<TEnv>(
|
|
|
386
377
|
ctx: HandlerContext<any, TEnv>,
|
|
387
378
|
loaderPromises: Map<string, Promise<any>>,
|
|
388
379
|
): void {
|
|
389
|
-
// Eagerly capture the HandleStore at setup time
|
|
390
|
-
// In workerd/Cloudflare, dynamic imports and
|
|
391
|
-
// can disrupt AsyncLocalStorage, causing
|
|
392
|
-
// undefined when handlers later call
|
|
393
|
-
//
|
|
394
|
-
const
|
|
380
|
+
// Eagerly capture the request context and HandleStore at setup time
|
|
381
|
+
// (before pipeline async ops). In workerd/Cloudflare, dynamic imports and
|
|
382
|
+
// fetch() in the match pipeline can disrupt AsyncLocalStorage, causing
|
|
383
|
+
// getRequestContext() to return undefined when handlers later call
|
|
384
|
+
// ctx.use(handle). Capturing early ensures references survive ALS disruption.
|
|
385
|
+
const reqCtxRef = _getRequestContext();
|
|
386
|
+
const handleStoreRef = reqCtxRef?._handleStore;
|
|
395
387
|
|
|
396
388
|
const useLoader = createLoaderExecutor(ctx, loaderPromises);
|
|
397
389
|
|
|
390
|
+
// Track whether we're inside a handle push callback. Loaders started
|
|
391
|
+
// from push callbacks (e.g. push(async () => ctx.use(Loader))) do NOT
|
|
392
|
+
// block segment resolution, so they must not be registered as handler
|
|
393
|
+
// dependencies for deadlock detection.
|
|
394
|
+
let insideHandlePush = false;
|
|
395
|
+
|
|
398
396
|
ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
399
397
|
if (isHandle(item)) {
|
|
400
398
|
const handle = item;
|
|
@@ -414,28 +412,53 @@ export function setupLoaderAccess<TEnv>(
|
|
|
414
412
|
) => {
|
|
415
413
|
if (!store) return;
|
|
416
414
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
415
|
+
if (typeof dataOrFn === "function") {
|
|
416
|
+
// Mark scope so ctx.use(loader) calls inside the callback
|
|
417
|
+
// are not registered as handler-to-loader deps.
|
|
418
|
+
insideHandlePush = true;
|
|
419
|
+
try {
|
|
420
|
+
const result = (dataOrFn as () => Promise<unknown>)();
|
|
421
|
+
store.push(handle.$$id, segmentId, result);
|
|
422
|
+
} finally {
|
|
423
|
+
insideHandlePush = false;
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
421
427
|
|
|
422
|
-
store.push(handle.$$id, segmentId,
|
|
428
|
+
store.push(handle.$$id, segmentId, dataOrFn);
|
|
423
429
|
};
|
|
424
430
|
}
|
|
425
431
|
|
|
426
|
-
// Deadlock guard
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
432
|
+
// Deadlock guard and handler-to-loader dependency tracking.
|
|
433
|
+
// Skip when inside a DSL loader scope (resolveLoaderData also calls
|
|
434
|
+
// ctx.use() but that's DSL-to-DSL, not handler-to-loader) or when
|
|
435
|
+
// inside a handle push callback (push callbacks don't block segment
|
|
436
|
+
// resolution so they can't cause rendered() deadlocks).
|
|
430
437
|
const loader = item as LoaderDefinition<any, any>;
|
|
431
|
-
if (
|
|
432
|
-
const reqCtx = _getRequestContext();
|
|
433
|
-
if (reqCtx
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
)
|
|
438
|
+
if (!isInsideLoaderScope() && !insideHandlePush) {
|
|
439
|
+
const reqCtx = reqCtxRef ?? _getRequestContext();
|
|
440
|
+
if (reqCtx) {
|
|
441
|
+
// Direction 1: handler awaits loader that already called rendered()
|
|
442
|
+
if (
|
|
443
|
+
loaderPromises.has(loader.$$id) &&
|
|
444
|
+
reqCtx._renderBarrierWaiters?.has(loader.$$id)
|
|
445
|
+
) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`Deadlock: handler is awaiting loader "${loader.$$id}" which called ctx.rendered(). ` +
|
|
448
|
+
`The loader is waiting for segment resolution, but the handler blocks resolution. ` +
|
|
449
|
+
`Move the data dependency to a loader-to-loader pattern instead.`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
// Direction 2: track dep so rendered() can detect the deadlock
|
|
453
|
+
// if the loader calls it later. Skip when the barrier has already
|
|
454
|
+
// resolved — no deadlock is possible (rendered() resolves immediately).
|
|
455
|
+
// _renderBarrierSegmentOrder is undefined before resolution, string[]
|
|
456
|
+
// after. This also prevents false positives from handle push callbacks
|
|
457
|
+
// that resume after their first await (post-barrier-resolution).
|
|
458
|
+
if (reqCtx._renderBarrierSegmentOrder === undefined) {
|
|
459
|
+
if (!reqCtx._handlerLoaderDeps) reqCtx._handlerLoaderDeps = new Set();
|
|
460
|
+
reqCtx._handlerLoaderDeps.add(loader.$$id);
|
|
461
|
+
}
|
|
439
462
|
}
|
|
440
463
|
}
|
|
441
464
|
|
|
@@ -194,11 +194,13 @@ async function* yieldFromStore<TEnv>(
|
|
|
194
194
|
state.cachedSegments = segments;
|
|
195
195
|
state.cachedMatchedIds = segments.map((s) => s.id);
|
|
196
196
|
|
|
197
|
-
// Set streaming flag and resolve render barrier.
|
|
197
|
+
// Set streaming flag (once) and resolve render barrier.
|
|
198
198
|
const reqCtx = handleStoreRef ? undefined : _lazyGetRequestContext?.();
|
|
199
199
|
const barrierReqCtx = reqCtx ?? _getRequestContext();
|
|
200
200
|
if (barrierReqCtx) {
|
|
201
|
-
barrierReqCtx._treeHasStreaming
|
|
201
|
+
if (barrierReqCtx._treeHasStreaming === undefined) {
|
|
202
|
+
barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
|
|
203
|
+
}
|
|
202
204
|
barrierReqCtx._resolveRenderBarrier(segments);
|
|
203
205
|
}
|
|
204
206
|
|
|
@@ -623,10 +625,12 @@ export function withCacheLookup<TEnv>(
|
|
|
623
625
|
yield segment;
|
|
624
626
|
}
|
|
625
627
|
|
|
626
|
-
// Set streaming flag and resolve render barrier.
|
|
628
|
+
// Set streaming flag (once) and resolve render barrier.
|
|
627
629
|
const barrierReqCtx = _getRequestContext();
|
|
628
630
|
if (barrierReqCtx) {
|
|
629
|
-
barrierReqCtx._treeHasStreaming
|
|
631
|
+
if (barrierReqCtx._treeHasStreaming === undefined) {
|
|
632
|
+
barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
|
|
633
|
+
}
|
|
630
634
|
barrierReqCtx._resolveRenderBarrier(cacheResult.segments);
|
|
631
635
|
}
|
|
632
636
|
|
package/src/rsc/handler.ts
CHANGED
|
@@ -1090,7 +1090,11 @@ export function createRSCHandler<
|
|
|
1090
1090
|
},
|
|
1091
1091
|
};
|
|
1092
1092
|
|
|
1093
|
-
const rscStream = renderToReadableStream(payload
|
|
1093
|
+
const rscStream = renderToReadableStream(payload, {
|
|
1094
|
+
onError: (error: unknown) => {
|
|
1095
|
+
callOnError(error, "rendering", { request, url, env });
|
|
1096
|
+
},
|
|
1097
|
+
});
|
|
1094
1098
|
|
|
1095
1099
|
const isRscRequest =
|
|
1096
1100
|
isPartial ||
|
package/src/rsc/loader-fetch.ts
CHANGED
|
@@ -168,8 +168,19 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
168
168
|
loaderResult: unknown;
|
|
169
169
|
}
|
|
170
170
|
const loaderPayload: LoaderPayload = { loaderResult: result };
|
|
171
|
-
const rscStream =
|
|
172
|
-
|
|
171
|
+
const rscStream = ctx.renderToReadableStream<LoaderPayload>(
|
|
172
|
+
loaderPayload,
|
|
173
|
+
{
|
|
174
|
+
onError: (error: unknown) => {
|
|
175
|
+
ctx.callOnError(error, "rendering", {
|
|
176
|
+
request,
|
|
177
|
+
url,
|
|
178
|
+
env,
|
|
179
|
+
loaderName: loaderId,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
);
|
|
173
184
|
|
|
174
185
|
return createResponseWithMergedHeaders(rscStream, {
|
|
175
186
|
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
@@ -199,7 +210,16 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
199
210
|
name: err.name,
|
|
200
211
|
},
|
|
201
212
|
};
|
|
202
|
-
const rscStream = ctx.renderToReadableStream(errorPayload
|
|
213
|
+
const rscStream = ctx.renderToReadableStream(errorPayload, {
|
|
214
|
+
onError: (error: unknown) => {
|
|
215
|
+
ctx.callOnError(error, "rendering", {
|
|
216
|
+
request,
|
|
217
|
+
url,
|
|
218
|
+
env,
|
|
219
|
+
loaderName: loaderId,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
});
|
|
203
223
|
|
|
204
224
|
return createResponseWithMergedHeaders(rscStream, {
|
|
205
225
|
status: 500,
|
|
@@ -259,7 +259,11 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
259
259
|
formState: actionResult,
|
|
260
260
|
};
|
|
261
261
|
|
|
262
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload
|
|
262
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
263
|
+
onError: (error: unknown) => {
|
|
264
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
265
|
+
},
|
|
266
|
+
});
|
|
263
267
|
// metricsStore=undefined is safe: the handler already stashed the early
|
|
264
268
|
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
265
269
|
// without falling back to a fresh startSSRSetup.
|
|
@@ -360,7 +364,11 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
360
364
|
},
|
|
361
365
|
};
|
|
362
366
|
|
|
363
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload
|
|
367
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
368
|
+
onError: (error: unknown) => {
|
|
369
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
370
|
+
},
|
|
371
|
+
});
|
|
364
372
|
// metricsStore=undefined is safe: the handler already stashed the early
|
|
365
373
|
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
366
374
|
// without falling back to a fresh startSSRSetup.
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -173,7 +173,11 @@ export async function handleRscRendering<TEnv>(
|
|
|
173
173
|
|
|
174
174
|
// Serialize to RSC stream
|
|
175
175
|
const rscSerializeStart = performance.now();
|
|
176
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload
|
|
176
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
177
|
+
onError: (error: unknown) => {
|
|
178
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
179
|
+
},
|
|
180
|
+
});
|
|
177
181
|
const rscSerializeDur = performance.now() - rscSerializeStart;
|
|
178
182
|
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
179
183
|
appendMetric(
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -226,6 +226,9 @@ export async function executeServerAction<TEnv>(
|
|
|
226
226
|
|
|
227
227
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
228
228
|
temporaryReferences,
|
|
229
|
+
onError: (error: unknown) => {
|
|
230
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
231
|
+
},
|
|
229
232
|
});
|
|
230
233
|
|
|
231
234
|
return createResponseWithMergedHeaders(rscStream, {
|
|
@@ -332,6 +335,9 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
332
335
|
const renderStart = performance.now();
|
|
333
336
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
334
337
|
temporaryReferences,
|
|
338
|
+
onError: (error: unknown) => {
|
|
339
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
340
|
+
},
|
|
335
341
|
});
|
|
336
342
|
const rscSerializeDur = performance.now() - renderStart;
|
|
337
343
|
// This measures synchronous stream creation, not end-to-end stream consumption.
|
package/src/rsc/types.ts
CHANGED
|
@@ -13,6 +13,25 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export type HandleData = Record<string, Record<string, unknown[]>>;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Build a HandleData snapshot from a HandleStore using segment ordering.
|
|
18
|
+
* Reads data directly from the store for each segment in order.
|
|
19
|
+
*/
|
|
20
|
+
export function buildHandleSnapshot(
|
|
21
|
+
handleStore: HandleStore,
|
|
22
|
+
segmentOrder: string[],
|
|
23
|
+
): HandleData {
|
|
24
|
+
const data: HandleData = {};
|
|
25
|
+
for (const segmentId of segmentOrder) {
|
|
26
|
+
const segData = handleStore.getDataForSegment(segmentId);
|
|
27
|
+
for (const handleName in segData) {
|
|
28
|
+
if (!data[handleName]) data[handleName] = {};
|
|
29
|
+
data[handleName][segmentId] = segData[handleName];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
function createLateHandlePushError(
|
|
17
36
|
handleName: string,
|
|
18
37
|
segmentId: string,
|
|
@@ -26,7 +26,12 @@ import {
|
|
|
26
26
|
contextSet,
|
|
27
27
|
isNonCacheable,
|
|
28
28
|
} from "../context-var.js";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createHandleStore,
|
|
31
|
+
buildHandleSnapshot,
|
|
32
|
+
type HandleStore,
|
|
33
|
+
type HandleData,
|
|
34
|
+
} from "./handle-store.js";
|
|
30
35
|
import { isHandle } from "../handle.js";
|
|
31
36
|
import { track, type MetricsStore } from "./context.js";
|
|
32
37
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
@@ -306,6 +311,19 @@ export interface RequestContext<
|
|
|
306
311
|
*/
|
|
307
312
|
_renderBarrierWaiters?: Set<string>;
|
|
308
313
|
|
|
314
|
+
/**
|
|
315
|
+
* @internal Loader IDs that handlers have started awaiting via ctx.use().
|
|
316
|
+
* Used for bidirectional deadlock detection: if a loader later calls
|
|
317
|
+
* rendered() and a handler already awaits it, we can detect the deadlock.
|
|
318
|
+
*/
|
|
319
|
+
_handlerLoaderDeps?: Set<string>;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @internal Cached HandleData snapshot built at barrier resolution time.
|
|
323
|
+
* Avoids rebuilding the snapshot on every loader ctx.use(handle) call.
|
|
324
|
+
*/
|
|
325
|
+
_renderBarrierHandleSnapshot?: HandleData;
|
|
326
|
+
|
|
309
327
|
/** @internal Per-request error dedup set for onError reporting */
|
|
310
328
|
_reportedErrors: WeakSet<object>;
|
|
311
329
|
|
|
@@ -362,6 +380,8 @@ export type PublicRequestContext<
|
|
|
362
380
|
| "_renderBarrierSegmentOrder"
|
|
363
381
|
| "_treeHasStreaming"
|
|
364
382
|
| "_renderBarrierWaiters"
|
|
383
|
+
| "_handlerLoaderDeps"
|
|
384
|
+
| "_renderBarrierHandleSnapshot"
|
|
365
385
|
| "_reportBackgroundError"
|
|
366
386
|
| "_debugPerformance"
|
|
367
387
|
| "_metricsStore"
|
|
@@ -808,10 +828,18 @@ export function createRequestContext<TEnv>(
|
|
|
808
828
|
) => {
|
|
809
829
|
if (barrierResolved) return;
|
|
810
830
|
barrierResolved = true;
|
|
811
|
-
|
|
831
|
+
const segOrder = segments
|
|
812
832
|
.filter((s) => s.type !== "loader")
|
|
813
833
|
.map((s) => s.id);
|
|
834
|
+
ctx._renderBarrierSegmentOrder = segOrder;
|
|
835
|
+
// Build and cache handle snapshot so loader ctx.use(handle) calls
|
|
836
|
+
// don't rebuild it on every invocation.
|
|
837
|
+
ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
|
|
838
|
+
handleStore,
|
|
839
|
+
segOrder,
|
|
840
|
+
);
|
|
814
841
|
ctx._renderBarrierWaiters = undefined;
|
|
842
|
+
ctx._handlerLoaderDeps = undefined;
|
|
815
843
|
if (resolveBarrier) resolveBarrier();
|
|
816
844
|
};
|
|
817
845
|
Object.defineProperty(ctx, "_renderBarrier", {
|