@rangojs/router 0.0.0-experimental.78adc454 → 0.0.0-experimental.7dc955ec
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/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +8 -0
- package/skills/route/SKILL.md +31 -0
- package/skills/typesafety/SKILL.md +10 -0
- package/src/router/segment-resolution/fresh.ts +14 -4
- package/src/router/segment-resolution/revalidation.ts +9 -0
- package/src/server/context.ts +2 -1
- package/src/server/request-context.ts +1 -2
package/dist/vite/index.js
CHANGED
|
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
|
|
|
1745
1745
|
// package.json
|
|
1746
1746
|
var package_default = {
|
|
1747
1747
|
name: "@rangojs/router",
|
|
1748
|
-
version: "0.0.0-experimental.
|
|
1748
|
+
version: "0.0.0-experimental.7dc955ec",
|
|
1749
1749
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1750
1750
|
keywords: [
|
|
1751
1751
|
"react",
|
package/package.json
CHANGED
|
@@ -162,6 +162,38 @@ middleware(async (ctx, next) => {
|
|
|
162
162
|
});
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
## Context Variable Cache Safety
|
|
166
|
+
|
|
167
|
+
Context variables created with `createVar()` are cacheable by default and can
|
|
168
|
+
be read freely inside `cache()` and `"use cache"` scopes. Non-cacheable vars
|
|
169
|
+
throw at read time to prevent request-specific data from being captured.
|
|
170
|
+
|
|
171
|
+
There are two ways to mark a value as non-cacheable:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// Var-level policy — inherently request-specific data
|
|
175
|
+
const Session = createVar<SessionData>({ cache: false });
|
|
176
|
+
|
|
177
|
+
// Write-level escalation — this specific write is non-cacheable
|
|
178
|
+
ctx.set(Theme, derivedTheme, { cache: false });
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
"Least cacheable wins": if either the var definition or the `ctx.set()` call
|
|
182
|
+
specifies `cache: false`, the value is non-cacheable.
|
|
183
|
+
|
|
184
|
+
**Behavior inside cache scopes:**
|
|
185
|
+
|
|
186
|
+
| Operation | Inside `cache()` / `"use cache"` |
|
|
187
|
+
| ----------------------------------- | -------------------------------- |
|
|
188
|
+
| `ctx.get(cacheableVar)` | Allowed |
|
|
189
|
+
| `ctx.get(nonCacheableVar)` | Throws |
|
|
190
|
+
| `ctx.set(var, value)` (cacheable) | Allowed |
|
|
191
|
+
| `ctx.header()`, `ctx.cookie()`, etc | Throws (response side effects) |
|
|
192
|
+
|
|
193
|
+
Write is dumb — `ctx.set()` stores the cache metadata but does not enforce.
|
|
194
|
+
Enforcement happens at read time (`ctx.get()`), where ALS detects the cache
|
|
195
|
+
scope and rejects non-cacheable reads.
|
|
196
|
+
|
|
165
197
|
## Loaders Are Always Fresh
|
|
166
198
|
|
|
167
199
|
Loaders are **never cached** by route-level `cache()`. Even on a full cache hit
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -173,6 +173,14 @@ const router = createRouter<AppBindings>({
|
|
|
173
173
|
KV entries require `expirationTtl >= 60s`. Short-lived entries (< 60s total TTL)
|
|
174
174
|
are only cached in L1.
|
|
175
175
|
|
|
176
|
+
## Context Variables Inside Cache Boundaries
|
|
177
|
+
|
|
178
|
+
Context variables (`createVar`) are cacheable by default and can be read and
|
|
179
|
+
written inside `cache()` scopes. Variables marked with `{ cache: false }` (at
|
|
180
|
+
the var level or write level) throw when read inside a cache scope. Response
|
|
181
|
+
side effects (`ctx.header()`, `ctx.cookie()`) always throw inside cache
|
|
182
|
+
boundaries. See `/cache-guide` for the full cache safety table.
|
|
183
|
+
|
|
176
184
|
## Nested Cache Boundaries
|
|
177
185
|
|
|
178
186
|
Override cache settings for specific sections:
|
package/skills/route/SKILL.md
CHANGED
|
@@ -181,6 +181,37 @@ String keys still work (`ctx.set("key", value)` / `ctx.get("key")`), but
|
|
|
181
181
|
Only route handlers and middleware can call `ctx.set()`. Layouts, parallels,
|
|
182
182
|
and intercepts can only read via `ctx.get()`.
|
|
183
183
|
|
|
184
|
+
#### Non-cacheable context variables
|
|
185
|
+
|
|
186
|
+
Mark a var as non-cacheable when it holds inherently request-specific data
|
|
187
|
+
(sessions, auth tokens, per-request IDs). There are two ways:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// Var-level: every value written to this var is non-cacheable
|
|
191
|
+
const Session = createVar<SessionData>({ cache: false });
|
|
192
|
+
|
|
193
|
+
// Write-level: escalate a normally-cacheable var for this specific write
|
|
194
|
+
const Theme = createVar<string>();
|
|
195
|
+
ctx.set(Theme, userTheme, { cache: false });
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
"Least cacheable wins" — if either the var definition or the write site says
|
|
199
|
+
`cache: false`, the value is non-cacheable.
|
|
200
|
+
|
|
201
|
+
Reading a non-cacheable var inside `cache()` or `"use cache"` throws at
|
|
202
|
+
runtime. This prevents request-specific data from leaking into cached output:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// This throws — Session is non-cacheable
|
|
206
|
+
async function CachedWidget(ctx) {
|
|
207
|
+
"use cache";
|
|
208
|
+
const session = ctx.get(Session); // Error: non-cacheable var read inside cache scope
|
|
209
|
+
return <Widget />;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Cacheable vars (the default) can be read freely inside cache scopes.
|
|
214
|
+
|
|
184
215
|
### Revalidation Contracts for Handler Data
|
|
185
216
|
|
|
186
217
|
Handler-first guarantees apply within a single full render pass. For partial
|
|
@@ -369,8 +369,18 @@ interface PaginationData {
|
|
|
369
369
|
perPage: number;
|
|
370
370
|
}
|
|
371
371
|
export const Pagination = createVar<PaginationData>();
|
|
372
|
+
|
|
373
|
+
// Non-cacheable var — reading inside cache() or "use cache" throws at runtime
|
|
374
|
+
const Session = createVar<SessionData>({ cache: false });
|
|
372
375
|
```
|
|
373
376
|
|
|
377
|
+
`createVar` accepts an optional options object. The `cache` option (default
|
|
378
|
+
`true`) controls whether the var's values can be read inside cache scopes.
|
|
379
|
+
Write-level escalation is also supported: `ctx.set(Var, value, { cache: false })`
|
|
380
|
+
marks a specific write as non-cacheable even if the var itself is cacheable.
|
|
381
|
+
"Least cacheable wins" — if either says `cache: false`, the value throws on
|
|
382
|
+
read inside `cache()` or `"use cache"`.
|
|
383
|
+
|
|
374
384
|
### Producer (handler or middleware)
|
|
375
385
|
|
|
376
386
|
```typescript
|
|
@@ -93,6 +93,14 @@ export async function resolveLoaders<TEnv>(
|
|
|
93
93
|
const loaderEntries = entry.loader ?? [];
|
|
94
94
|
if (loaderEntries.length === 0) return [];
|
|
95
95
|
|
|
96
|
+
// DSL loaders are always fresh (never cached), so temporarily clear the
|
|
97
|
+
// cache scope flag. This allows loaders to read non-cacheable vars even
|
|
98
|
+
// inside cache() boundaries. Handler ctx.use(loader) does NOT get this
|
|
99
|
+
// exemption — the handler is cached, so its loader results are too.
|
|
100
|
+
const store = RSCRouterContext.getStore();
|
|
101
|
+
const savedCacheScope = store?.insideCacheScope;
|
|
102
|
+
if (store) store.insideCacheScope = false;
|
|
103
|
+
|
|
96
104
|
const shortCode = shortCodeOverride ?? entry.shortCode;
|
|
97
105
|
const hasLoading = "loading" in entry && entry.loading !== undefined;
|
|
98
106
|
const loadingDisabled = hasLoading && entry.loading === false;
|
|
@@ -100,9 +108,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
100
108
|
|
|
101
109
|
if (!loadingDisabled) {
|
|
102
110
|
// Streaming loaders: promises kick off now, settle during RSC serialization.
|
|
103
|
-
|
|
104
|
-
// RSC/SSR stream consumption, after the perf timeline is logged.
|
|
105
|
-
return loaderEntries.map((loaderEntry, i) => {
|
|
111
|
+
const segments = loaderEntries.map((loaderEntry, i) => {
|
|
106
112
|
const { loader } = loaderEntry;
|
|
107
113
|
const segmentId = `${shortCode}D${i}.${loader.$$id}`;
|
|
108
114
|
return {
|
|
@@ -122,16 +128,20 @@ export async function resolveLoaders<TEnv>(
|
|
|
122
128
|
belongsToRoute,
|
|
123
129
|
};
|
|
124
130
|
});
|
|
131
|
+
// Restore cache scope after all loader promises are kicked off
|
|
132
|
+
if (store) store.insideCacheScope = savedCacheScope;
|
|
133
|
+
return segments;
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
// Loading disabled: still start all loaders in parallel, but only emit
|
|
128
137
|
// settled promises so handlers don't stream loading placeholders.
|
|
129
|
-
// We can measure actual execution time here since we await all loaders.
|
|
130
138
|
const pendingLoaderData = loaderEntries.map((loaderEntry) => {
|
|
131
139
|
const start = performance.now();
|
|
132
140
|
const promise = resolveLoaderData(loaderEntry, ctx, ctx.pathname);
|
|
133
141
|
return { promise, start, loaderId: loaderEntry.loader.$$id };
|
|
134
142
|
});
|
|
143
|
+
// Restore cache scope after all loader promises are kicked off
|
|
144
|
+
if (store) store.insideCacheScope = savedCacheScope;
|
|
135
145
|
await Promise.all(pendingLoaderData.map((p) => p.promise));
|
|
136
146
|
|
|
137
147
|
return loaderEntries.map((loaderEntry, i) => {
|
|
@@ -146,6 +146,12 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
146
146
|
const loaderEntries = entry.loader ?? [];
|
|
147
147
|
if (loaderEntries.length === 0) return { segments: [], matchedIds: [] };
|
|
148
148
|
|
|
149
|
+
// DSL loaders are always fresh — temporarily clear cache scope
|
|
150
|
+
// so non-cacheable var reads are allowed inside loader functions.
|
|
151
|
+
const store = RSCRouterContext.getStore();
|
|
152
|
+
const savedCacheScope = store?.insideCacheScope;
|
|
153
|
+
if (store) store.insideCacheScope = false;
|
|
154
|
+
|
|
149
155
|
const shortCode = shortCodeOverride ?? entry.shortCode;
|
|
150
156
|
|
|
151
157
|
const loaderMeta = loaderEntries.map((loaderEntry, i) => ({
|
|
@@ -242,6 +248,9 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
242
248
|
}),
|
|
243
249
|
);
|
|
244
250
|
|
|
251
|
+
// Restore cache scope after all loader promises are kicked off
|
|
252
|
+
if (store) store.insideCacheScope = savedCacheScope;
|
|
253
|
+
|
|
245
254
|
return { segments, matchedIds };
|
|
246
255
|
}
|
|
247
256
|
|
package/src/server/context.ts
CHANGED
|
@@ -672,7 +672,8 @@ export function track(label: string, depth?: number): () => void {
|
|
|
672
672
|
|
|
673
673
|
/**
|
|
674
674
|
* Check if the current execution is inside a cache() DSL boundary.
|
|
675
|
-
*
|
|
675
|
+
* Returns false inside loader execution — loaders are always fresh
|
|
676
|
+
* (never cached), so non-cacheable reads are safe.
|
|
676
677
|
*/
|
|
677
678
|
export function isInsideCacheScope(): boolean {
|
|
678
679
|
return RSCRouterContext.getStore()?.insideCacheScope === true;
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from "../context-var.js";
|
|
30
30
|
import { createHandleStore, type HandleStore } from "./handle-store.js";
|
|
31
31
|
import { isHandle } from "../handle.js";
|
|
32
|
-
import { track, type MetricsStore } from "./context.js";
|
|
32
|
+
import { track, RSCRouterContext, type MetricsStore } from "./context.js";
|
|
33
33
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
34
34
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
35
35
|
import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
@@ -942,7 +942,6 @@ export function createUseFunction<TEnv>(
|
|
|
942
942
|
),
|
|
943
943
|
};
|
|
944
944
|
|
|
945
|
-
// Start loader execution with tracking
|
|
946
945
|
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
947
946
|
const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
|
|
948
947
|
doneLoader();
|