@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1c0bdfad
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 +12 -0
- package/README.md +245 -49
- package/dist/bin/rango.js +441 -134
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3453 -1240
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +76 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +247 -23
- package/skills/caching/SKILL.md +318 -15
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +50 -6
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +279 -53
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +771 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +153 -109
- package/skills/rango/SKILL.md +251 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +101 -5
- package/skills/router-setup/SKILL.md +116 -8
- package/skills/scripts/SKILL.md +179 -0
- 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 +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -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 +123 -0
- package/skills/typesafety/SKILL.md +332 -29
- package/skills/use-cache/SKILL.md +46 -14
- 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 +1 -66
- 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/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +197 -150
- 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/navigation-bridge.ts +111 -31
- package/src/browser/navigation-client.ts +201 -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/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +187 -112
- package/src/browser/prefetch/cache.ts +230 -35
- package/src/browser/prefetch/fetch.ts +338 -39
- 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 +111 -16
- package/src/browser/react/NavigationProvider.tsx +135 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -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-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- 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 +80 -5
- package/src/browser/rsc-router.tsx +218 -76
- package/src/browser/scroll-restoration.ts +54 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -61
- package/src/browser/types.ts +91 -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 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +165 -36
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +125 -24
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +272 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +214 -48
- package/src/cache/cache-scope.ts +236 -89
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2224 -171
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +89 -27
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +11 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +31 -31
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +119 -290
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +56 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- 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 +93 -12
- package/src/index.ts +133 -15
- package/src/internal-debug.ts +11 -10
- 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/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -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 +469 -276
- 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 +53 -12
- 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 +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +50 -24
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +274 -56
- 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 +33 -6
- package/src/router/match-middleware/cache-lookup.ts +214 -263
- package/src/router/match-middleware/cache-store.ts +73 -33
- 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 +270 -172
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +132 -90
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +195 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +8 -28
- package/src/router/router-interfaces.ts +115 -35
- package/src/router/router-options.ts +172 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +264 -77
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +63 -37
- package/src/router/segment-resolution/revalidation.ts +474 -385
- 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 +5 -1
- package/src/router/segment-wrappers.ts +8 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +163 -59
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +163 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +658 -511
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +77 -11
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +105 -72
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +263 -112
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +32 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +281 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +26 -46
- package/src/server/request-context.ts +398 -172
- package/src/ssr/index.tsx +25 -16
- 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 +701 -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 +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +371 -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 +343 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -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 +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- 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 +112 -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 +214 -132
- 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 +57 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +6 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +155 -65
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- 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 +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -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/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +46 -37
- package/src/vite/plugins/virtual-entries.ts +13 -18
- package/src/vite/rango.ts +238 -295
- package/src/vite/router-discovery.ts +940 -149
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- 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/directive-prologue.ts +40 -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 +81 -34
- 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/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -10,7 +10,10 @@ import type {
|
|
|
10
10
|
HandleData,
|
|
11
11
|
StreamingToken,
|
|
12
12
|
} from "./types.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
filterSegmentOrder,
|
|
15
|
+
filterRouteSegmentIds,
|
|
16
|
+
} from "./react/filter-segment-order.js";
|
|
14
17
|
|
|
15
18
|
// Polyfill Symbol.dispose for Safari and older browsers
|
|
16
19
|
if (typeof Symbol.dispose === "undefined") {
|
|
@@ -79,6 +82,8 @@ export interface DerivedNavigationState {
|
|
|
79
82
|
state: "idle" | "loading";
|
|
80
83
|
/** Whether any operation is streaming */
|
|
81
84
|
isStreaming: boolean;
|
|
85
|
+
/** Whether a navigation is active (fetching or streaming, before commit) */
|
|
86
|
+
isNavigating: boolean;
|
|
82
87
|
/** Current committed location */
|
|
83
88
|
location: NavigationLocation;
|
|
84
89
|
/** URL being navigated to (null if idle) */
|
|
@@ -111,11 +116,24 @@ export type ActionStateListener = (state: TrackedActionState) => void;
|
|
|
111
116
|
export type HandleListener = () => void;
|
|
112
117
|
|
|
113
118
|
/**
|
|
114
|
-
* Internal handle state stored in controller
|
|
119
|
+
* Internal handle state stored in controller.
|
|
120
|
+
*
|
|
121
|
+
* Two segment lists are exposed because they serve different consumers:
|
|
122
|
+
*
|
|
123
|
+
* - `segmentOrder` drives handle collection (collectHandleData). Includes
|
|
124
|
+
* parallel slot ids and reorders them after their parent so later-wins
|
|
125
|
+
* collect functions (e.g. Meta) get the right precedence.
|
|
126
|
+
* - `routeSegmentIds` is the layouts-and-routes-only list documented by
|
|
127
|
+
* `useSegments().segmentIds`. Parallels and loader sub-ids are stripped;
|
|
128
|
+
* raw matched order is preserved.
|
|
129
|
+
*
|
|
130
|
+
* Both are derived from the same `matched` input on each setHandleData call
|
|
131
|
+
* so they stay in sync.
|
|
115
132
|
*/
|
|
116
133
|
export interface HandleState {
|
|
117
134
|
data: HandleData;
|
|
118
135
|
segmentOrder: string[];
|
|
136
|
+
routeSegmentIds: string[];
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
/**
|
|
@@ -150,6 +168,22 @@ export interface ActionHandle extends Disposable {
|
|
|
150
168
|
startStreaming(): StreamingToken;
|
|
151
169
|
/** Record segments that were revalidated */
|
|
152
170
|
recordRevalidatedSegments(segmentIds: string[]): void;
|
|
171
|
+
/**
|
|
172
|
+
* Claim the subset of a location-state payload this action may write. A key
|
|
173
|
+
* is claimed only if no later-initiated action in the SAME cohort has already
|
|
174
|
+
* claimed it, so same-key concurrent writes resolve to the last-initiated
|
|
175
|
+
* action while distinct keys from every action survive. Recording the claim
|
|
176
|
+
* stops a later-settling earlier action from overwriting it. Arbitration is
|
|
177
|
+
* scoped to the action's cohort (its originating history entry, captured at
|
|
178
|
+
* startAction), so an action on one entry cannot suppress an action on
|
|
179
|
+
* another that happens to write the same slot.
|
|
180
|
+
*
|
|
181
|
+
* Contract: this guarantees the FINAL value is the last-initiated action's,
|
|
182
|
+
* not that the loser is never momentarily visible. When an earlier-initiated
|
|
183
|
+
* action settles first, its value is merged and observable until the
|
|
184
|
+
* later-initiated winner's response lands and overwrites it.
|
|
185
|
+
*/
|
|
186
|
+
claimLocationState(state: Record<string, unknown>): Record<string, unknown>;
|
|
153
187
|
/** Complete the action with result */
|
|
154
188
|
complete(result?: unknown): void;
|
|
155
189
|
/** Fail the action with error */
|
|
@@ -176,7 +210,12 @@ export interface EventController {
|
|
|
176
210
|
abortNavigation(): void;
|
|
177
211
|
|
|
178
212
|
// Action operations
|
|
179
|
-
startAction(
|
|
213
|
+
startAction(
|
|
214
|
+
actionId: string,
|
|
215
|
+
args: unknown[],
|
|
216
|
+
/** Originating history entry key; scopes location-state arbitration. */
|
|
217
|
+
cohort?: string,
|
|
218
|
+
): ActionHandle;
|
|
180
219
|
abortAllActions(): void;
|
|
181
220
|
|
|
182
221
|
// State access
|
|
@@ -200,6 +239,14 @@ export interface EventController {
|
|
|
200
239
|
data: HandleData,
|
|
201
240
|
matched?: string[],
|
|
202
241
|
isPartial?: boolean,
|
|
242
|
+
/**
|
|
243
|
+
* Segment ids that were re-resolved on the server this request (the
|
|
244
|
+
* partial response's `diff`). On a partial update, any existing bucket
|
|
245
|
+
* keyed under one of these ids that has no incoming entry is treated as
|
|
246
|
+
* stale and cleared. Without this, a parallel slot that revalidates but
|
|
247
|
+
* pushes nothing leaves its previous bucket in place forever.
|
|
248
|
+
*/
|
|
249
|
+
resolvedIds?: string[],
|
|
203
250
|
): void;
|
|
204
251
|
getHandleState(): HandleState;
|
|
205
252
|
|
|
@@ -214,10 +261,6 @@ export interface EventController {
|
|
|
214
261
|
hadAnyConcurrentActions(): boolean;
|
|
215
262
|
}
|
|
216
263
|
|
|
217
|
-
// ============================================================================
|
|
218
|
-
// Default States
|
|
219
|
-
// ============================================================================
|
|
220
|
-
|
|
221
264
|
const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
222
265
|
state: "idle",
|
|
223
266
|
actionId: null,
|
|
@@ -238,20 +281,23 @@ function matchesActionId(
|
|
|
238
281
|
entryActionId: string,
|
|
239
282
|
): boolean {
|
|
240
283
|
if (subscriptionId.includes("#")) {
|
|
241
|
-
// Full ID: exact match
|
|
242
284
|
return subscriptionId === entryActionId;
|
|
243
285
|
}
|
|
244
|
-
// Action name only: suffix match (matches "anything#actionName")
|
|
245
286
|
return entryActionId.endsWith(`#${subscriptionId}`);
|
|
246
287
|
}
|
|
247
288
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
289
|
+
// Batch rapid notifications into one microtask to prevent render storms
|
|
290
|
+
function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
291
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
292
|
+
return () => {
|
|
293
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
294
|
+
timeout = setTimeout(() => {
|
|
295
|
+
timeout = null;
|
|
296
|
+
listeners.forEach((listener) => listener());
|
|
297
|
+
}, 0);
|
|
298
|
+
};
|
|
299
|
+
}
|
|
251
300
|
|
|
252
|
-
/**
|
|
253
|
-
* Configuration for creating an event controller
|
|
254
|
-
*/
|
|
255
301
|
export interface EventControllerConfig {
|
|
256
302
|
initialLocation?: NavigationLocation;
|
|
257
303
|
}
|
|
@@ -269,61 +315,56 @@ export interface EventControllerConfig {
|
|
|
269
315
|
export function createEventController(
|
|
270
316
|
config?: EventControllerConfig,
|
|
271
317
|
): EventController {
|
|
272
|
-
// ========================================================================
|
|
273
|
-
// Source of Truth
|
|
274
|
-
// ========================================================================
|
|
275
|
-
|
|
276
|
-
// Current navigation in progress (null = idle)
|
|
277
318
|
let currentNavigation: NavigationEntry | null = null;
|
|
278
319
|
|
|
279
|
-
// All in-flight actions (keyed by unique instance ID)
|
|
280
320
|
const inflightActions = new Map<string, ActionEntry>();
|
|
281
321
|
|
|
282
|
-
// Committed location (updated when navigation completes)
|
|
283
322
|
let location: NavigationLocation =
|
|
284
323
|
config?.initialLocation ??
|
|
285
324
|
(typeof window !== "undefined"
|
|
286
325
|
? new URL(window.location.href)
|
|
287
326
|
: new URL("/", "http://localhost"));
|
|
288
327
|
|
|
289
|
-
// Track if any concurrent actions occurred (for consolidation)
|
|
290
328
|
let hadAnyConcurrentActions = false;
|
|
291
329
|
|
|
292
|
-
// Track segments revalidated by concurrent actions
|
|
293
330
|
const concurrentRevalidatedSegments = new Set<string>();
|
|
294
331
|
|
|
295
|
-
//
|
|
332
|
+
// Monotonic dispatch counter: every startAction() takes the next value
|
|
333
|
+
// (private), so a larger sequence means the action was initiated later.
|
|
334
|
+
let actionDispatchSeq = 0;
|
|
335
|
+
|
|
336
|
+
// Concurrent location-state arbitration, scoped PER COHORT (history entry).
|
|
337
|
+
// Each cohort owns a slotKey->winningDispatchSeq map plus a refcount of its
|
|
338
|
+
// inflight actions; claimLocationState() consults its own cohort's map so
|
|
339
|
+
// same-key writes within one entry resolve to the last-initiated action
|
|
340
|
+
// regardless of settle order, while actions on different entries never
|
|
341
|
+
// compete. A cohort's map is freed once its LAST action's cleanup runs — the
|
|
342
|
+
// same brief post-settle grace (doSettle's 100ms timer) as other action
|
|
343
|
+
// teardown, not when every action everywhere settles — so a long-running
|
|
344
|
+
// action in one cohort can never retain arbitration keys from other cohorts
|
|
345
|
+
// that have since drained. It is never cleared in clearConsolidation, which
|
|
346
|
+
// fires
|
|
347
|
+
// per-action on divert/error and would let a later-settling earlier action
|
|
348
|
+
// wrongly reclaim a key a sibling already won.
|
|
349
|
+
const cohortArbitration = new Map<
|
|
350
|
+
string,
|
|
351
|
+
{ keySeq: Map<string, number>; inflight: number }
|
|
352
|
+
>();
|
|
353
|
+
|
|
296
354
|
let activeStreamCount = 0;
|
|
297
355
|
|
|
298
|
-
// Handle data from RSC payload
|
|
299
356
|
let handleData: HandleData = {};
|
|
300
357
|
let handleSegmentOrder: string[] = [];
|
|
358
|
+
let routeSegmentIds: string[] = [];
|
|
301
359
|
|
|
302
|
-
// Merged route params from current match
|
|
303
360
|
let routeParams: Record<string, string> = {};
|
|
304
361
|
|
|
305
|
-
// ========================================================================
|
|
306
|
-
// Listeners
|
|
307
|
-
// ========================================================================
|
|
308
|
-
|
|
309
362
|
const stateListeners = new Set<StateListener>();
|
|
310
363
|
const actionListeners = new Map<string, Set<ActionStateListener>>();
|
|
311
364
|
const handleListeners = new Set<HandleListener>();
|
|
312
365
|
|
|
313
|
-
|
|
314
|
-
let notifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
366
|
+
const notify = makeDebouncedNotifier(stateListeners);
|
|
315
367
|
|
|
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
|
-
}
|
|
325
|
-
|
|
326
|
-
// Debounce per-action notifications
|
|
327
368
|
const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
328
369
|
|
|
329
370
|
function notifyAction(actionId: string) {
|
|
@@ -335,8 +376,6 @@ export function createEventController(
|
|
|
335
376
|
actionId,
|
|
336
377
|
setTimeout(() => {
|
|
337
378
|
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
379
|
for (const [subscriptionId, listeners] of actionListeners) {
|
|
341
380
|
if (matchesActionId(subscriptionId, actionId)) {
|
|
342
381
|
const state = getActionState(subscriptionId);
|
|
@@ -347,25 +386,9 @@ export function createEventController(
|
|
|
347
386
|
);
|
|
348
387
|
}
|
|
349
388
|
|
|
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
|
-
// ========================================================================
|
|
389
|
+
const notifyHandles = makeDebouncedNotifier(handleListeners);
|
|
366
390
|
|
|
367
391
|
function getState(): DerivedNavigationState {
|
|
368
|
-
// Build inflight actions list (for compatibility with existing API)
|
|
369
392
|
const inflightActionsList: InflightAction[] = [...inflightActions.values()]
|
|
370
393
|
.filter((a) => a.phase !== "settling")
|
|
371
394
|
.map((a) => ({
|
|
@@ -375,20 +398,20 @@ export function createEventController(
|
|
|
375
398
|
startedAt: a.startedAt,
|
|
376
399
|
}));
|
|
377
400
|
|
|
378
|
-
// State: loading if navigation OR actions are in progress
|
|
379
|
-
// Background revalidations (skipLoadingState) don't affect visible state
|
|
380
401
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
381
402
|
const isVisibleNavigation =
|
|
382
403
|
currentNavigation !== null &&
|
|
383
404
|
!currentNavigation.options?.skipLoadingState;
|
|
384
405
|
const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
385
406
|
|
|
386
|
-
// Streaming: true if any active streams (navigation or action) or loading
|
|
387
407
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
388
408
|
|
|
389
409
|
return {
|
|
390
410
|
state,
|
|
391
411
|
isStreaming,
|
|
412
|
+
// True when a navigation is active (fetching or streaming, before
|
|
413
|
+
// commit). Broader than pendingUrl which clears during streaming.
|
|
414
|
+
isNavigating: currentNavigation !== null,
|
|
392
415
|
location,
|
|
393
416
|
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
|
|
394
417
|
// Background revalidations (skipLoadingState) don't expose a pending URL.
|
|
@@ -402,28 +425,20 @@ export function createEventController(
|
|
|
402
425
|
}
|
|
403
426
|
|
|
404
427
|
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;
|
|
428
|
+
const entry = [...inflightActions.values()]
|
|
429
|
+
.filter((a) => matchesActionId(actionId, a.actionId))
|
|
430
|
+
.reduce<ActionEntry | undefined>((best, a) => {
|
|
431
|
+
if (!best) return a;
|
|
432
|
+
const aActive = a.phase !== "settling";
|
|
433
|
+
const bActive = best.phase !== "settling";
|
|
434
|
+
if (aActive !== bActive) return aActive ? a : best;
|
|
435
|
+
return a.startedAt > best.startedAt ? a : best;
|
|
436
|
+
}, undefined);
|
|
421
437
|
|
|
422
438
|
if (!entry) {
|
|
423
439
|
return { ...DEFAULT_ACTION_STATE };
|
|
424
440
|
}
|
|
425
441
|
|
|
426
|
-
// Derive state from phase
|
|
427
442
|
let state: ActionLifecycleState;
|
|
428
443
|
switch (entry.phase) {
|
|
429
444
|
case "fetching":
|
|
@@ -538,10 +553,29 @@ export function createEventController(
|
|
|
538
553
|
// Action Operations
|
|
539
554
|
// ========================================================================
|
|
540
555
|
|
|
541
|
-
function startAction(
|
|
556
|
+
function startAction(
|
|
557
|
+
actionId: string,
|
|
558
|
+
args: unknown[],
|
|
559
|
+
cohort?: string,
|
|
560
|
+
): ActionHandle {
|
|
542
561
|
const id = `${actionId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
562
|
+
// Private to this handle: never exposed on the returned ActionHandle.
|
|
563
|
+
const dispatchSeq = actionDispatchSeq++;
|
|
543
564
|
const abort = new AbortController();
|
|
544
565
|
|
|
566
|
+
// Register this action under its cohort (originating history entry). The
|
|
567
|
+
// cohort's arbitration is created on first use and freed when its last
|
|
568
|
+
// action settles (see doSettle). Keyless entries share the "" cohort.
|
|
569
|
+
const cohortId = cohort ?? "";
|
|
570
|
+
let arb = cohortArbitration.get(cohortId);
|
|
571
|
+
if (!arb) {
|
|
572
|
+
arb = { keySeq: new Map<string, number>(), inflight: 0 };
|
|
573
|
+
cohortArbitration.set(cohortId, arb);
|
|
574
|
+
}
|
|
575
|
+
// const so the captured reference stays non-undefined inside doSettle.
|
|
576
|
+
const arbitration = arb;
|
|
577
|
+
arbitration.inflight++;
|
|
578
|
+
|
|
545
579
|
// Track if this action started while others were pending (concurrent)
|
|
546
580
|
const hadConcurrent = inflightActions.size > 0;
|
|
547
581
|
if (hadConcurrent) {
|
|
@@ -565,11 +599,28 @@ export function createEventController(
|
|
|
565
599
|
let settled = false;
|
|
566
600
|
let streamingEnded = false;
|
|
567
601
|
let actionCompleted = false;
|
|
602
|
+
let cohortReleased = false;
|
|
568
603
|
let pendingResult:
|
|
569
604
|
| { type: "success"; value?: unknown }
|
|
570
605
|
| { type: "error"; value: unknown }
|
|
571
606
|
| null = null;
|
|
572
607
|
|
|
608
|
+
// Release this action's hold on its cohort arbitration exactly once: drop
|
|
609
|
+
// the refcount and, only if the map still points at THIS arbitration object,
|
|
610
|
+
// delete it. A newer generation may have replaced it (e.g. abortAllActions
|
|
611
|
+
// cleared the map and a fresh action recreated the same cohort id), so a
|
|
612
|
+
// stale settlement must never delete the newer one by id.
|
|
613
|
+
function releaseCohort() {
|
|
614
|
+
if (cohortReleased) return;
|
|
615
|
+
cohortReleased = true;
|
|
616
|
+
if (
|
|
617
|
+
--arbitration.inflight <= 0 &&
|
|
618
|
+
cohortArbitration.get(cohortId) === arbitration
|
|
619
|
+
) {
|
|
620
|
+
cohortArbitration.delete(cohortId);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
573
624
|
function doSettle() {
|
|
574
625
|
if (settled) return;
|
|
575
626
|
settled = true;
|
|
@@ -577,6 +628,9 @@ export function createEventController(
|
|
|
577
628
|
// Cleanup after brief delay (allow useAction to read result)
|
|
578
629
|
setTimeout(() => {
|
|
579
630
|
inflightActions.delete(id);
|
|
631
|
+
// Free this cohort's arbitration once its last action has settled, so
|
|
632
|
+
// a long-running action elsewhere cannot pin keys from a drained entry.
|
|
633
|
+
releaseCohort();
|
|
580
634
|
// Check for consolidation
|
|
581
635
|
if (inflightActions.size === 0) {
|
|
582
636
|
// All actions done - reset tracking
|
|
@@ -605,6 +659,19 @@ export function createEventController(
|
|
|
605
659
|
doSettle();
|
|
606
660
|
}
|
|
607
661
|
|
|
662
|
+
// streamingEnded is forced here for the "streaming never started" case so
|
|
663
|
+
// tryFinalize can run; otherwise the streaming token's end() finalizes.
|
|
664
|
+
function settleWith(result: NonNullable<typeof pendingResult>) {
|
|
665
|
+
if (!inflightActions.has(id) || settled) return;
|
|
666
|
+
actionCompleted = true;
|
|
667
|
+
entry.completed = true;
|
|
668
|
+
pendingResult = result;
|
|
669
|
+
if (entry.phase === "fetching" || streamingEnded) {
|
|
670
|
+
streamingEnded = true;
|
|
671
|
+
tryFinalize();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
608
675
|
return {
|
|
609
676
|
id,
|
|
610
677
|
abort,
|
|
@@ -640,36 +707,32 @@ export function createEventController(
|
|
|
640
707
|
segmentIds.forEach((id) => concurrentRevalidatedSegments.add(id));
|
|
641
708
|
},
|
|
642
709
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
710
|
+
claimLocationState(state: Record<string, unknown>) {
|
|
711
|
+
const winning: Record<string, unknown> = {};
|
|
712
|
+
// Arbitrate against this action's OWN captured arbitration object, not a
|
|
713
|
+
// live map lookup: a concurrent map clear/replace (abortAllActions, a
|
|
714
|
+
// stale settlement) must not make this action silently stop recording
|
|
715
|
+
// and accept every key.
|
|
716
|
+
const keySeq = arbitration.keySeq;
|
|
717
|
+
for (const key of Object.keys(state)) {
|
|
718
|
+
const prevSeq = keySeq.get(key);
|
|
719
|
+
// Strictly-greater: a later-initiated action wins a key over an
|
|
720
|
+
// earlier one in the same cohort regardless of arrival order. Equal
|
|
721
|
+
// cannot happen (dispatchSeq is unique per action).
|
|
722
|
+
if (prevSeq === undefined || dispatchSeq > prevSeq) {
|
|
723
|
+
keySeq.set(key, dispatchSeq);
|
|
724
|
+
winning[key] = state[key];
|
|
725
|
+
}
|
|
655
726
|
}
|
|
656
|
-
|
|
727
|
+
return winning;
|
|
657
728
|
},
|
|
658
729
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
actionCompleted = true;
|
|
663
|
-
entry.completed = true;
|
|
664
|
-
pendingResult = { type: "error", value: error };
|
|
730
|
+
complete(result?: unknown) {
|
|
731
|
+
settleWith({ type: "success", value: result });
|
|
732
|
+
},
|
|
665
733
|
|
|
666
|
-
|
|
667
|
-
|
|
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
|
|
734
|
+
fail(error: unknown) {
|
|
735
|
+
settleWith({ type: "error", value: error });
|
|
673
736
|
},
|
|
674
737
|
|
|
675
738
|
getRevalidatedSegments(): Set<string> {
|
|
@@ -685,6 +748,10 @@ export function createEventController(
|
|
|
685
748
|
[Symbol.dispose]() {
|
|
686
749
|
// If aborted, another navigation/error took over - don't touch state
|
|
687
750
|
if (abort.signal.aborted) {
|
|
751
|
+
// Aborted actions skip doSettle, so release the cohort hold here to
|
|
752
|
+
// keep the per-cohort refcount balanced (no leak when an action is
|
|
753
|
+
// aborted individually rather than via abortAllActions).
|
|
754
|
+
releaseCohort();
|
|
688
755
|
inflightActions.delete(id);
|
|
689
756
|
notify();
|
|
690
757
|
notifyAction(actionId);
|
|
@@ -719,6 +786,7 @@ export function createEventController(
|
|
|
719
786
|
}
|
|
720
787
|
hadAnyConcurrentActions = false;
|
|
721
788
|
concurrentRevalidatedSegments.clear();
|
|
789
|
+
cohortArbitration.clear();
|
|
722
790
|
notify();
|
|
723
791
|
// Notify all action listeners directly by subscription ID.
|
|
724
792
|
// actionListeners keys are subscription IDs (possibly short names like
|
|
@@ -739,8 +807,13 @@ export function createEventController(
|
|
|
739
807
|
data: HandleData,
|
|
740
808
|
matched?: string[],
|
|
741
809
|
isPartial?: boolean,
|
|
810
|
+
resolvedIds?: string[],
|
|
742
811
|
): void {
|
|
743
|
-
const
|
|
812
|
+
const rawMatched = matched ?? [];
|
|
813
|
+
const newSegmentOrder = filterSegmentOrder(rawMatched);
|
|
814
|
+
// Separate list for useSegments(): "layouts and routes only" — strip
|
|
815
|
+
// parallels (".@") and loader sub-ids (D digit) without reordering.
|
|
816
|
+
const newRouteSegmentIds = filterRouteSegmentIds(rawMatched);
|
|
744
817
|
|
|
745
818
|
if (isPartial && newSegmentOrder.length > 0) {
|
|
746
819
|
// Partial update: merge new data with existing
|
|
@@ -752,10 +825,19 @@ export function createEventController(
|
|
|
752
825
|
handleData[handleName][segmentId] = data[handleName][segmentId];
|
|
753
826
|
}
|
|
754
827
|
}
|
|
755
|
-
|
|
828
|
+
const resolvedIdSet =
|
|
829
|
+
resolvedIds && resolvedIds.length > 0 ? new Set(resolvedIds) : null;
|
|
830
|
+
// Cleanup pass:
|
|
831
|
+
// a) segment dropped from the match list — delete its bucket.
|
|
832
|
+
// b) segment was re-resolved this request but pushed nothing for
|
|
833
|
+
// this handle — its previous bucket is stale.
|
|
834
|
+
// (a) is the existing behavior; (b) requires resolvedIds.
|
|
756
835
|
for (const handleName of Object.keys(handleData)) {
|
|
757
836
|
for (const segmentId of Object.keys(handleData[handleName])) {
|
|
758
|
-
|
|
837
|
+
const droppedFromMatch = !newSegmentOrder.includes(segmentId);
|
|
838
|
+
const reresolvedWithoutPush =
|
|
839
|
+
resolvedIdSet?.has(segmentId) && !data[handleName]?.[segmentId];
|
|
840
|
+
if (droppedFromMatch || reresolvedWithoutPush) {
|
|
759
841
|
delete handleData[handleName][segmentId];
|
|
760
842
|
}
|
|
761
843
|
}
|
|
@@ -765,6 +847,7 @@ export function createEventController(
|
|
|
765
847
|
handleData = data;
|
|
766
848
|
}
|
|
767
849
|
handleSegmentOrder = newSegmentOrder;
|
|
850
|
+
routeSegmentIds = newRouteSegmentIds;
|
|
768
851
|
|
|
769
852
|
notifyHandles();
|
|
770
853
|
}
|
|
@@ -773,6 +856,7 @@ export function createEventController(
|
|
|
773
856
|
return {
|
|
774
857
|
data: handleData,
|
|
775
858
|
segmentOrder: handleSegmentOrder,
|
|
859
|
+
routeSegmentIds,
|
|
776
860
|
};
|
|
777
861
|
}
|
|
778
862
|
|
|
@@ -860,40 +944,3 @@ export function createEventController(
|
|
|
860
944
|
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
861
945
|
};
|
|
862
946
|
}
|
|
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
|
+
}
|