@rangojs/router 0.0.0-experimental.105 → 0.0.0-experimental.106

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.
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: observability
3
+ description: Debug Rango request performance with debugPerformance, Server-Timing, structured telemetry, and tracing
4
+ argument-hint:
5
+ ---
6
+
7
+ # Observability
8
+
9
+ Use this when you need to understand request latency, cache decisions,
10
+ revalidation behavior, loader overlap, or production traces.
11
+
12
+ Rango exposes two complementary observability surfaces:
13
+
14
+ 1. **Performance timeline** (`debugPerformance`) — per-request waterfall for
15
+ local or targeted debugging. It prints to the console and emits
16
+ `Server-Timing`.
17
+ 2. **Structured telemetry** (`telemetry`) — lifecycle events sent to a pluggable
18
+ sink for production monitoring, OpenTelemetry, or custom metrics.
19
+
20
+ See `packages/rangojs-router/docs/telemetry.md` for the complete event contract.
21
+
22
+ ## Performance timeline
23
+
24
+ Enable globally while debugging:
25
+
26
+ ```typescript
27
+ import { createRouter } from "@rangojs/router";
28
+
29
+ const router = createRouter({
30
+ document: Document,
31
+ urls: urlpatterns,
32
+ debugPerformance: true,
33
+ });
34
+ ```
35
+
36
+ Or enable for selected requests from middleware:
37
+
38
+ ```typescript
39
+ middleware(async (ctx, next) => {
40
+ if (ctx.url.searchParams.has("debug")) {
41
+ ctx.debugPerformance();
42
+ }
43
+ await next();
44
+ });
45
+ ```
46
+
47
+ Call `ctx.debugPerformance()` before `await next()`. The request then prints a
48
+ shared-axis waterfall and adds a `Server-Timing` header.
49
+
50
+ Read the timeline as intervals:
51
+
52
+ - `handler:total` is the whole router request.
53
+ - `render:total` / `ssr-render-html` show the render pass.
54
+ - `loader:*` rows should overlap render work. If a loader starts only after the
55
+ render bar, it is serialized latency.
56
+ - Cache, route matching, middleware pre/post, RSC serialization, and SSR phases
57
+ appear as separate spans, so the slow phase is visible without guessing.
58
+
59
+ ## Structured telemetry
60
+
61
+ Use telemetry when you want durable production events rather than a one-request
62
+ debug waterfall.
63
+
64
+ ```typescript
65
+ import { createRouter, createConsoleSink } from "@rangojs/router";
66
+
67
+ const router = createRouter({
68
+ document: Document,
69
+ urls: urlpatterns,
70
+ telemetry: createConsoleSink(),
71
+ });
72
+ ```
73
+
74
+ For OpenTelemetry:
75
+
76
+ ```typescript
77
+ import { createRouter, createOTelSink } from "@rangojs/router";
78
+ import { trace } from "@opentelemetry/api";
79
+
80
+ const router = createRouter({
81
+ document: Document,
82
+ urls: urlpatterns,
83
+ telemetry: createOTelSink(trace.getTracer("my-app")),
84
+ });
85
+ ```
86
+
87
+ Custom sinks implement `emit(event)`:
88
+
89
+ ```typescript
90
+ import { createRouter } from "@rangojs/router";
91
+
92
+ const router = createRouter({
93
+ document: Document,
94
+ urls: urlpatterns,
95
+ telemetry: {
96
+ emit(event) {
97
+ myMetrics.record(event);
98
+ },
99
+ },
100
+ });
101
+ ```
102
+
103
+ Events include `request.start/end/error`, `loader.start/end/error`,
104
+ `handler.error`, `cache.decision`, and `revalidation.decision`.
105
+
106
+ ## Debugging revalidation and stale data
107
+
108
+ When stale UI or unexpected partial renders are the question, use all three
109
+ layers together:
110
+
111
+ ```typescript
112
+ import { createConsoleSink, createRouter } from "@rangojs/router";
113
+
114
+ const router = createRouter({
115
+ document: Document,
116
+ urls: urlpatterns,
117
+ debugPerformance: true,
118
+ telemetry: createConsoleSink(),
119
+ });
120
+ ```
121
+
122
+ Then inspect:
123
+
124
+ - `revalidation.decision` telemetry to see which segment re-ran or skipped.
125
+ - cache spans / `cache.decision` events to see hit, miss, stale, and background
126
+ revalidation behavior.
127
+ - loader spans to confirm live loaders overlap the render rather than blocking
128
+ first paint.
129
+ - the `Server-Timing` header to compare local logs with browser-network timing.
130
+
131
+ ## Zero-overhead defaults
132
+
133
+ `debugPerformance` is off by default, and `telemetry` emits nothing unless a sink
134
+ is configured. Per-request `ctx.debugPerformance()` lets you turn on the
135
+ waterfall only for the route, user, or query param you are investigating.
@@ -8,9 +8,6 @@ argument-hint: [@slot-name]
8
8
 
9
9
  Parallel routes render multiple components simultaneously in named slots.
10
10
 
11
- Canonical semantics reference:
12
- [docs/execution-model.md](../../docs/internal/execution-model.md)
13
-
14
11
  ## Basic Parallel Routes
15
12
 
16
13
  ```typescript
@@ -340,7 +337,7 @@ parallel(
340
337
  () => [
341
338
  loader(CartLoader),
342
339
  // Revalidate when cart actions occur
343
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
340
+ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
344
341
  ]
345
342
  )
346
343
  ```
@@ -364,7 +361,7 @@ the parallel consumer:
364
361
  ```typescript
365
362
  // revalidation-contracts.ts
366
363
  export const revalidateCartData = ({ actionId }) =>
367
- actionId?.includes("src/actions/cart.ts#") ?? false;
364
+ actionId?.includes("src/actions/cart.ts#") || undefined;
368
365
 
369
366
  layout(CartLayout, () => [
370
367
  revalidate(revalidateCartData), // producer reruns
@@ -482,7 +479,7 @@ export const shopPatterns = urls(({
482
479
  () => [
483
480
  loader(CartLoader),
484
481
  loading(<CartSkeleton />),
485
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
482
+ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
486
483
  ]
487
484
  ),
488
485
 
@@ -11,9 +11,6 @@ deserialization path, same segment system. The worker handles every request --
11
11
  there are NO static .html or .rsc files served from assets. The worker reads
12
12
  pre-computed Flight payloads instead of executing handler code.
13
13
 
14
- Canonical semantics reference:
15
- [docs/execution-model.md](../../docs/internal/execution-model.md)
16
-
17
14
  ## API: Prerender
18
15
 
19
16
  ### Static Route (no params)
@@ -640,16 +637,7 @@ At runtime, the cache-lookup middleware uses these flags:
640
637
 
641
638
  ## Contributor Checklist
642
639
 
643
- Before changing prerender behavior, read these docs and run these tests.
644
-
645
- ### Docs to re-read
646
-
647
- - [Prerender API design](../../docs/prerender-api-design.md) -- canonical
648
- architecture: build-time flow, runtime flow, storage, Passthrough, intercept
649
- - [Execution model](../../docs/internal/execution-model.md) -- handler-first
650
- ordering, middleware scope, context visibility rules
651
- - [Semantic change checklist](../../docs/internal/semantic-change-checklist.md)
652
- -- gate for any change to execution semantics
640
+ Before changing prerender behavior, run these tests.
653
641
 
654
642
  ### Tests to run
655
643
 
@@ -676,10 +664,3 @@ pnpm --filter @rangojs/router exec playwright test handler-first
676
664
  dev/build-only and do not need a production counterpart.
677
665
  - Behavioral assertions (rendered content, loader freshness, Passthrough
678
666
  fallback, intercept variant selection) must work in the production build.
679
-
680
- ## Maintenance References
681
-
682
- - [Stability next steps plan](../../docs/internal/stability-next-steps-plan.md)
683
- -- completed parity and cleanup pass (reference for decisions made)
684
- - [Test quality baseline](../../docs/internal/test-quality-baseline.md) --
685
- measured test inventory, sleep debt, production coverage gaps
@@ -8,35 +8,209 @@ argument-hint:
8
8
 
9
9
  Django-inspired RSC router with composable URL patterns, type-safe href, and server components.
10
10
 
11
+ This page is the mental model to read **before** the catalog. A flat list of
12
+ skills gives nothing to slot details into, so a reader free-associates from local
13
+ vocabulary — which is exactly how `revalidate()` gets misread as caching. Start
14
+ with the shape, then pick a primitive.
15
+
16
+ ## The shape of rango (read first)
17
+
18
+ - **Routes are expressed, not configured.** The `urls()` tree shows where every
19
+ route, layout, loader, and cache lives. No file-system convention, no hunting.
20
+ - **Two freshness axes, orthogonal:**
21
+ - _stored-value freshness_ — `"use cache"`, `cache()`, loader `cache()`
22
+ (SWR is first-class where the store supports it; `"use cache"` ships a
23
+ default SWR window; see `/cache-guide`)
24
+ - _client-update selection_ — `revalidate()`
25
+ - **Loaders are the live data layer** — fresh every request by default, even
26
+ inside a cached render. They run **in parallel** right after middleware and
27
+ **stream**, so data latency overlaps first paint instead of blocking it (a
28
+ cache hit streams UI instantly while loaders resolve fresh alongside). Opt into
29
+ caching explicitly. See `/loader` → "Parallel and streaming".
30
+ - **One identity, one store** — loaders, handles, cached fns, and actions are all
31
+ `path#export`; all caches share one store; `revalidateTag` cuts across them.
32
+ - **Type-safe end to end** — route names, params, search schemas, loader return
33
+ types, context vars, and `href` / `reverse` are checked at compile time
34
+ (`/typesafety`).
35
+ - **See where time goes** — turn on `debugPerformance` early (router option, or
36
+ `ctx.debugPerformance()` in middleware for per-request opt-in). It prints a
37
+ per-request waterfall + `Server-Timing` header; loaders should overlap the
38
+ render bar, not serialize after it. For production, wire `telemetry` to a
39
+ console, OpenTelemetry, or custom sink. See `/observability`.
40
+
41
+ Most features are **just-in-time**: the core is `urls()`, `path()`, `layout()`,
42
+ `include()`, and `reverse()`. Caching, parallel routes, intercepts, prerender,
43
+ i18n, themes, and the rest are opt-in — reach for them when a requirement
44
+ appears, not up front.
45
+
46
+ ## Composability: structure vs config
47
+
48
+ - `path()` / `include()` are **structure** — they define URLs and must stay
49
+ visible in `urls()`. They cannot be hidden in a factory. `include()` composes
50
+ whole modules (separation of real concerns); `path()` places a leaf.
51
+ - Everything else — `cache`, `loader`, `loading`, `middleware`, `revalidate`,
52
+ `parallel`, `intercept`, `errorBoundary`, … — is **config**. It attaches to a
53
+ node via its `use` callback, is importable, and extracts into factories that
54
+ return arrays (`withAuth()`, `withCaching()`), flattened automatically.
55
+
56
+ To decide where something can live: **does it define a URL? structure, stays in
57
+ `urls()`. Does it modify a node? config, compose freely.**
58
+
59
+ ## Pick a primitive
60
+
61
+ | I need to… | Use | Skill |
62
+ | ------------------------------------- | -------------------------------- | ----------------------- |
63
+ | render data fresh every request | `loader()` + `useLoader()` | /loader |
64
+ | cache a rendered subtree | `cache()` on a segment | /caching |
65
+ | cache one function/component's result | `"use cache"` | /use-cache |
66
+ | cache a loader's data | `loader(L, () => [cache()])` | /loader, /caching |
67
+ | re-render a segment after an action | `revalidate()` | /loader |
68
+ | mutate | `"use server"` action | /server-actions |
69
+ | debug a slow request | `debugPerformance` / telemetry | /observability |
70
+ | share config across routes | factory returning a helper array | /composability |
71
+ | compose a sub-app / module | `include()` | /route |
72
+ | modal / soft navigation | `intercept()` | /intercept |
73
+ | pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
74
+ | stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
75
+
76
+ ## Invariants
77
+
78
+ - `path()`/`include()` are always visible in `urls()`; config helpers are extractable.
79
+ - **Cache decides freshness; `revalidate()` decides client-update.** Orthogonal; compose.
80
+ - Loaders resolve fresh every request (even inside `cache()`) and never run twice/request.
81
+ - Inside `"use cache"`: `cookies()`/`headers()` and `ctx` side-effects
82
+ (`set`/`header`/`setTheme`/`onResponse`/`setLocationState`) throw; `ctx.use(Handle)`
83
+ is captured on miss and replayed on hit. (The non-cacheable read guard is a
84
+ separate `cache()`-boundary check — see the correctness bullet below.)
85
+ - One identity `path#export` (`functionId`/`$$id`/`actionId`); one store;
86
+ `revalidateTag` cuts across all cache mechanisms.
87
+ - `useLoader` / `useHandle` / `useFetchLoader` are client-only.
88
+ - Caches are correctness-first: persistent store keys are version-segmented (no
89
+ cross-deploy drift), the forward/back cache is mutation-aware, and
90
+ `createVar({ cache: false })` throws on a **direct** read inside a `cache()`
91
+ boundary (a deliberately non-propagating guard). See `/cache-guide` →
92
+ "Correctness & invalidation".
93
+ - Nested caches: the outer cache window bounds the inner — an inner shorter TTL
94
+ only applies when the enclosing cache recomputes; put a value in a loader if it
95
+ must be fresher. See `/cache-guide` → "Combining Both".
96
+
97
+ ## Don't confuse
98
+
99
+ - `revalidate()` ≠ cache invalidation — partial-render selection vs value freshness.
100
+ - host router `.lazy()` (lazy import of a handler/sub-app) vs `.map()` (inline Response).
101
+ - `cache()` (segment, in the DSL) vs `"use cache"` (function/component directive).
102
+ - `loader()` registration (server) vs `useLoader()` consumption (client).
103
+
104
+ ### Coming from another framework (false friends)
105
+
106
+ Same words, different jobs — this is the most common source of the
107
+ `revalidate()`-is-caching misread.
108
+
109
+ | You may know | Maps to Rango axis | Watch out |
110
+ | ------------------------------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
111
+ | Next.js `export const revalidate = N` | **Axis 1** (cache) | Same word, opposite meaning. Next's `revalidate` is time-based cache expiry; Rango's `revalidate()` is **axis 2**. Use `cache({ ttl })` for the Next behavior. |
112
+ | Next.js `revalidatePath` / `revalidateTag` | **Axis 1** (cache) | Cache busting. Rango's tag bust is `revalidateTag`; there is no `revalidatePath`. |
113
+ | React Router / Remix `shouldRevalidate` | **Axis 2** | This is the correct mental model for Rango's `revalidate()`. |
114
+ | HTTP `Cache-Control` / ISR | **Axis 1** | Edge/document layer — see `/document-cache`. Separate from both `cache()` and `revalidate()`. |
115
+ | Remix/RR `loader` | live data | Like Rango loaders, fresh per request — but Rango loaders run in parallel and stream (latency overlaps first paint), and can opt into caching on demand. |
116
+
117
+ See `/cache-guide` for the axis-1 decision guide, `/loader` and `/route` for
118
+ `revalidate()` (axis 2), and `/document-cache` for the edge layer.
119
+
120
+ ## Canonical shape
121
+
122
+ ```ts
123
+ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
124
+ layout(<ShopLayout />, () => [ // structure: wraps children
125
+ loader(CartLoader, () => [ // config: live data
126
+ // partial-render axis: re-run on cart actions, defer otherwise.
127
+ // ctx.isAction() matches by reference (rename-safe), not by string.
128
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
129
+ ]),
130
+ path("/shop/:slug", ProductPage, { name: "product" }, () => [ // structure: leaf
131
+ loader(ProductLoader, () => [cache({ ttl: 60 })]), // config: cache loader DATA
132
+ loading(<ProductSkeleton />), // config
133
+ withRecs(), // composed factory (config array)
134
+ ]),
135
+ ]),
136
+ ]);
137
+ ```
138
+
139
+ One tree, both axes visible: structure (`layout`/`path`) vs config (everything
140
+ else), freshness (`cache`) vs client-update (`revalidate`). Actions are matched
141
+ by reference with `ctx.isAction(Action)` (rename-safe, where `CartActions` is an
142
+ `import * as CartActions from "./actions/cart"`); see `/typesafety` → "Stable
143
+ identity".
144
+
145
+ **The source is the source of truth.** Structure, types, and update policy are
146
+ visible and local in the tree — read top-down, no hidden global model to hold in
147
+ your head. A snippet earns its place only if, from the code alone, you can answer:
148
+ _what URLs exist and who owns them?_ (composition), _can I trust this reference
149
+ without leaving the call site?_ (type-safety), _what re-renders after this
150
+ action?_ (partial rendering). If any answer needs another file, it isn't legible
151
+ yet.
152
+
11
153
  ## Skills
12
154
 
13
- | Skill | Description |
14
- | ----------------------- | -------------------------------------------------------------------------- |
15
- | `/router-setup` | Create and configure the RSC router |
16
- | `/route` | Define routes with `urls()` and `path()` |
17
- | `/layout` | Layouts that wrap child routes |
18
- | `/loader` | Data loaders with `createLoader()` |
19
- | `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
20
- | `/i18n` | Locale routing with `:locale?`, resolution chains, react-intl integration |
21
- | `/middleware` | Request processing and authentication |
22
- | `/intercept` | Modal/slide-over patterns for soft navigation |
23
- | `/parallel` | Multi-column layouts and sidebars |
24
- | `/caching` | Segment caching with memory or KV stores |
25
- | `/use-cache` | Function-level caching with `"use cache"` directive |
26
- | `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
27
- | `/document-cache` | Edge caching with Cache-Control headers |
28
- | `/theme` | Light/dark mode with FOUC prevention |
29
- | `/links` | URL generation: ctx.reverse, href, useHref, useMount, scopedReverse |
30
- | `/hooks` | Client-side React hooks |
31
- | `/typesafety` | Type-safe routes, params, href, and environment |
32
- | `/host-router` | Multi-app host routing with domain/subdomain patterns |
33
- | `/tailwind` | Set up Tailwind CSS v4 with `?url` imports |
34
- | `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |
35
- | `/mime-routes` | Content negotiation — same URL, different response types via Accept header |
36
- | `/fonts` | Load web fonts with preload hints |
37
- | `/bundle-analysis` | Audit your app's production bundle for server leaks and oversized chunks |
38
- | `/migrate-nextjs` | Migrate a Next.js App Router project to Rango |
39
- | `/migrate-react-router` | Migrate a React Router / Remix project to Rango |
155
+ Grouped by concern — read when you need to…
156
+
157
+ **Structure & routing** shape URLs, layouts, navigation, and request processing:
158
+
159
+ | Skill | Description |
160
+ | ------------------------- | -------------------------------------------------------------------------- |
161
+ | `/router-setup` | Create and configure the RSC router |
162
+ | `/route` | Define routes with `urls()`, `path()`, and `include()` |
163
+ | `/layout` | Layouts that wrap child routes |
164
+ | `/parallel` | Multi-column layouts and sidebars |
165
+ | `/intercept` | Modal/slide-over patterns for soft navigation |
166
+ | `/middleware` | Request processing and authentication |
167
+ | `/host-router` | Multi-app host routing with domain/subdomain patterns |
168
+ | `/links` | URL generation: ctx.reverse, href, useHref, useMount, scopedReverse |
169
+ | `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |
170
+ | `/mime-routes` | Content negotiation same URL, different response types via Accept header |
171
+ | `/streams-and-websockets` | SSE via `path.stream` and WebSocket upgrades via `path.any` |
172
+ | `/handler-use` | Attach default loaders/middleware to a handler via `handler.use` |
173
+ | `/composability` | Reusable route-helper factories (structure vs config) |
174
+
175
+ **Data & caching** fetch, mutate, and cache:
176
+
177
+ | Skill | Description |
178
+ | ----------------- | ----------------------------------------------------------------------- |
179
+ | `/loader` | Data loaders with `createLoader()` and `revalidate()` |
180
+ | `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
181
+ | `/caching` | Segment caching with memory or KV stores |
182
+ | `/use-cache` | Function-level caching with `"use cache"` directive |
183
+ | `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
184
+ | `/document-cache` | Edge caching with Cache-Control headers |
185
+ | `/prerender` | Pre-render route segments at build time (Passthrough live fallback) |
186
+
187
+ **Client & presentation** — build the client-side UX:
188
+
189
+ | Skill | Description |
190
+ | ------------------- | ------------------------------------------------------------------------- |
191
+ | `/hooks` | Client-side React hooks |
192
+ | `/theme` | Light/dark mode with FOUC prevention |
193
+ | `/i18n` | Locale routing with `:locale?`, resolution chains, react-intl integration |
194
+ | `/fonts` | Load web fonts with preload hints |
195
+ | `/tailwind` | Set up Tailwind CSS v4 with `?url` imports |
196
+ | `/view-transitions` | React View Transitions on layouts, routes, and parallel slots |
197
+ | `/breadcrumbs` | Built-in Breadcrumbs handle for breadcrumb navigation |
198
+
199
+ **Observability & production health**:
200
+
201
+ | Skill | Description |
202
+ | ------------------ | ------------------------------------------------------------------------ |
203
+ | `/observability` | `debugPerformance`, `Server-Timing`, structured telemetry, tracing |
204
+ | `/bundle-analysis` | Audit your app's production bundle for server leaks and oversized chunks |
205
+ | `/debug-manifest` | Inspect route manifest structure |
206
+
207
+ **Setup, types & migration**:
208
+
209
+ | Skill | Description |
210
+ | ----------------------- | ----------------------------------------------- |
211
+ | `/typesafety` | Type-safe routes, params, href, and environment |
212
+ | `/migrate-nextjs` | Migrate a Next.js App Router project to Rango |
213
+ | `/migrate-react-router` | Migrate a React Router / Remix project to Rango |
40
214
 
41
215
  ## Quick Start
42
216
 
@@ -234,14 +234,22 @@ Cacheable vars (the default) can be read freely inside cache scopes.
234
234
 
235
235
  ### Revalidation Contracts for Handler Data
236
236
 
237
+ > **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
238
+ > It decides whether this segment re-runs and streams to the client on a
239
+ > navigation or action — never whether a cached value is stale. The cache
240
+ > decides hit/miss/ttl/swr independently and never reads `revalidate()`. See
241
+ > `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
242
+
237
243
  Handler-first guarantees apply within a single full render pass. For partial
238
244
  action revalidation, define named revalidation contracts and reuse them on both
239
245
  the producer route and the consumer child segments.
240
246
 
241
247
  ```typescript
242
248
  // revalidation-contracts.ts
249
+ // Defer (|| undefined), not ?? false: a hard `false` short-circuits the chain,
250
+ // so when the same segment composes multiple contracts the later ones never run.
243
251
  export const revalidateCheckoutData = ({ actionId }) =>
244
- actionId?.includes("src/actions/checkout.ts#") ?? false;
252
+ actionId?.includes("src/actions/checkout.ts#") || undefined;
245
253
 
246
254
  path("/checkout", CheckoutPage, { name: "checkout" }, () => [
247
255
  revalidate(revalidateCheckoutData), // producer (route handler) reruns
@@ -270,9 +278,6 @@ path("/checkout", CheckoutPage, { name: "checkout" }, () => [
270
278
  ]);
271
279
  ```
272
280
 
273
- For scope/revalidation guarantees and non-guarantees, see:
274
- [docs/execution-model.md](../../docs/internal/execution-model.md)
275
-
276
281
  ## Redirects
277
282
 
278
283
  ### Basic redirect
@@ -32,35 +32,37 @@ Actions mutate state; route handlers and loaders read the latest state. After
32
32
  an action finishes, Rango performs a server-side revalidation render for the
33
33
  matched route so the UI receives fresh segment output and loader data.
34
34
 
35
- The main control point is `revalidate(({ actionId }) => ...)` on the segment
36
- that owns the data. This applies to `path()` handlers, `layout()` handlers,
37
- `parallel()` slots, `intercept()` routes, and loader registrations:
35
+ The main control point is `revalidate((ctx) => ...)` on the segment that owns
36
+ the data. Match specific actions by imported reference with `ctx.isAction()`;
37
+ use raw `actionId` only when you intentionally need path or directory matching.
38
+ This applies to `path()` handlers, `layout()` handlers, `parallel()` slots,
39
+ `intercept()` routes, and loader registrations:
38
40
 
39
41
  ```typescript
40
42
  // urls.tsx — path/layout/parallel/intercept/loader/revalidate are passed in by urls()
41
43
  import { urls } from "@rangojs/router";
44
+ import * as CartActions from "./actions/cart";
42
45
 
43
46
  export const urlpatterns = urls(({ path, loader, revalidate }) => [
44
47
  // The loader belongs to the route that consumes its data — nest it inside
45
48
  // the owning path() so the segment owns its data dependency.
46
49
  path("/cart", CartPage, { name: "cart" }, () => [
47
- revalidate(
48
- ({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
49
- ),
50
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
50
51
  loader(CartLoader, () => [
51
- revalidate(
52
- ({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
53
- ),
52
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
54
53
  ]),
55
54
  ]),
56
55
  ]);
57
56
  ```
58
57
 
59
- For module-level `"use server"` files, the `actionId` passed to every
58
+ `ctx.isAction()` resolves the imported action reference the same way the router
59
+ derives `actionId`, so it matches in both dev and production and survives action
60
+ renames/moves as type errors instead of silent substring drift.
61
+
62
+ For module-level `"use server"` files, the raw `actionId` passed to every
60
63
  server-side `revalidate()` predicate is path-bearing in the server/RSC
61
- environment in both dev and production: `src/actions/cart.ts#addToCart`. This
62
- is intentional so path, layout, parallel, intercept, and loader revalidation
63
- predicates can filter by action file, directory, or export name.
64
+ environment in both dev and production: `src/actions/cart.ts#addToCart`. This is
65
+ the escape hatch for broad filters by action file, directory, or export name.
64
66
 
65
67
  Actions and the follow-up revalidation render share one request context.
66
68
  Values written in the action with `ctx.set(MyVar, value)` or `ctx.set("key",
@@ -91,15 +93,18 @@ export async function switchTenant(tenantId: string) {
91
93
  ```typescript
92
94
  // urls.tsx
93
95
  import { urls } from "@rangojs/router";
96
+ import * as TenantActions from "./actions/tenant";
94
97
  import { ChangedTenant } from "./context";
95
98
 
96
99
  export const urlpatterns = urls(({ path, revalidate }) => [
97
100
  path("/dashboard/:tenantId", DashboardPage, { name: "dashboard" }, () => [
98
- revalidate(
99
- ({ actionId, context }) =>
100
- actionId?.startsWith("src/actions/tenant.ts#") &&
101
- context.get(ChangedTenant) === context.params.tenantId,
102
- ),
101
+ revalidate((ctx) => {
102
+ if (!ctx.isAction(TenantActions)) return undefined;
103
+ return (
104
+ ctx.context.get(ChangedTenant) === ctx.context.params.tenantId ||
105
+ undefined
106
+ );
107
+ }),
103
108
  ]),
104
109
  ]);
105
110
  ```
@@ -380,13 +385,18 @@ re-render so the UI updates. Rango runs the action, then evaluates
380
385
  `revalidate()` on matched segments and loaders. Each path, layout, parallel,
381
386
  intercept, or loader rule decides whether that piece re-renders/re-resolves.
382
387
 
383
- The `actionId` arrives as part of the revalidation context match it to
384
- scope re-runs to specific actions.
388
+ Use `ctx.isAction()` for specific actions or modules. It accepts one action,
389
+ several actions, or a namespace import (`import * as CartActions`). Pair it with
390
+ `|| undefined` for "revalidate on match, otherwise defer to defaults/downstream
391
+ rules."
385
392
 
386
393
  ```typescript
387
394
  // urls.tsx — inside the urls() callback. Nest each loader inside the path(),
388
395
  // layout(), or parallel() that owns its data so the route tree mirrors the
389
396
  // data dependencies.
397
+ import * as AccountActions from "./actions/account";
398
+ import * as CartActions from "./actions/cart";
399
+
390
400
  urls(({ path, loader, revalidate }) => [
391
401
  path("/", HomePage, { name: "home" }, () => [
392
402
  // Loader data re-runs by default after any action. Opt out with revalidate(() => false).
@@ -395,36 +405,37 @@ urls(({ path, loader, revalidate }) => [
395
405
 
396
406
  // Re-render the cart page handler AND re-resolve its loader after cart actions
397
407
  path("/cart", CartPage, { name: "cart" }, () => [
398
- revalidate(
399
- ({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
400
- ),
408
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
401
409
  loader(CartLoader, () => [
402
- revalidate(
403
- ({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
404
- ),
410
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
405
411
  ]),
406
412
  ]),
407
413
 
408
- // Re-run after any action under src/actions/account/
414
+ // Re-run after any action exported by the account actions module
409
415
  path("/account", AccountPage, { name: "account" }, () => [
410
416
  loader(AccountLoader, () => [
411
- revalidate(
412
- ({ actionId }) => actionId?.startsWith("src/actions/account/") ?? false,
413
- ),
417
+ revalidate((ctx) => ctx.isAction(AccountActions) || undefined),
414
418
  ]),
415
419
  ]),
416
420
  ]);
417
421
  ```
418
422
 
419
- `actionId` is stable per action. For actions exported from a module-level
420
- `"use server"` file, the ID is prefixed with the source file path
421
- (`src/actions/cart.ts#addToCart`), so substring matching by file path is the
422
- recommended scope. **Inline `"use server"` actions** (declared inside an RSC
423
- component) intentionally keep their hashed IDs — file paths are withheld
424
- from the client for security. If you need file-path-based revalidation
425
- predicates, define the action in a module-level `"use server"` file rather
426
- than inline. See `/loader` for the full revalidation contract (deferred
427
- returns, soft suggestions).
423
+ The raw `actionId` string stays available for broad path filters:
424
+
425
+ ```typescript
426
+ // Match any action under src/actions/account/, including modules not imported here.
427
+ revalidate(
428
+ ({ actionId }) => actionId?.startsWith("src/actions/account/") || undefined,
429
+ );
430
+ ```
431
+
432
+ For actions exported from a module-level `"use server"` file, the ID is prefixed
433
+ with the source file path (`src/actions/cart.ts#addToCart`). **Inline `"use
434
+ server"` actions** (declared inside an RSC component) intentionally keep their
435
+ hashed IDs — file paths are withheld from the client for security. If you need
436
+ file-path-based revalidation predicates, define the action in a module-level
437
+ `"use server"` file rather than inline. See `/loader` for the full revalidation
438
+ contract (deferred returns, soft suggestions).
428
439
 
429
440
  ### Cross-segment dependencies
430
441
 
@@ -434,8 +445,9 @@ stale context. Share the same `revalidate` predicate on both producer and
434
445
  consumer:
435
446
 
436
447
  ```typescript
437
- const revalidateCart = ({ actionId }) =>
438
- actionId?.startsWith("src/actions/cart.ts#") ?? false;
448
+ import * as CartActions from "./actions/cart";
449
+
450
+ const revalidateCart = (ctx) => ctx.isAction(CartActions) || undefined;
439
451
 
440
452
  urls(({ path, layout, loader, revalidate }) => [
441
453
  layout(CartLayout, () => [