@rangojs/router 0.1.1 → 0.2.0

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 (48) hide show
  1. package/README.md +2 -1
  2. package/dist/types/cache/document-cache.d.ts +3 -1
  3. package/dist/types/cache/types.d.ts +4 -3
  4. package/dist/types/route-definition/helpers-types.d.ts +6 -6
  5. package/dist/types/router/match-handlers.d.ts +2 -3
  6. package/dist/types/router/segment-resolution/view-transition-default.d.ts +3 -4
  7. package/dist/types/router/transition-when.d.ts +13 -0
  8. package/dist/types/rsc/shell-serve.d.ts +2 -0
  9. package/dist/types/rsc/transition-gate.d.ts +10 -14
  10. package/dist/types/server/request-context.d.ts +5 -1
  11. package/dist/types/testing/e2e/index.d.ts +1 -1
  12. package/dist/types/testing/index.d.ts +2 -2
  13. package/dist/types/testing/run-transition-when.d.ts +6 -5
  14. package/dist/types/testing/shell-status.d.ts +23 -3
  15. package/dist/types/types/segments.d.ts +27 -22
  16. package/dist/types/urls/path-helper-types.d.ts +4 -3
  17. package/dist/vite/index.js +1 -1
  18. package/package.json +20 -21
  19. package/skills/cache-guide/SKILL.md +6 -3
  20. package/skills/catalog.json +7 -1
  21. package/skills/deployment-caching/SKILL.md +176 -0
  22. package/skills/document-cache/SKILL.md +30 -3
  23. package/skills/ppr/SKILL.md +57 -25
  24. package/skills/prerender/SKILL.md +15 -8
  25. package/skills/rango/SKILL.md +20 -17
  26. package/skills/testing/SKILL.md +1 -1
  27. package/skills/testing/cache-prerender.md +5 -1
  28. package/skills/vercel/SKILL.md +22 -1
  29. package/skills/view-transitions/SKILL.md +12 -8
  30. package/src/cache/cache-scope.ts +6 -1
  31. package/src/cache/document-cache.ts +4 -2
  32. package/src/cache/types.ts +4 -3
  33. package/src/route-definition/helpers-types.ts +6 -6
  34. package/src/router/match-handlers.ts +44 -6
  35. package/src/router/match-middleware/cache-lookup.ts +10 -3
  36. package/src/router/segment-resolution/view-transition-default.ts +9 -5
  37. package/src/router/transition-when.ts +76 -0
  38. package/src/rsc/progressive-enhancement.ts +9 -14
  39. package/src/rsc/rsc-rendering.ts +142 -31
  40. package/src/rsc/shell-serve.ts +3 -0
  41. package/src/rsc/transition-gate.ts +37 -40
  42. package/src/server/request-context.ts +6 -0
  43. package/src/testing/e2e/index.ts +5 -0
  44. package/src/testing/index.ts +9 -1
  45. package/src/testing/run-transition-when.ts +42 -9
  46. package/src/testing/shell-status.ts +92 -3
  47. package/src/types/segments.ts +27 -22
  48. package/src/urls/path-helper-types.ts +4 -3
@@ -209,9 +209,8 @@ interface TransitionConfig {
209
209
  default?: string | Record<string, string>; // fallback for any phase
210
210
  name?: string; // explicit view-transition-name
211
211
  viewTransition?: "auto" | false; // boundary opt-out (see below)
212
- // Conditional gate, evaluated server-side AFTER the route handler. Return
213
- // false to drop this transition for the request, so the navigation streams its
214
- // loading() fallback instead of holding. See the gate section below.
212
+ // Conditional server-side gate. PPR routes run it before route handlers and
213
+ // on every replay; other routes run it after handlers on fresh resolution.
215
214
  when?: (ctx: TransitionWhenContext) => boolean;
216
215
  }
217
216
  ```
@@ -223,14 +222,19 @@ interface TransitionConfig {
223
222
 
224
223
  ## Conditional transitions (`when`)
225
224
 
226
- `transition({ when })` gates the hold per request. The predicate runs **server-side, AFTER the route handler** and outside any cache scope; return `false` to drop this segment's transition for the request (the navigation streams its `loading()` fallback instead of holding).
225
+ `transition({ when })` gates the hold per request. The predicate runs server-side and outside any cache scope; return `false` to drop this segment's transition for the request (the navigation streams its `loading()` fallback instead of holding).
227
226
 
228
- Its context mirrors the `revalidate()` predicate args — the same navigation/action metadata — plus `get`/`env` for post-handler reads:
227
+ Timing follows the route's rendering contract:
228
+
229
+ - On an ordinary route it runs after the route handler during fresh resolution, so `get()` can read handler- and middleware-set context. Cache/prerender hits replay the stored decision.
230
+ - On a `ppr` route it is automatically hoisted before route handlers and runs on every match, including runtime-cache, prerender, document-shell, and partial-navigation replay. It can read URL/params/action metadata, `env`, and middleware-set context, but not values set by route handlers. This is what keeps the handler-free PPR fast path available without a second API.
231
+
232
+ Its context mirrors the `revalidate()` predicate args — the same navigation/action metadata — plus `get`/`env` for request-context reads:
229
233
 
230
234
  ```ts
231
235
  import type { TransitionWhenContext } from "@rangojs/router";
232
236
 
233
- // Hold only when the handler marked this request (handler sets, gate reads):
237
+ // Ordinary route: hold only when the handler marked this request:
234
238
  transition({ when: (ctx) => ctx.get(KeepScroll) === true });
235
239
 
236
240
  // Hold only when arriving from a specific page (the navigation SOURCE):
@@ -252,13 +256,13 @@ transition({
252
256
  | `toRouteName` (and `fromRouteName`) | route **name** | when the route is named (undefined for unnamed/auto-generated) |
253
257
  | `actionId` / `actionUrl` / `actionResult` / `formData` | the server action that triggered this render | action-triggered renders only |
254
258
  | `method` | `"GET"` (nav) / `"POST"` (action) | always |
255
- | `get` / `env` | read handler/middleware vars + app env | always |
259
+ | `get` / `env` | read request vars + app env | always; PPR timing exposes middleware vars, not handler writes |
256
260
 
257
261
  A predicate that throws is reported to `router.onError` (phase `"rendering"`) and treated as no-hold (conservative).
258
262
 
259
263
  **Same-route content-holds need the transition present on the FIRST render.** The same-route hold works by giving the route a param-agnostic key so a param change reconciles instead of remounting — but that key is established when the route first mounts. A source gate that returns `false` on the initial full load (where `currentUrl`/`currentParams`/`fromRouteName` are undefined) drops the transition before the route mounts, so the route mounts _outside_ a transition scope and **every** later same-route param nav remounts (flashing the skeleton) regardless of what the gate decides on those navs. Write source gates so they hold when there is no source — e.g. `({ currentParams }) => currentParams?.tab !== "raw"` (true on the initial load) rather than `=== "details"` (false on the initial load) — when the same-route content-hold must engage. This only affects same-route param navigations; action-only or cross-route gating is unaffected (no shared param key is in play).
260
264
 
261
- **Prefetch / cache caveat.** The gate runs during resolution, so a **prefetched** navigation decides at prefetch time — `currentUrl`/`currentParams`/`fromRouteName` reflect the page the prefetch fired from, not necessarily the click-time source and a `cache()`/prerender hit replays the stored transition without re-running the predicate. A source-sensitive gate can therefore be frozen to prefetch/store-time state. This covers ~99% of navigations; if yours must reflect the exact click-time source, source-scope the prefetch (`<Link prefetchKey=":source">`) and don't `cache()` that segment.
265
+ **Prefetch / cache caveat.** A **prefetched** navigation still decides at prefetch time — `currentUrl`/`currentParams`/`fromRouteName` reflect the page the prefetch fired from, not necessarily the click-time source. Non-PPR `cache()`/prerender hits also replay the stored transition without rerunning the predicate. PPR routes rerun it on the server for each cache/prerender/PPR match, but a completed browser prefetch still carries its earlier Flight decision. If the exact click-time source matters, source-scope the prefetch (`<Link prefetchKey=":source">`).
262
266
 
263
267
  ## Opting out of the router boundary (place your own `<ViewTransition>`)
264
268
 
@@ -108,6 +108,8 @@ function getDefaultRouteCacheKey(
108
108
  // CacheScope
109
109
  // ============================================================================
110
110
 
111
+ const CACHE_HIT_OBSERVERS = new WeakMap<CacheScope, () => void>();
112
+
111
113
  /**
112
114
  * CacheScope represents a cache boundary in the route tree.
113
115
  *
@@ -348,6 +350,7 @@ export class CacheScope {
348
350
  );
349
351
  }
350
352
 
353
+ CACHE_HIT_OBSERVERS.get(this)?.();
351
354
  return { segments, shouldRevalidate };
352
355
  } catch (error) {
353
356
  // Covers a store.get() failure AND a throwing consumer key()/keyGenerator
@@ -555,9 +558,11 @@ export function resolveShellImplicitCacheScope(
555
558
  if (scope) return scope;
556
559
  const marker = getRequestContext()?._shellImplicitCache;
557
560
  if (!marker) return null;
558
- return new CacheScope(
561
+ const implicitScope = new CacheScope(
559
562
  { ttl: marker.ttl, swr: marker.swr, store: marker.store },
560
563
  null,
561
564
  marker.keyPrefix,
562
565
  );
566
+ if (marker.onHit) CACHE_HIT_OBSERVERS.set(implicitScope, marker.onHit);
567
+ return implicitScope;
563
568
  }
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Document-Level Cache Middleware
3
3
  *
4
- * Caches full HTTP responses at the edge based on Cache-Control headers.
4
+ * Caches full HTTP responses in the configured app store based on Cache-Control
5
+ * headers. A deployment CDN may independently consume the same shared-cache
6
+ * directives; this middleware itself runs inside the worker/function.
5
7
  * Routes opt-in to caching by setting s-maxage or stale-while-revalidate headers.
6
8
  *
7
9
  * Flow:
@@ -58,7 +60,7 @@ function parseCacheControl(header: string | null): CacheDirectives | null {
58
60
 
59
61
  // RFC 7234: in a SHARED cache, `private` and `no-store` forbid storage and
60
62
  // MUST win over `s-maxage` even though `private, s-maxage` is contradictory.
61
- // The document cache is a shared edge store, so refuse both regardless of any
63
+ // The document cache is a shared app store, so refuse both regardless of any
62
64
  // s-maxage / stale-while-revalidate also present. Match standalone directive
63
65
  // tokens (start/end, whitespace, comma, semicolon, or `=` bounded), not a
64
66
  // substring, so a value containing "private" cannot false-veto.
@@ -319,9 +319,10 @@ export interface ShellCacheEntry {
319
319
  */
320
320
  handlerLiveHoles?: boolean;
321
321
  /**
322
- * The capture encountered transition({ when }). Its effective hold policy is
323
- * request-dependent, so handler-free document and navigation replay must
324
- * re-run resolution to collect and evaluate the predicate.
322
+ * Legacy/fallback marker for a transition({ when }) predicate that was not
323
+ * evaluated by the PPR pre-handler gate. Such an entry stays conservatively
324
+ * ineligible for handler-free replay. New PPR matches evaluate known segment
325
+ * predicates before the pipeline and do not set this marker.
325
326
  */
326
327
  transitionWhen?: true;
327
328
  /** Capture-generation start time; tag invalidations at or after it win. */
@@ -460,11 +460,11 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
460
460
  * startTransition only when the router sets viewTransition: false.
461
461
  *
462
462
  * Conditional hold: pass `when: (ctx) => boolean` to gate the transition per
463
- * request. It runs server-side AFTER the route handler (so it can read state
464
- * the handler set via `ctx.get(...)`); returning false drops this transition
465
- * for the request, so the navigation streams its loading() skeleton instead of
466
- * holding. This is a post-handler predicate distinct from intercept()'s
467
- * match-time `when` config selector (`intercept(slot, route, Comp, { when })`).
463
+ * request. It normally runs server-side after the route handler. On a `ppr`
464
+ * route it is automatically hoisted before route handlers and reevaluated on
465
+ * every replay, so it may read URL/params/action/env and middleware-set context
466
+ * but not handler-set context. Returning false drops this transition for the
467
+ * request. This is distinct from intercept()'s match-time `when` selector.
468
468
  *
469
469
  * ```typescript
470
470
  * // Attach to a single route
@@ -491,7 +491,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
491
491
  * @param config - ViewTransition configuration (enter, exit, update, share,
492
492
  * default, name), `viewTransition: "auto" | false` to toggle the router
493
493
  * boundary (createRouter({ viewTransition }) sets the app-wide default), and
494
- * `when: (ctx) => boolean` to gate the transition per request post-handler
494
+ * `when: (ctx) => boolean` to gate the transition per request
495
495
  * @param children - Optional callback returning child routes to wrap
496
496
  */
497
497
  transition: {
@@ -1,12 +1,19 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
3
3
  import { sanitizeError } from "../errors";
4
- import type { ErrorInfo, ErrorPhase, MatchResult } from "../types";
5
4
  import type {
6
- EntryData,
7
- InterceptEntry,
8
- InterceptSelectorContext,
9
- } from "../server/context";
5
+ ErrorBoundaryHandler,
6
+ ErrorInfo,
7
+ ErrorPhase,
8
+ MatchResult,
9
+ NotFoundBoundaryHandler,
10
+ } from "../types";
11
+ import {
12
+ isPprEntry,
13
+ type EntryData,
14
+ type InterceptEntry,
15
+ type InterceptSelectorContext,
16
+ } from "../server/context.js";
10
17
  import type { MatchApiDeps } from "./types.js";
11
18
  import type { RouterContext } from "./router-context.js";
12
19
  import { runWithRouterContext } from "./router-context.js";
@@ -30,7 +37,6 @@ import {
30
37
  startRevalidationTrace,
31
38
  flushRevalidationTrace,
32
39
  } from "./logging.js";
33
- import type { ErrorBoundaryHandler, NotFoundBoundaryHandler } from "../types";
34
40
  import type { MiddlewareFn } from "./middleware.js";
35
41
  import {
36
42
  type TelemetrySink,
@@ -41,6 +47,7 @@ import {
41
47
  buildCacheSignalSegments,
42
48
  } from "./telemetry.js";
43
49
  import { _getRequestContext } from "../server/request-context.js";
50
+ import { evaluatePprTransitionWhen } from "./transition-when.js";
44
51
 
45
52
  /**
46
53
  * Per-call telemetry lifecycle emitter for match()/matchPartial(). Each method
@@ -227,6 +234,35 @@ export function createMatchHandlers<TEnv = any>(
227
234
  if (reqCtx) reqCtx._cacheSignal = segments;
228
235
  };
229
236
 
237
+ const evaluatePprTransitionWhenForMatch = (
238
+ ctx: MatchContext<TEnv>,
239
+ isPartial: boolean,
240
+ ): void => {
241
+ const reqCtx = _getRequestContext();
242
+ if (!reqCtx) return;
243
+ reqCtx._pprTransitionDecisions = undefined;
244
+ if (!isPprEntry(ctx.manifestEntry)) return;
245
+
246
+ evaluatePprTransitionWhen(
247
+ ctx.entries,
248
+ reqCtx,
249
+ {
250
+ params: ctx.matched.params,
251
+ routeName: ctx.interceptSelectorContext.toRouteName,
252
+ },
253
+ (error, segmentId) =>
254
+ callOnError(error, "rendering", {
255
+ request: ctx.request,
256
+ url: ctx.url,
257
+ env: ctx.env,
258
+ params: ctx.matched.params,
259
+ segmentId,
260
+ isPartial,
261
+ handledByBoundary: false,
262
+ }),
263
+ );
264
+ };
265
+
230
266
  async function createMatchContextForFull(
231
267
  request: Request,
232
268
  env: TEnv,
@@ -313,6 +349,7 @@ export function createMatchHandlers<TEnv = any>(
313
349
  }
314
350
 
315
351
  const ctx = result as MatchContext<TEnv>;
352
+ evaluatePprTransitionWhenForMatch(ctx, false);
316
353
 
317
354
  try {
318
355
  const state = createPipelineState();
@@ -424,6 +461,7 @@ export function createMatchHandlers<TEnv = any>(
424
461
  emitter.end(0, false);
425
462
  return null;
426
463
  }
464
+ evaluatePprTransitionWhenForMatch(ctx, true);
427
465
 
428
466
  if (isRouterDebugEnabled()) {
429
467
  startRevalidationTrace({
@@ -460,6 +460,8 @@ export function withCacheLookup<TEnv>(
460
460
  state.shouldRevalidate = cacheResult.shouldRevalidate;
461
461
  state.cachedSegments = cacheResult.segments;
462
462
  state.cachedMatchedIds = cacheResult.segments.map((s) => s.id);
463
+ const pprTransitionDecisions =
464
+ _getRequestContext()?._pprTransitionDecisions;
463
465
 
464
466
  const canCheckSegmentRevalidation =
465
467
  !ctx.isFullMatch &&
@@ -533,8 +535,13 @@ export function withCacheLookup<TEnv>(
533
535
  reason: "cached-no-rules",
534
536
  });
535
537
  }
536
- segment.component = null;
537
- segment.loading = undefined;
538
+ // A PPR transition decision must reach the client even when this cached
539
+ // segment otherwise needs no update. Keep its replayed component so the
540
+ // partial result retains the segment for gateTransitions().
541
+ if (!pprTransitionDecisions?.has(segment.id)) {
542
+ segment.component = null;
543
+ segment.loading = undefined;
544
+ }
538
545
  yield segment;
539
546
  continue;
540
547
  }
@@ -566,7 +573,7 @@ export function withCacheLookup<TEnv>(
566
573
  shouldRevalidate,
567
574
  });
568
575
 
569
- if (!shouldRevalidate) {
576
+ if (!shouldRevalidate && !pprTransitionDecisions?.has(segment.id)) {
570
577
  segment.component = null;
571
578
  segment.loading = undefined;
572
579
  }
@@ -23,10 +23,9 @@ import { getRequestContext } from "../../server/request-context.js";
23
23
  * gate, which also avoids needless object allocation and payload growth.
24
24
  *
25
25
  * `when`: a server-only predicate. It is STRIPPED from the returned config (a
26
- * function cannot cross Flight or the segment cache) and recorded on the request
27
- * context keyed by `segmentId`, so rsc-rendering can evaluate it post-handler
28
- * outside any cache scope and drop this segment's transition when it returns
29
- * false. Used by both the fresh and revalidation resolution paths.
26
+ * function cannot cross Flight or the segment cache). PPR routes evaluate it
27
+ * from the manifest before the pipeline; other routes record it here for the
28
+ * existing post-handler gate. Used by both fresh and revalidation resolution.
30
29
  */
31
30
  export function applyViewTransitionDefault(
32
31
  transition: EntryData["transition"],
@@ -39,7 +38,12 @@ export function applyViewTransitionDefault(
39
38
  if (segmentId !== undefined) {
40
39
  try {
41
40
  const ctx = getRequestContext();
42
- (ctx._transitionWhen ??= []).push({ id: segmentId, when: result.when });
41
+ if (!ctx._pprTransitionDecisions?.has(segmentId)) {
42
+ (ctx._transitionWhen ??= []).push({
43
+ id: segmentId,
44
+ when: result.when,
45
+ });
46
+ }
43
47
  } catch {
44
48
  // No active request context (e.g. a unit test calling this util
45
49
  // directly). Skip collection; the strip below still applies so the
@@ -0,0 +1,76 @@
1
+ import { getParallelSlotEntries, type EntryData } from "../server/context.js";
2
+ import type { RequestContext } from "../server/request-context.js";
3
+ import type { TransitionWhenContext } from "../types/segments.js";
4
+
5
+ export type TransitionWhenErrorReporter = (
6
+ error: unknown,
7
+ segmentId: string,
8
+ ) => void;
9
+
10
+ export function createTransitionWhenContext<TEnv>(
11
+ ctx: RequestContext<TEnv>,
12
+ target?: {
13
+ params: Record<string, string>;
14
+ routeName?: string;
15
+ },
16
+ ): TransitionWhenContext<Record<string, string>, TEnv> {
17
+ return {
18
+ currentUrl: ctx._gateCurrentUrl,
19
+ currentParams: ctx._gateCurrentParams,
20
+ fromRouteName: ctx._prevRouteKey as TransitionWhenContext["fromRouteName"],
21
+ nextUrl: ctx.url,
22
+ nextParams: target?.params ?? ctx.params,
23
+ toRouteName: (target?.routeName ??
24
+ ctx.routeName) as TransitionWhenContext["toRouteName"],
25
+ actionId: ctx._gateActionId,
26
+ actionUrl: ctx._gateActionUrl,
27
+ actionResult: ctx._gateActionResult,
28
+ formData: ctx._gateFormData,
29
+ method: ctx.request.method,
30
+ get: ctx.get,
31
+ env: ctx.env,
32
+ };
33
+ }
34
+
35
+ /** Evaluate server-only transition gates before handlers on a PPR route. */
36
+ export function evaluatePprTransitionWhen<TEnv>(
37
+ entries: EntryData[],
38
+ ctx: RequestContext<TEnv>,
39
+ target: {
40
+ params: Record<string, string>;
41
+ routeName?: string;
42
+ },
43
+ reportError: TransitionWhenErrorReporter,
44
+ ): void {
45
+ const decisions = new Map<string, boolean>();
46
+ const whenContext = createTransitionWhenContext(ctx, target);
47
+ const visited = new Set<EntryData>();
48
+
49
+ const evaluate = (entry: EntryData, segmentId: string): void => {
50
+ const when = entry.transition?.when;
51
+ if (!when) return;
52
+ try {
53
+ decisions.set(segmentId, when(whenContext) !== false);
54
+ } catch (error) {
55
+ decisions.set(segmentId, false);
56
+ reportError(error, segmentId);
57
+ }
58
+ };
59
+
60
+ const visitEntryAndOrphanLayouts = (entry: EntryData): void => {
61
+ if (visited.has(entry)) return;
62
+ visited.add(entry);
63
+ evaluate(entry, entry.shortCode);
64
+
65
+ for (const orphan of entry.layout) visitEntryAndOrphanLayouts(orphan);
66
+ // Rendered parallel slots are leaves identified by their owning segment.
67
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
68
+ entry.parallel,
69
+ )) {
70
+ evaluate(parallelEntry, `${entry.shortCode}.${slot}`);
71
+ }
72
+ };
73
+
74
+ for (const entry of entries) visitEntryAndOrphanLayouts(entry);
75
+ ctx._pprTransitionDecisions = decisions.size > 0 ? decisions : undefined;
76
+ }
@@ -277,10 +277,15 @@ export async function handleProgressiveEnhancement<TEnv>(
277
277
  // JS/PE parity: this is an action's revalidation render, so mark it BEFORE
278
278
  // matching — a stale `foregroundOnAction` cache entry must re-execute in the
279
279
  // foreground during the re-render, exactly as the JS path's
280
- // revalidateAfterAction does. The transition({ when }) gate fields below are
281
- // set post-match (the gate reads them after rendering); foregroundOnAction
282
- // reads _inActionRevalidation during the match, so it must be set here.
283
- getRequestContext()._inActionRevalidation = true;
280
+ // revalidateAfterAction does. PPR transition({ when }) runs during match,
281
+ // while foregroundOnAction reads _inActionRevalidation there too, so all
282
+ // action metadata must be available before matching.
283
+ const peReqCtx = getRequestContext();
284
+ peReqCtx._inActionRevalidation = true;
285
+ peReqCtx._gateActionId = directActionId ?? undefined;
286
+ peReqCtx._gateActionUrl = new URL(url);
287
+ peReqCtx._gateActionResult = actionResult;
288
+ peReqCtx._gateFormData = formData;
284
289
 
285
290
  const match = await ctx.router.match(renderRequest, { env });
286
291
 
@@ -291,16 +296,6 @@ export async function handleProgressiveEnhancement<TEnv>(
291
296
  });
292
297
  }
293
298
 
294
- // Expose the no-JS action to the transition({ when }) gate. currentUrl/Params
295
- // are absent on this full-render path (no navigation snapshot); useActionState
296
- // ids are block-scoped, so only a direct action id is available here.
297
- // actionUrl is the page the action was submitted from (this request's url).
298
- const peReqCtx = getRequestContext();
299
- peReqCtx._gateActionId = directActionId ?? undefined;
300
- peReqCtx._gateActionUrl = new URL(url);
301
- peReqCtx._gateActionResult = actionResult;
302
- peReqCtx._gateFormData = formData;
303
-
304
299
  const payload: RscPayload = {
305
300
  metadata: {
306
301
  pathname: url.pathname,