@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100
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/AGENTS.md +9 -0
- package/README.md +1037 -4
- package/dist/bin/rango.js +1619 -157
- package/dist/vite/index.js +5762 -2301
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +71 -63
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +367 -71
- package/skills/host-router/SKILL.md +218 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +176 -8
- package/skills/layout/SKILL.md +124 -3
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +474 -47
- package/skills/middleware/SKILL.md +207 -37
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +15 -11
- package/skills/parallel/SKILL.md +272 -1
- package/skills/prerender/SKILL.md +467 -65
- package/skills/rango/SKILL.md +89 -21
- package/skills/response-routes/SKILL.md +152 -91
- package/skills/route/SKILL.md +305 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +333 -86
- package/skills/use-cache/SKILL.md +324 -0
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +136 -68
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +374 -561
- package/src/browser/navigation-client.ts +228 -70
- package/src/browser/navigation-store.ts +97 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +376 -315
- package/src/browser/prefetch/cache.ts +314 -0
- package/src/browser/prefetch/fetch.ts +282 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +191 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +152 -0
- package/src/browser/react/Link.tsx +255 -71
- package/src/browser/react/NavigationProvider.tsx +152 -24
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +55 -0
- package/src/browser/react/index.ts +15 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -120
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +78 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +83 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +85 -99
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +246 -64
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +158 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +84 -23
- package/src/build/generate-route-types.ts +39 -828
- package/src/build/index.ts +4 -5
- package/src/build/route-trie.ts +85 -32
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +418 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +618 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +342 -0
- package/src/cache/cache-scope.ts +167 -307
- package/src/cache/cf/cf-cache-store.ts +573 -21
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +6 -1
- package/src/client.tsx +118 -302
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +77 -7
- package/src/handle.ts +55 -10
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +65 -45
- package/src/index.rsc.ts +138 -21
- package/src/index.ts +206 -51
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +25 -143
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +159 -13
- package/src/prerender.ts +397 -29
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +231 -121
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1134 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +483 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition.ts +1 -1431
- package/src/route-map-builder.ts +162 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +66 -9
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +418 -86
- package/src/router/intercept-resolution.ts +35 -20
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +359 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +98 -32
- package/src/router/match-api.ts +196 -261
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +441 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +415 -86
- package/src/router/match-middleware/cache-store.ts +91 -29
- package/src/router/match-middleware/intercept-resolution.ts +48 -21
- package/src/router/match-middleware/segment-resolution.ts +73 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +154 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +209 -0
- package/src/router/middleware.ts +373 -371
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +292 -52
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +152 -39
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +756 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1407 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1315
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +111 -39
- package/src/router/types.ts +17 -9
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +642 -2011
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +864 -1114
- package/src/rsc/helpers.ts +181 -19
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +395 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +360 -0
- package/src/rsc/rsc-rendering.ts +256 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +360 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +52 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +187 -38
- package/src/server/context.ts +333 -59
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +603 -109
- package/src/server.ts +35 -155
- package/src/ssr/index.tsx +107 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +764 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +209 -0
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +167 -0
- package/src/types.ts +1 -1757
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +108 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -1282
- package/src/use-loader.tsx +161 -81
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +376 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +486 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -2063
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +98 -0
- package/src/vite/plugins/client-ref-dedup.ts +131 -0
- package/src/vite/plugins/client-ref-hashing.ts +117 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +816 -0
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +336 -0
- package/src/vite/plugins/version-injector.ts +109 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +497 -0
- package/src/vite/router-discovery.ts +1423 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +161 -0
- package/src/vite/utils/prerender-utils.ts +222 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- package/src/vite/expose-prerender-handler-id.ts +0 -429
- package/src/vite/package-resolution.ts +0 -125
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: streams-and-websockets
|
|
3
|
+
description: Long-lived Response handlers — Server-Sent Events (SSE) via path.stream and WebSocket upgrades via path.any on Cloudflare Workers, including middleware interaction and runtime caveats.
|
|
4
|
+
argument-hint: "[sse | websocket | agents]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Streams and WebSockets
|
|
8
|
+
|
|
9
|
+
Response routes can return long-lived responses — SSE streams and WebSocket
|
|
10
|
+
upgrades. Both require a `Response` that the router must forward through the
|
|
11
|
+
middleware chain without reconstruction.
|
|
12
|
+
|
|
13
|
+
## When each fits
|
|
14
|
+
|
|
15
|
+
| Shape | Tag | Status | Body | Runtime |
|
|
16
|
+
| ----------- | --------------- | ------ | ------------------------------- | -------------------------------- |
|
|
17
|
+
| Server-Sent | `path.stream()` | 200 | `ReadableStream` (event-stream) | any runtime (Node, workerd, bun) |
|
|
18
|
+
| WebSocket | `path.any()` | 101 | `null` + `webSocket` property | Cloudflare Workers (workerd) |
|
|
19
|
+
|
|
20
|
+
- **SSE** is a regular 200 response with `content-type: text/event-stream`
|
|
21
|
+
and a `ReadableStream` body. Works everywhere, flows through middleware
|
|
22
|
+
normally.
|
|
23
|
+
- **WebSocket upgrades** produce a status-101 response with a non-standard
|
|
24
|
+
`webSocket` property (Cloudflare). The router detects these and forwards
|
|
25
|
+
them without reconstruction; `Vary` and `Server-Timing` are skipped, and
|
|
26
|
+
stub headers are merged in place on a best-effort basis.
|
|
27
|
+
|
|
28
|
+
## Server-Sent Events (SSE)
|
|
29
|
+
|
|
30
|
+
Use `path.stream()` (or `path.any()` if you need full control) to return a
|
|
31
|
+
`ReadableStream`. Each chunk is an `event-stream` frame:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { urls } from "@rangojs/router";
|
|
35
|
+
|
|
36
|
+
export const urlpatterns = urls(({ path }) => [
|
|
37
|
+
path.stream(
|
|
38
|
+
"/events/ticks",
|
|
39
|
+
(ctx) => {
|
|
40
|
+
const encoder = new TextEncoder();
|
|
41
|
+
|
|
42
|
+
const stream = new ReadableStream({
|
|
43
|
+
async start(controller) {
|
|
44
|
+
let count = 0;
|
|
45
|
+
const interval = setInterval(() => {
|
|
46
|
+
controller.enqueue(
|
|
47
|
+
encoder.encode(`event: tick\ndata: ${++count}\n\n`),
|
|
48
|
+
);
|
|
49
|
+
}, 1000);
|
|
50
|
+
|
|
51
|
+
// Honor client disconnect — signal comes from ctx.request.signal
|
|
52
|
+
ctx.request.signal.addEventListener("abort", () => {
|
|
53
|
+
clearInterval(interval);
|
|
54
|
+
controller.close();
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return new Response(stream, {
|
|
60
|
+
headers: {
|
|
61
|
+
"content-type": "text/event-stream",
|
|
62
|
+
"cache-control": "no-store",
|
|
63
|
+
// Disable proxy buffering on Nginx/Traefik deployments
|
|
64
|
+
"x-accel-buffering": "no",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
{ name: "ticks" },
|
|
69
|
+
),
|
|
70
|
+
]);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Client
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
"use client";
|
|
77
|
+
const source = new EventSource("/events/ticks");
|
|
78
|
+
source.addEventListener("tick", (e) => console.log("tick", e.data));
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### SSE caveats
|
|
82
|
+
|
|
83
|
+
- **Never wrap SSE routes in `cache()`** — a cached `ReadableStream` is read
|
|
84
|
+
once and would replay an empty body on the next hit. `path.stream` is
|
|
85
|
+
already excluded from response-route caching, but don't layer a custom
|
|
86
|
+
cache() middleware on top.
|
|
87
|
+
- **Middleware is fine.** Global/route middleware rewraps the SSE `Response`
|
|
88
|
+
as `new Response(response.body, { status, headers })` to merge stub headers.
|
|
89
|
+
The `ReadableStream` body is passed by reference, not consumed, so the
|
|
90
|
+
client sees the stream unchanged. (WebSocket upgrades are the exception —
|
|
91
|
+
those bypass rewrap entirely; see below.)
|
|
92
|
+
- **Honor `ctx.request.signal`.** Without wiring abort to your source
|
|
93
|
+
(timer, DB cursor, upstream fetch), the stream leaks when the client
|
|
94
|
+
disconnects.
|
|
95
|
+
- **Disable Nginx/CDN buffering** via `x-accel-buffering: no` and ensure
|
|
96
|
+
no intermediate proxy rebuffers. On Cloudflare Workers this is a non-issue.
|
|
97
|
+
|
|
98
|
+
## WebSockets (Cloudflare Workers)
|
|
99
|
+
|
|
100
|
+
WebSocket upgrades on workerd produce a response with `status: 101` and a
|
|
101
|
+
non-standard `webSocket` property. The router detects this shape and forwards
|
|
102
|
+
the `Response` without reconstruction — the 101 status and the `webSocket`
|
|
103
|
+
property are preserved. `Vary` and `Server-Timing` writes are skipped, and
|
|
104
|
+
stub-header merging (cookies/custom headers set via `ctx.header()` or
|
|
105
|
+
`cookies().set()`) is best-effort: the router attempts to apply them in
|
|
106
|
+
place, but silently skips any write rejected by a runtime that exposes
|
|
107
|
+
immutable upgrade headers.
|
|
108
|
+
|
|
109
|
+
### Minimal upgrade handler
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { urls } from "@rangojs/router";
|
|
113
|
+
|
|
114
|
+
export const urlpatterns = urls(({ path }) => [
|
|
115
|
+
path.any(
|
|
116
|
+
"/ws",
|
|
117
|
+
(ctx) => {
|
|
118
|
+
// Manual WebSocketPair on workerd
|
|
119
|
+
const upgrade = ctx.request.headers.get("upgrade");
|
|
120
|
+
if (upgrade !== "websocket") {
|
|
121
|
+
return new Response("expected upgrade: websocket", { status: 426 });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { 0: client, 1: server } = new WebSocketPair();
|
|
125
|
+
server.accept();
|
|
126
|
+
server.addEventListener("message", (e) => {
|
|
127
|
+
server.send(`echo: ${e.data}`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return new Response(null, {
|
|
131
|
+
status: 101,
|
|
132
|
+
webSocket: client,
|
|
133
|
+
} as ResponseInit);
|
|
134
|
+
},
|
|
135
|
+
{ name: "ws" },
|
|
136
|
+
),
|
|
137
|
+
]);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Durable Object pattern
|
|
141
|
+
|
|
142
|
+
Route into a Durable Object that owns the connection:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
export const urlpatterns = urls(({ path }) => [
|
|
146
|
+
path.any(
|
|
147
|
+
"/rooms/:roomId",
|
|
148
|
+
async (ctx) => {
|
|
149
|
+
const id = ctx.env.ROOMS.idFromName(ctx.params.roomId);
|
|
150
|
+
const stub = ctx.env.ROOMS.get(id);
|
|
151
|
+
// The DO's fetch handler calls handleWebSocketUpgrade(request)
|
|
152
|
+
// and returns the 101 Response. We forward it unchanged.
|
|
153
|
+
return stub.fetch(ctx.request);
|
|
154
|
+
},
|
|
155
|
+
{ name: "room" },
|
|
156
|
+
),
|
|
157
|
+
]);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Using the `agents` library
|
|
161
|
+
|
|
162
|
+
`routeAgentRequest` from `agents` returns a 101 `Response` targeted at a
|
|
163
|
+
Durable Object. Return it directly from `path.any()`:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { routeAgentRequest } from "agents";
|
|
167
|
+
import { urls } from "@rangojs/router";
|
|
168
|
+
|
|
169
|
+
export const urlpatterns = urls(({ path }) => [
|
|
170
|
+
path.any("/agents/*", async (ctx) => {
|
|
171
|
+
const response = await routeAgentRequest(ctx.request, ctx.env);
|
|
172
|
+
if (!response) {
|
|
173
|
+
return new Response("not found", { status: 404 });
|
|
174
|
+
}
|
|
175
|
+
return response;
|
|
176
|
+
}),
|
|
177
|
+
]);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Middleware interaction
|
|
181
|
+
|
|
182
|
+
### Forwarded, not reconstructed
|
|
183
|
+
|
|
184
|
+
When a middleware is matched for the upgrade URL, the middleware still runs
|
|
185
|
+
**before** `next()` — but the Response from `next()` is forwarded as-is
|
|
186
|
+
rather than re-wrapped. This preserves:
|
|
187
|
+
|
|
188
|
+
- The 101 status (which would otherwise throw `RangeError: Responses may
|
|
189
|
+
only be constructed with status codes in the range 200 to 599, inclusive`
|
|
190
|
+
on standards-compliant runtimes).
|
|
191
|
+
- The Cloudflare `webSocket` property (which would otherwise be silently
|
|
192
|
+
dropped by `new Response(body, ...)` on workerd).
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// This works — logger runs, but the 101 flows through unchanged.
|
|
196
|
+
router.use(async (ctx, next) => {
|
|
197
|
+
console.log("ws request", ctx.url.pathname);
|
|
198
|
+
return next();
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Don't try to set cookies on an upgrade
|
|
203
|
+
|
|
204
|
+
Stub cookie/header writes made before `await next()` are applied to the
|
|
205
|
+
upgrade response on a best-effort basis — the router attempts an in-place
|
|
206
|
+
merge and skips any write rejected by runtimes that expose immutable 101
|
|
207
|
+
headers. Either way, a browser completing a WS handshake never reads them.
|
|
208
|
+
Do not rely on this for auth or state propagation: set cookies via a prior
|
|
209
|
+
HTTP request instead (e.g. during login), then read them at upgrade time
|
|
210
|
+
via `ctx.request.headers.get("cookie")`.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// Avoid: this cookie may not land on the upgrade response, and the client
|
|
214
|
+
// never reads it during the handshake regardless.
|
|
215
|
+
router.use(async (ctx, next) => {
|
|
216
|
+
cookies().set("last-ws-at", Date.now().toString());
|
|
217
|
+
return next();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Prefer: authenticate by reading a cookie set on a prior HTTP request.
|
|
221
|
+
path.any("/ws", (ctx) => {
|
|
222
|
+
const session = parseCookie(ctx.request.headers.get("cookie"))?.session;
|
|
223
|
+
if (!verify(session)) return new Response("unauthorized", { status: 401 });
|
|
224
|
+
// ...upgrade
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Short-circuit before upgrade
|
|
229
|
+
|
|
230
|
+
Middleware can return a non-101 Response to deny the upgrade outright:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
router.use(async (ctx, next) => {
|
|
234
|
+
if (!isAllowed(ctx.request)) {
|
|
235
|
+
return new Response("forbidden", { status: 403 });
|
|
236
|
+
}
|
|
237
|
+
return next();
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Caching
|
|
242
|
+
|
|
243
|
+
- **SSE** — do not combine with `cache()` (streams can't be replayed).
|
|
244
|
+
- **WebSocket** — `cache()` is inert because only `status === 200` is cacheable.
|
|
245
|
+
|
|
246
|
+
## Runtime caveats
|
|
247
|
+
|
|
248
|
+
| Runtime | SSE | WebSocket upgrade (101) |
|
|
249
|
+
| -------------------------------------- | --- | ---------------------------------------------------- |
|
|
250
|
+
| Cloudflare Workers (workerd) | OK | OK (native `WebSocketPair`, DO, `agents`) |
|
|
251
|
+
| Node (undici fetch) | OK | N/A — Node's HTTP server must upgrade |
|
|
252
|
+
| Bun | OK | Bun's native `upgrade()` — not a Response-based path |
|
|
253
|
+
| Dev (Vite + `@cloudflare/vite-plugin`) | OK | OK via workerd emulation |
|
|
254
|
+
|
|
255
|
+
When running in pure Node without workerd, a `status: 101` Response cannot
|
|
256
|
+
even be constructed (`new Response(null, { status: 101 })` throws). For
|
|
257
|
+
tests, fabricate upgrade-style responses by overriding `.status` on a real
|
|
258
|
+
Response instance:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const upgrade = new Response(null, { status: 200 });
|
|
262
|
+
Object.defineProperty(upgrade, "status", { value: 101, configurable: true });
|
|
263
|
+
// optional: attach a webSocket stub
|
|
264
|
+
Object.defineProperty(upgrade, "webSocket", {
|
|
265
|
+
value: { stub: "ws" },
|
|
266
|
+
configurable: true,
|
|
267
|
+
enumerable: true,
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Testing
|
|
272
|
+
|
|
273
|
+
- Unit tests: `isWebSocketUpgradeResponse` and `executeMiddleware` passthrough
|
|
274
|
+
cases live in `src/rsc/__tests__/helpers.test.ts` and
|
|
275
|
+
`src/router/middleware.test.ts`.
|
|
276
|
+
- E2E: cover both dev and production modes against a workerd target. SSE
|
|
277
|
+
can be tested on any runtime; WS upgrades need workerd (use
|
|
278
|
+
`@cloudflare/vite-plugin` or `wrangler dev`).
|
|
279
|
+
|
|
280
|
+
## See also
|
|
281
|
+
|
|
282
|
+
- `response-routes` — the parent skill for `path.json/text/html/stream/any`.
|
|
283
|
+
- `middleware` — how global and route-level middleware compose with handlers.
|
package/skills/theme/SKILL.md
CHANGED
|
@@ -25,31 +25,32 @@ const router = createRouter<Env>({
|
|
|
25
25
|
document: Document,
|
|
26
26
|
urls: urlpatterns,
|
|
27
27
|
theme: {
|
|
28
|
-
defaultTheme: "system",
|
|
28
|
+
defaultTheme: "system", // "light" | "dark" | "system"
|
|
29
29
|
themes: ["light", "dark"],
|
|
30
|
-
attribute: "class",
|
|
30
|
+
attribute: "class", // or "data-theme"
|
|
31
31
|
storageKey: "theme",
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
33
|
});
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## Server (in loaders/middleware)
|
|
37
37
|
|
|
38
38
|
```typescript
|
|
39
|
-
import { createLoader
|
|
39
|
+
import { createLoader } from "@rangojs/router";
|
|
40
|
+
import type { Middleware } from "@rangojs/router";
|
|
40
41
|
|
|
41
42
|
// In a loader
|
|
42
|
-
export const SettingsLoader = createLoader(
|
|
43
|
-
const currentTheme = ctx.theme;
|
|
43
|
+
export const SettingsLoader = createLoader(async (ctx) => {
|
|
44
|
+
const currentTheme = ctx.theme; // read from cookie
|
|
44
45
|
return { theme: currentTheme };
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
// In middleware
|
|
48
|
-
export const themeMiddleware =
|
|
49
|
+
export const themeMiddleware: Middleware = async (ctx, next) => {
|
|
49
50
|
// Set theme based on user preference
|
|
50
51
|
ctx.setTheme("dark");
|
|
51
52
|
await next();
|
|
52
|
-
}
|
|
53
|
+
};
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
## Client
|