@rangojs/router 0.0.0-experimental.90 → 0.0.0-experimental.91

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.
@@ -2040,7 +2040,7 @@ import { resolve } from "node:path";
2040
2040
  // package.json
2041
2041
  var package_default = {
2042
2042
  name: "@rangojs/router",
2043
- version: "0.0.0-experimental.90",
2043
+ version: "0.0.0-experimental.91",
2044
2044
  description: "Django-inspired RSC router with composable URL patterns",
2045
2045
  keywords: [
2046
2046
  "react",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.90",
3
+ "version": "0.0.0-experimental.91",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -132,6 +132,15 @@
132
132
  "access": "public",
133
133
  "tag": "experimental"
134
134
  },
135
+ "scripts": {
136
+ "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
137
+ "prepublishOnly": "pnpm build",
138
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit",
139
+ "test": "playwright test",
140
+ "test:ui": "playwright test --ui",
141
+ "test:unit": "vitest run",
142
+ "test:unit:watch": "vitest"
143
+ },
135
144
  "dependencies": {
136
145
  "@vitejs/plugin-rsc": "^0.5.23",
137
146
  "debug": "^4.4.1",
@@ -143,12 +152,12 @@
143
152
  "@playwright/test": "^1.49.1",
144
153
  "@types/debug": "^4.1.12",
145
154
  "@types/node": "^24.10.1",
146
- "@types/react": "^19.2.7",
147
- "@types/react-dom": "^19.2.3",
155
+ "@types/react": "catalog:",
156
+ "@types/react-dom": "catalog:",
148
157
  "esbuild": "^0.27.0",
149
158
  "jiti": "^2.6.1",
150
- "react": "^19.2.4",
151
- "react-dom": "^19.2.4",
159
+ "react": "catalog:",
160
+ "react-dom": "catalog:",
152
161
  "tinyexec": "^0.3.2",
153
162
  "typescript": "^5.3.0",
154
163
  "vitest": "^4.0.0"
@@ -166,13 +175,5 @@
166
175
  "vite": {
167
176
  "optional": true
168
177
  }
169
- },
170
- "scripts": {
171
- "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
172
- "typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit",
173
- "test": "playwright test",
174
- "test:ui": "playwright test --ui",
175
- "test:unit": "vitest run",
176
- "test:unit:watch": "vitest"
177
178
  }
178
- }
179
+ }
@@ -59,6 +59,14 @@ interface EvaluateRevalidationOptions<TEnv> {
59
59
  stale?: boolean;
60
60
  /** Trace source hint for the revalidation trace */
61
61
  traceSource?: RevalidationTraceEntry["source"];
62
+ /**
63
+ * Override the segment-type-derived default. When set, the value is used as
64
+ * the seed `defaultShouldRevalidate` passed to user revalidate fns and the
65
+ * reason flows into the trace. Callers use this when client-knowledge
66
+ * (e.g. parallel slot not in clientSegmentIds) should dictate the seed
67
+ * instead of the params/method-based heuristic.
68
+ */
69
+ defaultOverride?: { value: boolean; reason: string };
62
70
  }
63
71
 
64
72
  /**
@@ -81,6 +89,7 @@ export async function evaluateRevalidation<TEnv>(
81
89
  actionContext,
82
90
  stale,
83
91
  traceSource,
92
+ defaultOverride,
84
93
  } = options;
85
94
  const nextParams = segment.params || {};
86
95
  const paramsChanged = !paramsEqual(nextParams, prevParams);
@@ -110,7 +119,12 @@ export async function evaluateRevalidation<TEnv>(
110
119
  let defaultShouldRevalidate: boolean;
111
120
  let defaultReason: string;
112
121
 
113
- if (request.method === "POST") {
122
+ if (defaultOverride) {
123
+ // Caller injected the seed (e.g. parallel slot not in clientSegmentIds).
124
+ // Skip the type-derived heuristic — caller knows better in this context.
125
+ defaultShouldRevalidate = defaultOverride.value;
126
+ defaultReason = defaultOverride.reason;
127
+ } else if (request.method === "POST") {
114
128
  // Actions: revalidate segments that belong to the route, skip parent chain
115
129
  if (segment.type === "route") {
116
130
  // Route segment always revalidates on actions
@@ -89,6 +89,27 @@ function observeStreamedHandler(
89
89
  });
90
90
  }
91
91
 
92
+ /**
93
+ * Trace a parallel slot that's being force-rendered on a full refetch (client
94
+ * has no cached state). User revalidate fns are bypassed in this case — see
95
+ * the call sites for the load-bearing rationale.
96
+ */
97
+ function traceFullRefetchedParallelSlot(
98
+ parallelId: string,
99
+ belongsToRoute: boolean,
100
+ ): void {
101
+ if (!isTraceActive()) return;
102
+ pushRevalidationTraceEntry({
103
+ segmentId: parallelId,
104
+ segmentType: "parallel",
105
+ belongsToRoute,
106
+ source: "parallel",
107
+ defaultShouldRevalidate: true,
108
+ finalShouldRevalidate: true,
109
+ reason: "full-refetch",
110
+ });
111
+ }
112
+
92
113
  // ---------------------------------------------------------------------------
93
114
  // Revalidation telemetry helper
94
115
  // ---------------------------------------------------------------------------
@@ -448,44 +469,30 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
448
469
 
449
470
  const isFullRefetch = clientSegmentIds.size === 0;
450
471
  const isNewParent = !clientSegmentIds.has(entry.shortCode);
451
- if (
452
- isFullRefetch ||
453
- clientSegmentIds.has(parallelId) ||
454
- belongsToRoute ||
455
- isNewParent
456
- ) {
457
- matchedIds.push(parallelId);
458
- }
472
+ // Always announce the slot in matchedIds — it's unconditionally appended
473
+ // to `segments` below, and a segment present in segments but missing from
474
+ // matched lets the client prune it (then it's missing from clientSegmentIds
475
+ // on the next request, perpetuating the staleness).
476
+ matchedIds.push(parallelId);
459
477
 
460
- const shouldResolve = await (async () => {
461
- if (isFullRefetch) {
462
- if (isTraceActive()) {
463
- pushRevalidationTraceEntry({
464
- segmentId: parallelId,
465
- segmentType: "parallel",
466
- belongsToRoute,
467
- source: "parallel",
468
- defaultShouldRevalidate: true,
469
- finalShouldRevalidate: true,
470
- reason: "full-refetch",
471
- });
472
- }
473
- return true;
474
- }
478
+ let shouldResolve: boolean;
479
+ if (isFullRefetch) {
480
+ // Client has nothing cached — slot MUST render. User revalidate fns are
481
+ // bypassed here because returning false would leave the segment blank
482
+ // with no client-side fallback.
483
+ traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
484
+ shouldResolve = true;
485
+ } else {
486
+ // For non-empty client sets, consult user revalidate fns. When the slot
487
+ // is unknown to the client, override the type-derived default so the
488
+ // soft chain seeds with the right "new segment" / "parent-chain" value.
489
+ let defaultOverride: { value: boolean; reason: string } | undefined;
475
490
  if (!clientSegmentIds.has(parallelId)) {
476
- const result = belongsToRoute || isNewParent;
477
- if (isTraceActive()) {
478
- pushRevalidationTraceEntry({
479
- segmentId: parallelId,
480
- segmentType: "parallel",
481
- belongsToRoute,
482
- source: "parallel",
483
- defaultShouldRevalidate: result,
484
- finalShouldRevalidate: result,
485
- reason: result ? "new-segment" : "skip-parent-chain",
486
- });
487
- }
488
- return result;
491
+ const value = belongsToRoute || isNewParent;
492
+ defaultOverride = {
493
+ value,
494
+ reason: value ? "new-segment" : "skip-parent-chain",
495
+ };
489
496
  }
490
497
 
491
498
  const dummySegment: ResolvedSegment = {
@@ -503,7 +510,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
503
510
  : {}),
504
511
  };
505
512
 
506
- return await evaluateRevalidation({
513
+ shouldResolve = await evaluateRevalidation({
507
514
  segment: dummySegment,
508
515
  prevParams,
509
516
  getPrevSegment: null,
@@ -519,8 +526,9 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
519
526
  actionContext,
520
527
  stale,
521
528
  traceSource: "parallel",
529
+ defaultOverride,
522
530
  });
523
- })();
531
+ }
524
532
  emitRevalidationDecision(
525
533
  parallelId,
526
534
  context.pathname,
@@ -868,7 +876,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
868
876
  prevUrl,
869
877
  nextUrl,
870
878
  routeKey,
871
- loaderPromises,
872
879
  true,
873
880
  deps,
874
881
  actionContext,
@@ -953,7 +960,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
953
960
  prevUrl,
954
961
  nextUrl,
955
962
  routeKey,
956
- loaderPromises,
957
963
  false,
958
964
  deps,
959
965
  actionContext,
@@ -980,7 +986,6 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
980
986
  prevUrl: URL,
981
987
  nextUrl: URL,
982
988
  routeKey: string,
983
- loaderPromises: Map<string, Promise<any>>,
984
989
  belongsToRoute: boolean,
985
990
  deps: SegmentResolutionDeps<TEnv>,
986
991
  actionContext?: ActionContext,
@@ -1166,21 +1171,20 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1166
1171
  const parallelId = `${orphan.shortCode}.${slot}`;
1167
1172
  matchedIds.push(parallelId);
1168
1173
 
1169
- const shouldResolve = await (async () => {
1170
- if (!clientSegmentIds.has(parallelId)) {
1171
- if (isTraceActive()) {
1172
- pushRevalidationTraceEntry({
1173
- segmentId: parallelId,
1174
- segmentType: "parallel",
1175
- belongsToRoute,
1176
- source: "parallel",
1177
- defaultShouldRevalidate: true,
1178
- finalShouldRevalidate: true,
1179
- reason: "new-segment",
1180
- });
1181
- }
1182
- return true;
1183
- }
1174
+ const isFullRefetch = clientSegmentIds.size === 0;
1175
+ let shouldResolve: boolean;
1176
+ if (isFullRefetch) {
1177
+ // Same load-bearing rationale as the main parallel path: full refetch
1178
+ // means the client has nothing to fall back to, so the slot must render.
1179
+ traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
1180
+ shouldResolve = true;
1181
+ } else {
1182
+ // When slot is unknown to the client, seed the soft chain with `true`
1183
+ // (orphan parallels always belong to the route — we want them rendered
1184
+ // unless the user explicitly opts out via revalidate()).
1185
+ const defaultOverride = clientSegmentIds.has(parallelId)
1186
+ ? undefined
1187
+ : { value: true, reason: "new-segment" };
1184
1188
 
1185
1189
  const dummySegment: ResolvedSegment = {
1186
1190
  id: parallelId,
@@ -1197,7 +1201,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1197
1201
  : {}),
1198
1202
  };
1199
1203
 
1200
- return await evaluateRevalidation({
1204
+ shouldResolve = await evaluateRevalidation({
1201
1205
  segment: dummySegment,
1202
1206
  prevParams,
1203
1207
  getPrevSegment: null,
@@ -1213,8 +1217,9 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1213
1217
  actionContext,
1214
1218
  stale,
1215
1219
  traceSource: "parallel",
1220
+ defaultOverride,
1216
1221
  });
1217
- })();
1222
+ }
1218
1223
  emitRevalidationDecision(
1219
1224
  parallelId,
1220
1225
  context.pathname,