@rangojs/router 0.0.0-experimental.136 → 0.0.0-experimental.137

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.
@@ -15,6 +15,7 @@ import type { MiddlewareFn } from "../router/middleware.js";
15
15
  import { executeMiddleware } from "../router/middleware.js";
16
16
  import { observePhase, PHASES } from "../router/instrument.js";
17
17
  import { gateTransitions } from "./transition-gate.js";
18
+ import { resolvedHandleStream } from "../handles/deferred-resolution.js";
18
19
  import type { RscPayload, ReactFormState } from "./types.js";
19
20
  import {
20
21
  createResponseWithMergedHeaders,
@@ -315,7 +316,8 @@ export async function handleProgressiveEnhancement<TEnv>(
315
316
  params: match.params,
316
317
  isPartial: false,
317
318
  rootLayout: ctx.router.rootLayout,
318
- handles: handleStore.stream(),
319
+ // PE full render: resolve deferred handle values server-side.
320
+ handles: resolvedHandleStream(handleStore),
319
321
  version: ctx.version,
320
322
  stateCookieName: ctx.router.resolvedStateCookieName,
321
323
  themeConfig: ctx.router.themeConfig,
@@ -462,7 +464,8 @@ async function renderPeErrorBoundary<TEnv>(
462
464
  isPartial: false,
463
465
  isError: true,
464
466
  rootLayout: ctx.router.rootLayout,
465
- handles: handleStore.stream(),
467
+ // PE error-boundary full render: resolve deferred handle values server-side.
468
+ handles: resolvedHandleStream(handleStore),
466
469
  version: ctx.version,
467
470
  stateCookieName: ctx.router.resolvedStateCookieName,
468
471
  themeConfig: ctx.router.themeConfig,
@@ -22,6 +22,7 @@ import {
22
22
  } from "./helpers.js";
23
23
  import type { HandlerContext } from "./handler-context.js";
24
24
  import { gateTransitions } from "./transition-gate.js";
25
+ import { resolvedHandleStream } from "../handles/deferred-resolution.js";
25
26
 
26
27
  export function handleRscRendering<TEnv>(
27
28
  ctx: HandlerContext<TEnv>,
@@ -78,7 +79,10 @@ async function handleRscRenderingInner<TEnv>(
78
79
  params: m.params,
79
80
  isPartial: false,
80
81
  rootLayout: ctx.router.rootLayout,
81
- handles: handleStore.stream(),
82
+ // Full render: resolve deferred handle values server-side so SSR markup and
83
+ // the first sync useHandle read see resolved values. Partial payloads below
84
+ // keep streaming (handleStore.stream()).
85
+ handles: resolvedHandleStream(handleStore),
82
86
  version: ctx.version,
83
87
  prefetchCacheTTL: ctx.router.prefetchCacheTTL,
84
88
  prefetchCacheSize: ctx.router.prefetchCacheSize,
package/src/ssr/index.tsx CHANGED
@@ -195,6 +195,7 @@ function createSsrEventController(opts: {
195
195
  subscribeToHandles: () => () => {},
196
196
  setHandleData: () => {},
197
197
  getHandleState: () => handleState,
198
+ setRouteSegmentIds: () => {},
198
199
  setParams: (nextParams) => {
199
200
  params = nextParams;
200
201
  },
@@ -12,7 +12,9 @@
12
12
  * It relies on createHandle registering the collect even in a bare test (it
13
13
  * assigns a runtime fallback id when the Vite plugin did not inject one). If a
14
14
  * handle's module was never imported (so createHandle never ran), the collect is
15
- * unregistered and this falls back to a flat array with a warning.
15
+ * unregistered and this falls back to the default identity collect — with a
16
+ * warning, since a CUSTOM collect that failed to register silently returns the
17
+ * wrong shape.
16
18
  */
17
19
 
18
20
  import { getCollectFn, type Handle } from "../handle.js";
@@ -25,16 +27,20 @@ export function collectHandle<TData, TAccumulated>(
25
27
  | ((segments: TData[][]) => TAccumulated)
26
28
  | undefined;
27
29
 
30
+ // Drop empty arrays matching production behavior (segment count/indices).
31
+ const nonEmpty = segments.filter((seg) => seg.length > 0) as TData[][];
32
+
33
+ // No registered collect (the handle's module was not imported): fall back to the
34
+ // default identity collect — the per-segment arrays as-is, mirroring production
35
+ // collectHandleData. Warn, because a handle with a CUSTOM collect would silently
36
+ // get the wrong shape (the runtime can't tell it from an intended default).
28
37
  if (!collectFn) {
29
38
  console.warn(
30
- `[rango] collectHandle: handle "${handle.$$id}" has no registered collect ` +
31
- `function. Import the handle's module so createHandle() runs. Falling ` +
32
- `back to a flat array.`,
39
+ `[rango] collectHandle: handle "${handle.$$id}" has no registered collect — ` +
40
+ `falling back to the identity (per-segment data as-is). Import the handle's ` +
41
+ `module so createHandle() runs if you expected a custom collect.`,
33
42
  );
34
- return segments.flat() as unknown as TAccumulated;
43
+ return nonEmpty as unknown as TAccumulated;
35
44
  }
36
-
37
- // Drop empty arrays matching production behavior (segment count/indices).
38
- const nonEmpty = segments.filter((seg) => seg.length > 0) as TData[][];
39
45
  return collectFn(nonEmpty);
40
46
  }
@@ -46,6 +46,7 @@ import {
46
46
  generateHistoryKey,
47
47
  } from "../browser/navigation-store.js";
48
48
  import { createEventController } from "../browser/event-controller.js";
49
+ import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
49
50
  import type { NavigationStore, NavigationBridge } from "../browser/types.js";
50
51
  import type { EventController } from "../browser/event-controller.js";
51
52
  import type { ResolvedSegment, RscMetadata } from "../browser/types.js";
@@ -481,8 +482,12 @@ export async function renderRoute(
481
482
 
482
483
  const eventController = createEventController({ initialLocation: url });
483
484
  eventController.setParams(initialMatch.params);
485
+ // Resolve-by-default: resolve any deferred (Promise) seeded handle values
486
+ // before applying, so the seeded handles reach collect/useHandle resolved —
487
+ // matching what the server/client do in a real app.
488
+ const resolvedSeed = await resolveDeferredHandleValues(handleSeed);
484
489
  eventController.setHandleData(
485
- handleSeed,
490
+ resolvedSeed,
486
491
  initialSegments.map((s) => s.id),
487
492
  );
488
493
 
@@ -1,75 +0,0 @@
1
- import type { HandleData } from "../types.js";
2
- import { isThenable } from "../../handles/is-thenable.js";
3
-
4
- /**
5
- * The set of handle names whose deferred (Promise) values MUST be resolved in
6
- * the store BEFORE the snapshot is applied during client navigation.
7
- *
8
- * The boundary: a handle belongs here only if its consumer `use()`s a promise in
9
- * <head>, above the route's <Suspense>. Suspending there would revert the
10
- * just-committed route and hide its loading fallback. Today that is Meta alone
11
- * (MetaTags lives in <head> and use()s deferred descriptors). Every OTHER handle
12
- * keeps the public DeferredHandleEntry contract: its deferred value reaches the
13
- * consumer AS A PROMISE during soft navigation, narrowed via isThenable().
14
- *
15
- * If a future head-placed handle starts use()-ing promises, add its name here.
16
- */
17
- export const HEAD_RESOLVE_HANDLE_NAMES: readonly string[] = [
18
- "__rsc_router_meta__",
19
- ];
20
-
21
- /**
22
- * True when a handle value in this snapshot is a deferred (Promise) value.
23
- *
24
- * When `onlyHandleNames` is given, only those handle buckets are considered;
25
- * deferred values under any other handle are ignored (they pass through to the
26
- * consumer as promises, by contract).
27
- */
28
- export function hasDeferredHandleValue(
29
- data: HandleData,
30
- onlyHandleNames?: readonly string[],
31
- ): boolean {
32
- const scope = onlyHandleNames ? new Set(onlyHandleNames) : null;
33
- for (const [handleName, segments] of Object.entries(data)) {
34
- if (scope && !scope.has(handleName)) continue;
35
- for (const values of Object.values(segments)) {
36
- if (values.some(isThenable)) return true;
37
- }
38
- }
39
- return false;
40
- }
41
-
42
- /**
43
- * Snapshot with deferred (Promise) values awaited; a rejected deferred is
44
- * dropped (it contributes nothing), mirroring the render-side REJECTED_META.
45
- * Promise.allSettled treats non-promise values as already-fulfilled, so plain
46
- * values pass through unchanged.
47
- *
48
- * When `onlyHandleNames` is given, ONLY those handle buckets are resolved; every
49
- * other bucket is copied through by reference (its deferred values keep their
50
- * promise identity so the consumer can narrow them).
51
- */
52
- export async function resolveDeferredHandleValues(
53
- data: HandleData,
54
- onlyHandleNames?: readonly string[],
55
- ): Promise<HandleData> {
56
- const scope = onlyHandleNames ? new Set(onlyHandleNames) : null;
57
- const out: HandleData = {};
58
- await Promise.all(
59
- Object.entries(data).flatMap(([handleName, segments]) => {
60
- // Out-of-scope buckets pass through untouched (promise identity kept).
61
- if (scope && !scope.has(handleName)) {
62
- out[handleName] = segments;
63
- return [];
64
- }
65
- out[handleName] = {};
66
- return Object.entries(segments).map(async ([segmentId, values]) => {
67
- const settled = await Promise.allSettled(values);
68
- out[handleName][segmentId] = settled
69
- .filter((r) => r.status === "fulfilled")
70
- .map((r) => (r as PromiseFulfilledResult<unknown>).value);
71
- });
72
- }),
73
- );
74
- return out;
75
- }