@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17
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 +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +270 -30
- package/skills/host-router/SKILL.md +82 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +294 -30
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +67 -40
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +86 -147
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +148 -19
- package/src/browser/navigation-client.ts +187 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +52 -26
- package/src/index.ts +100 -38
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +437 -274
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +52 -10
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +120 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +173 -143
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +190 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +242 -75
- package/src/router/segment-resolution/helpers.ts +63 -24
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +2 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +338 -126
- package/src/ssr/index.tsx +23 -15
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +192 -99
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +51 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- 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 +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +27 -29
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -79,6 +79,8 @@ export interface DerivedNavigationState {
|
|
|
79
79
|
state: "idle" | "loading";
|
|
80
80
|
/** Whether any operation is streaming */
|
|
81
81
|
isStreaming: boolean;
|
|
82
|
+
/** Whether a navigation is active (fetching or streaming, before commit) */
|
|
83
|
+
isNavigating: boolean;
|
|
82
84
|
/** Current committed location */
|
|
83
85
|
location: NavigationLocation;
|
|
84
86
|
/** URL being navigated to (null if idle) */
|
|
@@ -111,11 +113,24 @@ export type ActionStateListener = (state: TrackedActionState) => void;
|
|
|
111
113
|
export type HandleListener = () => void;
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
|
-
* Internal handle state stored in controller
|
|
116
|
+
* Internal handle state stored in controller.
|
|
117
|
+
*
|
|
118
|
+
* Two segment lists are exposed because they serve different consumers:
|
|
119
|
+
*
|
|
120
|
+
* - `segmentOrder` drives handle collection (collectHandleData). Includes
|
|
121
|
+
* parallel slot ids and reorders them after their parent so later-wins
|
|
122
|
+
* collect functions (e.g. Meta) get the right precedence.
|
|
123
|
+
* - `routeSegmentIds` is the layouts-and-routes-only list documented by
|
|
124
|
+
* `useSegments().segmentIds`. Parallels and loader sub-ids are stripped;
|
|
125
|
+
* raw matched order is preserved.
|
|
126
|
+
*
|
|
127
|
+
* Both are derived from the same `matched` input on each setHandleData call
|
|
128
|
+
* so they stay in sync.
|
|
115
129
|
*/
|
|
116
130
|
export interface HandleState {
|
|
117
131
|
data: HandleData;
|
|
118
132
|
segmentOrder: string[];
|
|
133
|
+
routeSegmentIds: string[];
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
/**
|
|
@@ -200,6 +215,14 @@ export interface EventController {
|
|
|
200
215
|
data: HandleData,
|
|
201
216
|
matched?: string[],
|
|
202
217
|
isPartial?: boolean,
|
|
218
|
+
/**
|
|
219
|
+
* Segment ids that were re-resolved on the server this request (the
|
|
220
|
+
* partial response's `diff`). On a partial update, any existing bucket
|
|
221
|
+
* keyed under one of these ids that has no incoming entry is treated as
|
|
222
|
+
* stale and cleared. Without this, a parallel slot that revalidates but
|
|
223
|
+
* pushes nothing leaves its previous bucket in place forever.
|
|
224
|
+
*/
|
|
225
|
+
resolvedIds?: string[],
|
|
203
226
|
): void;
|
|
204
227
|
getHandleState(): HandleState;
|
|
205
228
|
|
|
@@ -214,10 +237,6 @@ export interface EventController {
|
|
|
214
237
|
hadAnyConcurrentActions(): boolean;
|
|
215
238
|
}
|
|
216
239
|
|
|
217
|
-
// ============================================================================
|
|
218
|
-
// Default States
|
|
219
|
-
// ============================================================================
|
|
220
|
-
|
|
221
240
|
const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
222
241
|
state: "idle",
|
|
223
242
|
actionId: null,
|
|
@@ -238,20 +257,23 @@ function matchesActionId(
|
|
|
238
257
|
entryActionId: string,
|
|
239
258
|
): boolean {
|
|
240
259
|
if (subscriptionId.includes("#")) {
|
|
241
|
-
// Full ID: exact match
|
|
242
260
|
return subscriptionId === entryActionId;
|
|
243
261
|
}
|
|
244
|
-
// Action name only: suffix match (matches "anything#actionName")
|
|
245
262
|
return entryActionId.endsWith(`#${subscriptionId}`);
|
|
246
263
|
}
|
|
247
264
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
265
|
+
// Batch rapid notifications into one microtask to prevent render storms
|
|
266
|
+
function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
267
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
268
|
+
return () => {
|
|
269
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
270
|
+
timeout = setTimeout(() => {
|
|
271
|
+
timeout = null;
|
|
272
|
+
listeners.forEach((listener) => listener());
|
|
273
|
+
}, 0);
|
|
274
|
+
};
|
|
275
|
+
}
|
|
251
276
|
|
|
252
|
-
/**
|
|
253
|
-
* Configuration for creating an event controller
|
|
254
|
-
*/
|
|
255
277
|
export interface EventControllerConfig {
|
|
256
278
|
initialLocation?: NavigationLocation;
|
|
257
279
|
}
|
|
@@ -269,61 +291,34 @@ export interface EventControllerConfig {
|
|
|
269
291
|
export function createEventController(
|
|
270
292
|
config?: EventControllerConfig,
|
|
271
293
|
): EventController {
|
|
272
|
-
// ========================================================================
|
|
273
|
-
// Source of Truth
|
|
274
|
-
// ========================================================================
|
|
275
|
-
|
|
276
|
-
// Current navigation in progress (null = idle)
|
|
277
294
|
let currentNavigation: NavigationEntry | null = null;
|
|
278
295
|
|
|
279
|
-
// All in-flight actions (keyed by unique instance ID)
|
|
280
296
|
const inflightActions = new Map<string, ActionEntry>();
|
|
281
297
|
|
|
282
|
-
// Committed location (updated when navigation completes)
|
|
283
298
|
let location: NavigationLocation =
|
|
284
299
|
config?.initialLocation ??
|
|
285
300
|
(typeof window !== "undefined"
|
|
286
301
|
? new URL(window.location.href)
|
|
287
302
|
: new URL("/", "http://localhost"));
|
|
288
303
|
|
|
289
|
-
// Track if any concurrent actions occurred (for consolidation)
|
|
290
304
|
let hadAnyConcurrentActions = false;
|
|
291
305
|
|
|
292
|
-
// Track segments revalidated by concurrent actions
|
|
293
306
|
const concurrentRevalidatedSegments = new Set<string>();
|
|
294
307
|
|
|
295
|
-
// Active streaming count (independent of navigation/action lifecycle)
|
|
296
308
|
let activeStreamCount = 0;
|
|
297
309
|
|
|
298
|
-
// Handle data from RSC payload
|
|
299
310
|
let handleData: HandleData = {};
|
|
300
311
|
let handleSegmentOrder: string[] = [];
|
|
312
|
+
let routeSegmentIds: string[] = [];
|
|
301
313
|
|
|
302
|
-
// Merged route params from current match
|
|
303
314
|
let routeParams: Record<string, string> = {};
|
|
304
315
|
|
|
305
|
-
// ========================================================================
|
|
306
|
-
// Listeners
|
|
307
|
-
// ========================================================================
|
|
308
|
-
|
|
309
316
|
const stateListeners = new Set<StateListener>();
|
|
310
317
|
const actionListeners = new Map<string, Set<ActionStateListener>>();
|
|
311
318
|
const handleListeners = new Set<HandleListener>();
|
|
312
319
|
|
|
313
|
-
|
|
314
|
-
let notifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
315
|
-
|
|
316
|
-
function notify() {
|
|
317
|
-
if (notifyTimeout !== null) {
|
|
318
|
-
clearTimeout(notifyTimeout);
|
|
319
|
-
}
|
|
320
|
-
notifyTimeout = setTimeout(() => {
|
|
321
|
-
notifyTimeout = null;
|
|
322
|
-
stateListeners.forEach((listener) => listener());
|
|
323
|
-
}, 0);
|
|
324
|
-
}
|
|
320
|
+
const notify = makeDebouncedNotifier(stateListeners);
|
|
325
321
|
|
|
326
|
-
// Debounce per-action notifications
|
|
327
322
|
const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
328
323
|
|
|
329
324
|
function notifyAction(actionId: string) {
|
|
@@ -335,8 +330,6 @@ export function createEventController(
|
|
|
335
330
|
actionId,
|
|
336
331
|
setTimeout(() => {
|
|
337
332
|
actionNotifyTimeouts.delete(actionId);
|
|
338
|
-
// Notify all listeners whose subscription ID matches this action
|
|
339
|
-
// This includes exact matches and suffix matches (e.g., "addToCart" matches "hash#addToCart")
|
|
340
333
|
for (const [subscriptionId, listeners] of actionListeners) {
|
|
341
334
|
if (matchesActionId(subscriptionId, actionId)) {
|
|
342
335
|
const state = getActionState(subscriptionId);
|
|
@@ -347,25 +340,9 @@ export function createEventController(
|
|
|
347
340
|
);
|
|
348
341
|
}
|
|
349
342
|
|
|
350
|
-
|
|
351
|
-
let handleNotifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
352
|
-
|
|
353
|
-
function notifyHandles() {
|
|
354
|
-
if (handleNotifyTimeout !== null) {
|
|
355
|
-
clearTimeout(handleNotifyTimeout);
|
|
356
|
-
}
|
|
357
|
-
handleNotifyTimeout = setTimeout(() => {
|
|
358
|
-
handleNotifyTimeout = null;
|
|
359
|
-
handleListeners.forEach((listener) => listener());
|
|
360
|
-
}, 0);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// ========================================================================
|
|
364
|
-
// Derived State
|
|
365
|
-
// ========================================================================
|
|
343
|
+
const notifyHandles = makeDebouncedNotifier(handleListeners);
|
|
366
344
|
|
|
367
345
|
function getState(): DerivedNavigationState {
|
|
368
|
-
// Build inflight actions list (for compatibility with existing API)
|
|
369
346
|
const inflightActionsList: InflightAction[] = [...inflightActions.values()]
|
|
370
347
|
.filter((a) => a.phase !== "settling")
|
|
371
348
|
.map((a) => ({
|
|
@@ -375,20 +352,20 @@ export function createEventController(
|
|
|
375
352
|
startedAt: a.startedAt,
|
|
376
353
|
}));
|
|
377
354
|
|
|
378
|
-
// State: loading if navigation OR actions are in progress
|
|
379
|
-
// Background revalidations (skipLoadingState) don't affect visible state
|
|
380
355
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
381
356
|
const isVisibleNavigation =
|
|
382
357
|
currentNavigation !== null &&
|
|
383
358
|
!currentNavigation.options?.skipLoadingState;
|
|
384
359
|
const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
385
360
|
|
|
386
|
-
// Streaming: true if any active streams (navigation or action) or loading
|
|
387
361
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
388
362
|
|
|
389
363
|
return {
|
|
390
364
|
state,
|
|
391
365
|
isStreaming,
|
|
366
|
+
// True when a navigation is active (fetching or streaming, before
|
|
367
|
+
// commit). Broader than pendingUrl which clears during streaming.
|
|
368
|
+
isNavigating: currentNavigation !== null,
|
|
392
369
|
location,
|
|
393
370
|
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
|
|
394
371
|
// Background revalidations (skipLoadingState) don't expose a pending URL.
|
|
@@ -402,28 +379,20 @@ export function createEventController(
|
|
|
402
379
|
}
|
|
403
380
|
|
|
404
381
|
function getActionState(actionId: string): TrackedActionState {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const settlingEntry = [...inflightActions.values()]
|
|
415
|
-
.filter(
|
|
416
|
-
(a) => matchesActionId(actionId, a.actionId) && a.phase === "settling",
|
|
417
|
-
)
|
|
418
|
-
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
419
|
-
|
|
420
|
-
const entry = activeEntry || settlingEntry;
|
|
382
|
+
const entry = [...inflightActions.values()]
|
|
383
|
+
.filter((a) => matchesActionId(actionId, a.actionId))
|
|
384
|
+
.reduce<ActionEntry | undefined>((best, a) => {
|
|
385
|
+
if (!best) return a;
|
|
386
|
+
const aActive = a.phase !== "settling";
|
|
387
|
+
const bActive = best.phase !== "settling";
|
|
388
|
+
if (aActive !== bActive) return aActive ? a : best;
|
|
389
|
+
return a.startedAt > best.startedAt ? a : best;
|
|
390
|
+
}, undefined);
|
|
421
391
|
|
|
422
392
|
if (!entry) {
|
|
423
393
|
return { ...DEFAULT_ACTION_STATE };
|
|
424
394
|
}
|
|
425
395
|
|
|
426
|
-
// Derive state from phase
|
|
427
396
|
let state: ActionLifecycleState;
|
|
428
397
|
switch (entry.phase) {
|
|
429
398
|
case "fetching":
|
|
@@ -605,6 +574,19 @@ export function createEventController(
|
|
|
605
574
|
doSettle();
|
|
606
575
|
}
|
|
607
576
|
|
|
577
|
+
// streamingEnded is forced here for the "streaming never started" case so
|
|
578
|
+
// tryFinalize can run; otherwise the streaming token's end() finalizes.
|
|
579
|
+
function settleWith(result: NonNullable<typeof pendingResult>) {
|
|
580
|
+
if (!inflightActions.has(id) || settled) return;
|
|
581
|
+
actionCompleted = true;
|
|
582
|
+
entry.completed = true;
|
|
583
|
+
pendingResult = result;
|
|
584
|
+
if (entry.phase === "fetching" || streamingEnded) {
|
|
585
|
+
streamingEnded = true;
|
|
586
|
+
tryFinalize();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
608
590
|
return {
|
|
609
591
|
id,
|
|
610
592
|
abort,
|
|
@@ -641,35 +623,11 @@ export function createEventController(
|
|
|
641
623
|
},
|
|
642
624
|
|
|
643
625
|
complete(result?: unknown) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
actionCompleted = true;
|
|
647
|
-
entry.completed = true;
|
|
648
|
-
pendingResult = { type: "success", value: result };
|
|
649
|
-
|
|
650
|
-
// If streaming never started or already ended, finalize immediately
|
|
651
|
-
// Otherwise wait for streaming to end
|
|
652
|
-
if (entry.phase === "fetching" || streamingEnded) {
|
|
653
|
-
streamingEnded = true; // Mark as ended if never started
|
|
654
|
-
tryFinalize();
|
|
655
|
-
}
|
|
656
|
-
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
626
|
+
settleWith({ type: "success", value: result });
|
|
657
627
|
},
|
|
658
628
|
|
|
659
629
|
fail(error: unknown) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
actionCompleted = true;
|
|
663
|
-
entry.completed = true;
|
|
664
|
-
pendingResult = { type: "error", value: error };
|
|
665
|
-
|
|
666
|
-
// If streaming never started or already ended, finalize immediately
|
|
667
|
-
// Otherwise wait for streaming to end
|
|
668
|
-
if (entry.phase === "fetching" || streamingEnded) {
|
|
669
|
-
streamingEnded = true; // Mark as ended if never started
|
|
670
|
-
tryFinalize();
|
|
671
|
-
}
|
|
672
|
-
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
630
|
+
settleWith({ type: "error", value: error });
|
|
673
631
|
},
|
|
674
632
|
|
|
675
633
|
getRevalidatedSegments(): Set<string> {
|
|
@@ -739,8 +697,15 @@ export function createEventController(
|
|
|
739
697
|
data: HandleData,
|
|
740
698
|
matched?: string[],
|
|
741
699
|
isPartial?: boolean,
|
|
700
|
+
resolvedIds?: string[],
|
|
742
701
|
): void {
|
|
743
|
-
const
|
|
702
|
+
const rawMatched = matched ?? [];
|
|
703
|
+
const newSegmentOrder = filterSegmentOrder(rawMatched);
|
|
704
|
+
// Separate list for useSegments(): "layouts and routes only" — strip
|
|
705
|
+
// parallels (".@") and loader sub-ids (D digit) without reordering.
|
|
706
|
+
const newRouteSegmentIds = rawMatched.filter(
|
|
707
|
+
(id) => !id.includes(".@") && !/D\d+\./.test(id),
|
|
708
|
+
);
|
|
744
709
|
|
|
745
710
|
if (isPartial && newSegmentOrder.length > 0) {
|
|
746
711
|
// Partial update: merge new data with existing
|
|
@@ -752,10 +717,19 @@ export function createEventController(
|
|
|
752
717
|
handleData[handleName][segmentId] = data[handleName][segmentId];
|
|
753
718
|
}
|
|
754
719
|
}
|
|
755
|
-
|
|
720
|
+
const resolvedIdSet =
|
|
721
|
+
resolvedIds && resolvedIds.length > 0 ? new Set(resolvedIds) : null;
|
|
722
|
+
// Cleanup pass:
|
|
723
|
+
// a) segment dropped from the match list — delete its bucket.
|
|
724
|
+
// b) segment was re-resolved this request but pushed nothing for
|
|
725
|
+
// this handle — its previous bucket is stale.
|
|
726
|
+
// (a) is the existing behavior; (b) requires resolvedIds.
|
|
756
727
|
for (const handleName of Object.keys(handleData)) {
|
|
757
728
|
for (const segmentId of Object.keys(handleData[handleName])) {
|
|
758
|
-
|
|
729
|
+
const droppedFromMatch = !newSegmentOrder.includes(segmentId);
|
|
730
|
+
const reresolvedWithoutPush =
|
|
731
|
+
resolvedIdSet?.has(segmentId) && !data[handleName]?.[segmentId];
|
|
732
|
+
if (droppedFromMatch || reresolvedWithoutPush) {
|
|
759
733
|
delete handleData[handleName][segmentId];
|
|
760
734
|
}
|
|
761
735
|
}
|
|
@@ -765,6 +739,7 @@ export function createEventController(
|
|
|
765
739
|
handleData = data;
|
|
766
740
|
}
|
|
767
741
|
handleSegmentOrder = newSegmentOrder;
|
|
742
|
+
routeSegmentIds = newRouteSegmentIds;
|
|
768
743
|
|
|
769
744
|
notifyHandles();
|
|
770
745
|
}
|
|
@@ -773,6 +748,7 @@ export function createEventController(
|
|
|
773
748
|
return {
|
|
774
749
|
data: handleData,
|
|
775
750
|
segmentOrder: handleSegmentOrder,
|
|
751
|
+
routeSegmentIds,
|
|
776
752
|
};
|
|
777
753
|
}
|
|
778
754
|
|
|
@@ -860,40 +836,3 @@ export function createEventController(
|
|
|
860
836
|
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
861
837
|
};
|
|
862
838
|
}
|
|
863
|
-
|
|
864
|
-
// ============================================================================
|
|
865
|
-
// Singleton
|
|
866
|
-
// ============================================================================
|
|
867
|
-
|
|
868
|
-
let controllerInstance: EventController | null = null;
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
* Initialize the global event controller
|
|
872
|
-
*/
|
|
873
|
-
export function initEventController(
|
|
874
|
-
config?: EventControllerConfig,
|
|
875
|
-
): EventController {
|
|
876
|
-
if (!controllerInstance) {
|
|
877
|
-
controllerInstance = createEventController(config);
|
|
878
|
-
}
|
|
879
|
-
return controllerInstance;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
/**
|
|
883
|
-
* Get the global event controller
|
|
884
|
-
*/
|
|
885
|
-
export function getEventController(): EventController {
|
|
886
|
-
if (!controllerInstance) {
|
|
887
|
-
throw new Error(
|
|
888
|
-
"Event controller not initialized. Call initEventController first.",
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
return controllerInstance;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/**
|
|
895
|
-
* Reset the controller instance (for testing)
|
|
896
|
-
*/
|
|
897
|
-
export function resetEventController(): void {
|
|
898
|
-
controllerInstance = null;
|
|
899
|
-
}
|
|
@@ -61,6 +61,27 @@ export function buildHistoryState(
|
|
|
61
61
|
return Object.keys(result).length > 0 ? result : null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Stamp an `idx` on the next history entry's state and call push/replaceState.
|
|
66
|
+
* Push increments the current idx; replace keeps it. Initial entry idx is 0.
|
|
67
|
+
* Used by useRouter().back() to detect "first entry in this session" without
|
|
68
|
+
* relying on the Navigation API.
|
|
69
|
+
*/
|
|
70
|
+
export function pushHistoryWithIdx(
|
|
71
|
+
state: Record<string, unknown> | null,
|
|
72
|
+
url: string,
|
|
73
|
+
replace: boolean,
|
|
74
|
+
): void {
|
|
75
|
+
const oldIdx = (window.history.state as { idx?: number } | null)?.idx ?? 0;
|
|
76
|
+
const newIdx = replace ? oldIdx : oldIdx + 1;
|
|
77
|
+
const finalState = { ...(state ?? {}), idx: newIdx };
|
|
78
|
+
if (replace) {
|
|
79
|
+
window.history.replaceState(finalState, "", url);
|
|
80
|
+
} else {
|
|
81
|
+
window.history.pushState(finalState, "", url);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
/**
|
|
65
86
|
* Merge server-set location state into the current history entry.
|
|
66
87
|
* Replaces the current history state and dispatches notification event
|
package/src/browser/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
|
-
// Browser Module - Browser entry point for
|
|
2
|
+
// Browser Module - Browser entry point for Rango
|
|
3
3
|
// ============================================================================
|
|
4
4
|
//
|
|
5
5
|
// Usage:
|
|
6
|
-
// import { initBrowserApp,
|
|
6
|
+
// import { initBrowserApp, Rango } from "rsc-router/browser";
|
|
7
7
|
//
|
|
8
8
|
// For React components (Link, useNavigation, etc.):
|
|
9
9
|
// import { Link, useNavigation, useAction, href } from "rsc-router/client";
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
// Browser app initialization
|
|
14
14
|
export {
|
|
15
15
|
initBrowserApp,
|
|
16
|
-
|
|
16
|
+
Rango,
|
|
17
17
|
type InitBrowserAppOptions,
|
|
18
18
|
} from "./rsc-router.js";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client seat of `invalidateClientCache()` (the `default` export condition).
|
|
3
|
+
*
|
|
4
|
+
* Makes the current client behave as if a server action had just completed:
|
|
5
|
+
* the history cache is marked stale (SWR), the prefetch map is flushed, the
|
|
6
|
+
* state rotates, and sibling tabs are broadcast to — the same
|
|
7
|
+
* `markCacheAsStaleAndBroadcast()` path the server-action bridge uses. This is
|
|
8
|
+
* the gentler mark-stale (not hard-clear) behavior, so Back renders the cached
|
|
9
|
+
* entry instantly and revalidates.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getRegisteredStore } from "./navigation-store-handle.js";
|
|
13
|
+
import { clearPrefetchCache } from "./prefetch/cache.js";
|
|
14
|
+
|
|
15
|
+
export function invalidateClientCache(): void {
|
|
16
|
+
if (typeof document === "undefined") {
|
|
17
|
+
// SSR pass of a client component also resolves the default condition. A
|
|
18
|
+
// render-time call must not take down the page; no-op with a dev warning.
|
|
19
|
+
if (process.env.NODE_ENV !== "production") {
|
|
20
|
+
console.warn(
|
|
21
|
+
"[rango] invalidateClientCache() was called during a server render; " +
|
|
22
|
+
"it is a no-op outside the browser.",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const store = getRegisteredStore();
|
|
29
|
+
if (store) {
|
|
30
|
+
store.markCacheAsStaleAndBroadcast();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Pre-boot: no store registered yet. clearPrefetchCache() (which rotates the
|
|
35
|
+
// state) is complete at this point — there is no history cache to mark and no
|
|
36
|
+
// sibling state worth broadcasting.
|
|
37
|
+
clearPrefetchCache();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Client no-op for `keepClientCache()`. It is a server action directive (the
|
|
42
|
+
* `react-server` condition sets a response header the action bridge reads);
|
|
43
|
+
* there is nothing to suppress from the client side.
|
|
44
|
+
*/
|
|
45
|
+
export function keepClientCache(): void {
|
|
46
|
+
if (process.env.NODE_ENV !== "production") {
|
|
47
|
+
console.warn(
|
|
48
|
+
"[rango] keepClientCache() has no effect on the client; it is a server " +
|
|
49
|
+
"action directive. Call it from inside a server action.",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -117,6 +117,7 @@ export function setupLinkInterception(
|
|
|
117
117
|
// Read navigation options from data attributes (set by Link component)
|
|
118
118
|
const scrollAttr = link.getAttribute("data-scroll");
|
|
119
119
|
const replaceAttr = link.getAttribute("data-replace");
|
|
120
|
+
const revalidateAttr = link.getAttribute("data-revalidate");
|
|
120
121
|
|
|
121
122
|
const navigateOptions: NavigateOptions = {};
|
|
122
123
|
if (scrollAttr === "false") {
|
|
@@ -125,6 +126,9 @@ export function setupLinkInterception(
|
|
|
125
126
|
if (replaceAttr === "true") {
|
|
126
127
|
navigateOptions.replace = true;
|
|
127
128
|
}
|
|
129
|
+
if (revalidateAttr === "false") {
|
|
130
|
+
navigateOptions.revalidate = false;
|
|
131
|
+
}
|
|
128
132
|
|
|
129
133
|
onNavigate(href, navigateOptions);
|
|
130
134
|
};
|