@rangojs/router 0.5.0 → 0.5.2

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 (65) hide show
  1. package/README.md +5 -1
  2. package/dist/types/browser/event-controller.d.ts +6 -0
  3. package/dist/types/browser/prefetch/cache.d.ts +31 -2
  4. package/dist/types/cache/cf/cf-cache-constants.d.ts +8 -1
  5. package/dist/types/cache/cf/cf-cache-store.d.ts +15 -1
  6. package/dist/types/cache/cf/cf-cache-types.d.ts +1 -1
  7. package/dist/types/cache/cf/cf-kv-utils.d.ts +21 -0
  8. package/dist/types/handles/is-thenable.d.ts +2 -4
  9. package/dist/types/route-definition/helpers-types.d.ts +5 -4
  10. package/dist/types/rsc/helpers.d.ts +3 -0
  11. package/dist/types/rsc/render-pipeline.d.ts +9 -0
  12. package/dist/types/rsc/routine-plan.d.ts +124 -0
  13. package/dist/types/rsc/shell-capture-constants.d.ts +17 -0
  14. package/dist/types/server/request-context.d.ts +4 -1
  15. package/dist/types/static-handler.d.ts +15 -1
  16. package/dist/types/urls/path-helper-types.d.ts +6 -7
  17. package/dist/types/vite/encryption-key.d.ts +2 -0
  18. package/dist/types/vite/plugins/expose-internal-ids.d.ts +10 -0
  19. package/dist/types/vite/plugins/server-ref-hashing.d.ts +24 -0
  20. package/dist/types/vite/plugins/server-reference-pattern.d.ts +1 -0
  21. package/dist/types/vite/utils/shared-utils.d.ts +12 -0
  22. package/dist/vite/index.js +154 -57
  23. package/package.json +3 -3
  24. package/skills/caching/SKILL.md +1 -1
  25. package/skills/mime-routes/SKILL.md +3 -1
  26. package/skills/parallel/SKILL.md +1 -1
  27. package/skills/response-routes/SKILL.md +4 -2
  28. package/skills/typesafety/generated-files-and-cli.md +16 -9
  29. package/skills/typesafety/route-types.md +5 -1
  30. package/skills/use-cache/SKILL.md +47 -0
  31. package/src/browser/event-controller.ts +40 -15
  32. package/src/browser/partial-update.ts +26 -11
  33. package/src/browser/prefetch/cache.ts +53 -9
  34. package/src/browser/prefetch/fetch.ts +83 -2
  35. package/src/browser/react/NavigationProvider.tsx +7 -0
  36. package/src/cache/cache-runtime.ts +89 -15
  37. package/src/cache/cf/cf-cache-constants.ts +8 -1
  38. package/src/cache/cf/cf-cache-store.ts +41 -64
  39. package/src/cache/cf/cf-cache-types.ts +1 -1
  40. package/src/cache/cf/cf-kv-utils.ts +38 -0
  41. package/src/cache/segment-codec.ts +21 -5
  42. package/src/handles/is-thenable.ts +2 -4
  43. package/src/route-definition/helpers-types.ts +5 -3
  44. package/src/router/match-middleware/cache-lookup.ts +24 -15
  45. package/src/rsc/handler.ts +26 -5
  46. package/src/rsc/helpers.ts +13 -0
  47. package/src/rsc/progressive-enhancement.ts +247 -70
  48. package/src/rsc/render-pipeline.ts +68 -24
  49. package/src/rsc/routine-plan.ts +359 -0
  50. package/src/rsc/rsc-rendering.ts +555 -302
  51. package/src/rsc/server-action.ts +180 -71
  52. package/src/rsc/shell-capture-constants.ts +18 -0
  53. package/src/rsc/shell-capture.ts +92 -21
  54. package/src/server/request-context.ts +6 -0
  55. package/src/ssr/ssr-root.tsx +1 -0
  56. package/src/static-handler.ts +18 -2
  57. package/src/urls/path-helper-types.ts +9 -10
  58. package/src/vite/encryption-key.ts +29 -0
  59. package/src/vite/plugins/expose-action-id.ts +2 -2
  60. package/src/vite/plugins/expose-internal-ids.ts +46 -0
  61. package/src/vite/plugins/server-ref-hashing.ts +74 -0
  62. package/src/vite/plugins/server-reference-pattern.ts +10 -0
  63. package/src/vite/rango.ts +9 -0
  64. package/src/vite/router-discovery.ts +21 -2
  65. package/src/vite/utils/shared-utils.ts +12 -7
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Routine plans — the request-level extension of the render stage driver
3
+ * (docs/design/routine-plans.md; protocol lineage in render-stage-driver.md).
4
+ * A plan is a synchronous generator that EMITS instructions before their work
5
+ * runs; the runner is the only code that executes them. Two effect shapes:
6
+ *
7
+ * step(name, fn) — sequential work; the plan suspends until its result.
8
+ * handoff(name, fn) — background registration (waitUntil-shaped); completes
9
+ * at scheduling and returns nothing.
10
+ *
11
+ * Plans compose by level: `yield* scope(name, plan)` flattens a nested plan
12
+ * into the same instruction stream while the trace records the scope. Tracking
13
+ * is derived, not declared — "what is running" is the instruction in hand.
14
+ *
15
+ * Deliberate boundaries (see the design doc's "Not built yet, on purpose"):
16
+ * the runner opens no observability spans — effects own their own
17
+ * instrumentation, and the render driver keeps PHASES.ssr — and there is no
18
+ * plan.return abort/cleanup path; the render driver's cleanup rules are the
19
+ * template if a caller ever needs one.
20
+ */
21
+
22
+ import { isThenable } from "../handles/is-thenable.js";
23
+
24
+ export type RoutineEffectKind = "step" | "handoff";
25
+
26
+ export type RoutineCommand =
27
+ | { kind: "step"; name: string; execute: () => unknown }
28
+ | { kind: "handoff"; name: string; execute: () => unknown }
29
+ | { kind: "enter"; name: string }
30
+ | { kind: "exit"; outcome: "done" | "failed"; error?: unknown };
31
+
32
+ export type RoutineCommandResult =
33
+ | { kind: "step"; value: unknown }
34
+ | { kind: "handoff" }
35
+ | { kind: "enter" }
36
+ | { kind: "exit" };
37
+
38
+ /** One imperative step: yields a command, returns its typed result. */
39
+ export type RoutineStep<T> = Generator<RoutineCommand, T, RoutineCommandResult>;
40
+
41
+ /** A full plan is shaped like a step; the alias marks intent at call sites. */
42
+ export type RoutinePlan<TReturn> = RoutineStep<TReturn>;
43
+
44
+ function expectResultKind<K extends RoutineCommandResult["kind"]>(
45
+ result: RoutineCommandResult,
46
+ kind: K,
47
+ name: string,
48
+ ): Extract<RoutineCommandResult, { kind: K }> {
49
+ if (result.kind !== kind) {
50
+ throw new Error(
51
+ `[routine] step "${name}" expected ${kind} result, received ${result.kind}`,
52
+ );
53
+ }
54
+ return result as Extract<RoutineCommandResult, { kind: K }>;
55
+ }
56
+
57
+ /**
58
+ * Sequential work. The `as Awaited<T>` narrow is the trust boundary with the
59
+ * runner, which resumes with the exact value `execute` produced.
60
+ */
61
+ export function* step<T>(
62
+ name: string,
63
+ execute: () => T | Promise<T>,
64
+ ): RoutineStep<Awaited<T>> {
65
+ const result = yield { kind: "step", name, execute };
66
+ return expectResultKind(result, "step", name).value as Awaited<T>;
67
+ }
68
+
69
+ /**
70
+ * Background handoff: successful registration completes the foreground step.
71
+ * A synchronous registration failure is thrown back into the plan, matching a
72
+ * direct waitUntil/scheduler call. A returned promise settles in the background;
73
+ * its rejection marks the trace but never fails the foreground response.
74
+ *
75
+ * Settlement tracking follows the RETURNED value: a returned promise is
76
+ * observed until it settles; a scheduler that fires-and-forgets internally
77
+ * (e.g. scheduleShellCapture via runBackground) returns void, so its trace
78
+ * entry settles at the scheduling call, not at background completion. That is
79
+ * deliberate — the plan's contract ends at the handoff.
80
+ */
81
+ export function* handoff(
82
+ name: string,
83
+ execute: () => unknown,
84
+ ): RoutineStep<void> {
85
+ const result = yield { kind: "handoff", name, execute };
86
+ expectResultKind(result, "handoff", name);
87
+ }
88
+
89
+ /**
90
+ * Run a nested plan as a named scope. `yield*` flattens its instructions into
91
+ * the parent stream; enter/exit only inform the trace. The scope reports its
92
+ * terminal outcome after internal recovery has either succeeded or escaped.
93
+ */
94
+ export function* scope<TReturn>(
95
+ name: string,
96
+ plan: RoutinePlan<TReturn>,
97
+ ): RoutineStep<TReturn> {
98
+ yield { kind: "enter", name };
99
+ try {
100
+ const value = yield* plan;
101
+ assertSynchronousReturn(value);
102
+ yield { kind: "exit", outcome: "done" };
103
+ return value;
104
+ } catch (error) {
105
+ yield { kind: "exit", outcome: "failed", error };
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ export interface RoutineTraceEntry {
111
+ name: string;
112
+ kind: RoutineEffectKind | "scope";
113
+ depth: number;
114
+ /** step/scope: running -> done|failed. handoff: pending -> settled|failed. */
115
+ state: "running" | "pending" | "done" | "settled" | "failed";
116
+ startedAt: number;
117
+ endedAt?: number;
118
+ settledAt?: number;
119
+ error?: unknown;
120
+ }
121
+
122
+ export interface RoutineTrace {
123
+ readonly name: string;
124
+ readonly entries: readonly RoutineTraceEntry[];
125
+ enterScope(name: string): void;
126
+ exitScope(outcome: "done" | "failed", error?: unknown): void;
127
+ /** Scope depth entries currently record at. Bridges (e.g. render stage
128
+ * events feeding child entries under a running step) capture this once and
129
+ * pass `depth + 1` explicitly. */
130
+ currentDepth(): number;
131
+ begin(
132
+ name: string,
133
+ kind: RoutineEffectKind,
134
+ depth?: number,
135
+ ): RoutineTraceEntry;
136
+ end(entry: RoutineTraceEntry): void;
137
+ fail(entry: RoutineTraceEntry, error: unknown): void;
138
+ settle(
139
+ entry: RoutineTraceEntry,
140
+ outcome: { status: "fulfilled" } | { status: "rejected"; error: unknown },
141
+ ): void;
142
+ /** Running scopes plus the foreground instruction currently in hand. */
143
+ active(): RoutineTraceEntry[];
144
+ format(): string;
145
+ formatActive(
146
+ entries?: readonly RoutineTraceEntry[],
147
+ activeAt?: number,
148
+ ): string;
149
+ }
150
+
151
+ export function createRoutineTrace(
152
+ name: string,
153
+ now: () => number = () => performance.now(),
154
+ ): RoutineTrace {
155
+ const entries: RoutineTraceEntry[] = [];
156
+ const scopes: RoutineTraceEntry[] = [];
157
+
158
+ const push = (
159
+ name: string,
160
+ kind: RoutineTraceEntry["kind"],
161
+ state: RoutineTraceEntry["state"],
162
+ depth: number = scopes.length,
163
+ ): RoutineTraceEntry => {
164
+ const entry: RoutineTraceEntry = {
165
+ name,
166
+ kind,
167
+ depth,
168
+ state,
169
+ startedAt: now(),
170
+ };
171
+ entries.push(entry);
172
+ return entry;
173
+ };
174
+
175
+ const active = (): RoutineTraceEntry[] =>
176
+ entries.filter((entry) => entry.state === "running");
177
+
178
+ return {
179
+ name,
180
+ entries,
181
+ enterScope(name) {
182
+ scopes.push(push(name, "scope", "running"));
183
+ },
184
+ exitScope(outcome, error) {
185
+ const scope = scopes.pop();
186
+ if (scope) {
187
+ scope.state = outcome;
188
+ scope.endedAt = now();
189
+ if (outcome === "failed") scope.error = error;
190
+ }
191
+ },
192
+ currentDepth() {
193
+ return scopes.length;
194
+ },
195
+ begin(name, kind, depth) {
196
+ return push(name, kind, kind === "step" ? "running" : "pending", depth);
197
+ },
198
+ end(entry) {
199
+ entry.state = "done";
200
+ entry.endedAt = now();
201
+ },
202
+ fail(entry, error) {
203
+ entry.state = "failed";
204
+ entry.endedAt = now();
205
+ entry.error = error;
206
+ },
207
+ settle(entry, outcome) {
208
+ entry.settledAt = now();
209
+ if (outcome.status === "fulfilled") {
210
+ entry.state = "settled";
211
+ } else {
212
+ entry.state = "failed";
213
+ entry.error = outcome.error;
214
+ }
215
+ },
216
+ active,
217
+ format() {
218
+ return entries
219
+ .map((entry) => {
220
+ const indent = " ".repeat(entry.depth);
221
+ const duration =
222
+ entry.endedAt !== undefined
223
+ ? ` ${(entry.endedAt - entry.startedAt).toFixed(1)}ms`
224
+ : "";
225
+ const settled =
226
+ entry.settledAt !== undefined
227
+ ? ` [${entry.state} +${(entry.settledAt - entry.startedAt).toFixed(1)}ms]`
228
+ : entry.state === "pending"
229
+ ? " [pending]"
230
+ : "";
231
+ const status =
232
+ entry.state === "failed" && entry.settledAt === undefined
233
+ ? " FAILED"
234
+ : "";
235
+ return `${indent}${entry.kind} ${entry.name}${duration}${settled}${status}`;
236
+ })
237
+ .join("\n");
238
+ },
239
+ formatActive(activeEntries = active(), activeAt = now()) {
240
+ return activeEntries
241
+ .map((entry) => {
242
+ const indent = " ".repeat(entry.depth);
243
+ const duration = (activeAt - entry.startedAt).toFixed(1);
244
+ return `${indent}${entry.kind} ${entry.name} ${duration}ms RUNNING`;
245
+ })
246
+ .join("\n");
247
+ },
248
+ };
249
+ }
250
+
251
+ export interface RoutineTraceOwner {
252
+ _activeRoutine?: RoutineTrace;
253
+ }
254
+
255
+ export interface RoutineRunnerOptions {
256
+ trace?: RoutineTrace;
257
+ owner?: RoutineTraceOwner;
258
+ }
259
+
260
+ function assertSynchronousReturn(value: unknown): void {
261
+ if (isThenable(value)) {
262
+ throw new Error(
263
+ "[routine] plans cannot return promises; await asynchronous work with step()",
264
+ );
265
+ }
266
+ }
267
+
268
+ function observeSettlement(
269
+ handle: Promise<unknown>,
270
+ trace: RoutineTrace | undefined,
271
+ entry: RoutineTraceEntry | undefined,
272
+ ): void {
273
+ // Attaching both callbacks marks the rejection handled on this branch, so a
274
+ // fire-and-forget failure never surfaces as an unhandled rejection.
275
+ handle.then(
276
+ () => {
277
+ if (trace && entry) trace.settle(entry, { status: "fulfilled" });
278
+ },
279
+ (error) => {
280
+ if (trace && entry) {
281
+ trace.settle(entry, { status: "rejected", error });
282
+ }
283
+ },
284
+ );
285
+ }
286
+
287
+ /**
288
+ * Interpret a plan. Invariants shared with driveRscRenderPlan: a command is
289
+ * observable before its work runs, `execute` runs exactly once, results and
290
+ * errors resume the plan with exact identity, and a plan-level try/catch
291
+ * around a step is the recovery mechanism.
292
+ */
293
+ export async function runRoutine<TReturn>(
294
+ plan: RoutinePlan<TReturn>,
295
+ options: RoutineRunnerOptions = {},
296
+ ): Promise<TReturn> {
297
+ const trace = options.trace;
298
+ const owner = options.owner;
299
+ const previousTrace = owner?._activeRoutine;
300
+ if (trace && owner) owner._activeRoutine = trace;
301
+
302
+ try {
303
+ let current = plan.next();
304
+ while (!current.done) {
305
+ const command = current.value;
306
+ switch (command.kind) {
307
+ case "enter": {
308
+ trace?.enterScope(command.name);
309
+ current = plan.next({ kind: "enter" });
310
+ break;
311
+ }
312
+ case "exit": {
313
+ trace?.exitScope(command.outcome, command.error);
314
+ current = plan.next({ kind: "exit" });
315
+ break;
316
+ }
317
+ case "step": {
318
+ const entry = trace?.begin(command.name, "step");
319
+ let value: unknown;
320
+ try {
321
+ value = await command.execute();
322
+ } catch (error) {
323
+ if (trace && entry) trace.fail(entry, error);
324
+ // Throw INTO the plan: a catching plan branches to recovery; an
325
+ // uncaught error propagates out of plan.throw with exact identity.
326
+ current = plan.throw(error);
327
+ break;
328
+ }
329
+ if (trace && entry) trace.end(entry);
330
+ current = plan.next({ kind: "step", value });
331
+ break;
332
+ }
333
+ case "handoff": {
334
+ const entry = trace?.begin(command.name, "handoff");
335
+ try {
336
+ const result = command.execute();
337
+ if (isThenable(result)) {
338
+ observeSettlement(Promise.resolve(result), trace, entry);
339
+ } else if (trace && entry) {
340
+ trace.settle(entry, { status: "fulfilled" });
341
+ }
342
+ } catch (error) {
343
+ if (trace && entry) trace.fail(entry, error);
344
+ current = plan.throw(error);
345
+ break;
346
+ }
347
+ current = plan.next({ kind: "handoff" });
348
+ break;
349
+ }
350
+ }
351
+ }
352
+ assertSynchronousReturn(current.value);
353
+ return current.value;
354
+ } finally {
355
+ if (trace && owner?._activeRoutine === trace) {
356
+ owner._activeRoutine = previousTrace;
357
+ }
358
+ }
359
+ }