@rangojs/router 0.0.0-experimental.c873df95 → 0.0.0-experimental.c9471f9b
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 +8 -4
- package/README.md +303 -741
- package/dist/bin/rango.js +724 -183
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +4276 -1206
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +86 -15
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +85 -6
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +251 -24
- package/skills/caching/SKILL.md +338 -13
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +110 -4
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +87 -56
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +73 -691
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +129 -27
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +94 -18
- package/skills/layout/SKILL.md +62 -19
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +302 -54
- package/skills/middleware/SKILL.md +59 -16
- package/skills/migrate-nextjs/SKILL.md +745 -0
- package/skills/migrate-react-router/SKILL.md +153 -0
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +29 -2
- package/skills/observability/SKILL.md +202 -0
- package/skills/parallel/SKILL.md +166 -10
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +178 -124
- package/skills/rango/SKILL.md +318 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +138 -49
- package/skills/route/SKILL.md +172 -9
- package/skills/router-setup/SKILL.md +131 -11
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +776 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +130 -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 +131 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -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/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -616
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +74 -15
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +337 -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 +252 -158
- 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/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +131 -30
- package/src/browser/navigation-client.ts +204 -87
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +203 -80
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +198 -125
- package/src/browser/prefetch/cache.ts +246 -71
- package/src/browser/prefetch/fetch.ts +358 -40
- package/src/browser/prefetch/queue.ts +113 -32
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +112 -15
- package/src/browser/react/NavigationProvider.tsx +213 -122
- 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 +99 -8
- package/src/browser/rsc-router.tsx +168 -28
- package/src/browser/scroll-restoration.ts +44 -27
- package/src/browser/segment-reconciler.ts +44 -12
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +244 -71
- package/src/browser/types.ts +127 -12
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +207 -158
- package/src/build/generate-route-types.ts +6 -1
- package/src/build/index.ts +11 -3
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +198 -41
- 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 +464 -63
- 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 +333 -117
- 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 +13 -21
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +454 -101
- package/src/cache/cache-scope.ts +233 -101
- package/src/cache/cache-tag.ts +149 -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 +2206 -372
- 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 +6 -16
- package/src/cache/document-cache.ts +126 -41
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +243 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +13 -21
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +194 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1132 -0
- package/src/client.rsc.tsx +41 -21
- package/src/client.tsx +116 -290
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +84 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +185 -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 +104 -34
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +9 -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 +100 -13
- package/src/index.ts +143 -19
- 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 +215 -86
- package/src/redirect-origin.ts +114 -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 +19 -77
- package/src/route-definition/dsl-helpers.ts +461 -304
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +153 -77
- package/src/route-definition/index.ts +4 -2
- package/src/route-definition/redirect.ts +51 -10
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +37 -46
- 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 +129 -30
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +48 -24
- package/src/router/lazy-includes.ts +85 -62
- package/src/router/loader-resolution.ts +286 -56
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +96 -53
- package/src/router/match-api.ts +178 -218
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +211 -165
- package/src/router/match-middleware/background-revalidation.ts +48 -21
- package/src/router/match-middleware/cache-lookup.ts +178 -277
- package/src/router/match-middleware/cache-store.ts +74 -45
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +124 -50
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +300 -177
- package/src/router/navigation-snapshot.ts +133 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +181 -150
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +203 -58
- package/src/router/preview-match.ts +35 -103
- package/src/router/request-classification.ts +291 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +256 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +146 -35
- package/src/router/router-options.ts +202 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +204 -88
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +155 -39
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +354 -319
- 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 +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +75 -0
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +105 -20
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -59
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +205 -70
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +682 -508
- 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/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +138 -15
- package/src/rsc/redirect-guard.ts +100 -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 +509 -73
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -113
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +62 -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 +167 -0
- package/src/segment-system.tsx +386 -205
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +280 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +152 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +38 -46
- package/src/server/request-context.ts +550 -171
- package/src/ssr/index.tsx +467 -175
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -0
- package/src/testing/dispatch.ts +813 -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 +199 -0
- package/src/testing/index.ts +105 -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 +357 -0
- package/src/testing/render-route.tsx +584 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -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 +56 -84
- 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 +145 -79
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +136 -15
- package/src/urls/include-helper.ts +40 -75
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -11
- package/src/urls/path-helper-types.ts +102 -23
- package/src/urls/path-helper.ts +62 -111
- package/src/urls/pattern-types.ts +84 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +98 -154
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +346 -89
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +255 -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 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +15 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +261 -6
- 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 +48 -95
- 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/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +138 -27
- package/src/vite/rango.ts +247 -132
- package/src/vite/router-discovery.ts +1091 -159
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- 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 +98 -38
- package/src/vite/utils/shared-utils.ts +144 -44
- 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") {
|
|
@@ -113,11 +116,24 @@ export type ActionStateListener = (state: TrackedActionState) => void;
|
|
|
113
116
|
export type HandleListener = () => void;
|
|
114
117
|
|
|
115
118
|
/**
|
|
116
|
-
* 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.
|
|
117
132
|
*/
|
|
118
133
|
export interface HandleState {
|
|
119
134
|
data: HandleData;
|
|
120
135
|
segmentOrder: string[];
|
|
136
|
+
routeSegmentIds: string[];
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
/**
|
|
@@ -152,6 +168,22 @@ export interface ActionHandle extends Disposable {
|
|
|
152
168
|
startStreaming(): StreamingToken;
|
|
153
169
|
/** Record segments that were revalidated */
|
|
154
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>;
|
|
155
187
|
/** Complete the action with result */
|
|
156
188
|
complete(result?: unknown): void;
|
|
157
189
|
/** Fail the action with error */
|
|
@@ -178,7 +210,12 @@ export interface EventController {
|
|
|
178
210
|
abortNavigation(): void;
|
|
179
211
|
|
|
180
212
|
// Action operations
|
|
181
|
-
startAction(
|
|
213
|
+
startAction(
|
|
214
|
+
actionId: string,
|
|
215
|
+
args: unknown[],
|
|
216
|
+
/** Originating history entry key; scopes location-state arbitration. */
|
|
217
|
+
cohort?: string,
|
|
218
|
+
): ActionHandle;
|
|
182
219
|
abortAllActions(): void;
|
|
183
220
|
|
|
184
221
|
// State access
|
|
@@ -202,8 +239,24 @@ export interface EventController {
|
|
|
202
239
|
data: HandleData,
|
|
203
240
|
matched?: string[],
|
|
204
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[],
|
|
205
250
|
): void;
|
|
206
251
|
getHandleState(): HandleState;
|
|
252
|
+
/**
|
|
253
|
+
* Update ONLY `routeSegmentIds` (what `useSegments` reads) from `matched`,
|
|
254
|
+
* leaving `data` and `segmentOrder` (what `useHandle` collects over) untouched.
|
|
255
|
+
* Used while a deferred handle is resolving: the route has changed (so
|
|
256
|
+
* `useSegments` must reflect the new segment ids) but `useHandle` still holds
|
|
257
|
+
* its previous value until the deferred snapshot is applied.
|
|
258
|
+
*/
|
|
259
|
+
setRouteSegmentIds(matched: string[]): void;
|
|
207
260
|
|
|
208
261
|
// Params operations
|
|
209
262
|
setParams(params: Record<string, string>): void;
|
|
@@ -216,10 +269,6 @@ export interface EventController {
|
|
|
216
269
|
hadAnyConcurrentActions(): boolean;
|
|
217
270
|
}
|
|
218
271
|
|
|
219
|
-
// ============================================================================
|
|
220
|
-
// Default States
|
|
221
|
-
// ============================================================================
|
|
222
|
-
|
|
223
272
|
const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
224
273
|
state: "idle",
|
|
225
274
|
actionId: null,
|
|
@@ -228,6 +277,13 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
228
277
|
result: null,
|
|
229
278
|
};
|
|
230
279
|
|
|
280
|
+
// Shared empty inflight-actions list. getState() hands back this exact reference
|
|
281
|
+
// whenever no action is inflight (the overwhelmingly common case), so the derived
|
|
282
|
+
// snapshot's `inflightActions` is referentially stable across notifies instead of
|
|
283
|
+
// a fresh [] each call. Read-only by contract (consumers only read length/spread),
|
|
284
|
+
// same as the shared DEFAULT_ACTION_STATE.
|
|
285
|
+
const EMPTY_INFLIGHT_ACTIONS: InflightAction[] = [];
|
|
286
|
+
|
|
231
287
|
/**
|
|
232
288
|
* Check if a subscription ID matches an action's full ID.
|
|
233
289
|
*
|
|
@@ -240,20 +296,23 @@ function matchesActionId(
|
|
|
240
296
|
entryActionId: string,
|
|
241
297
|
): boolean {
|
|
242
298
|
if (subscriptionId.includes("#")) {
|
|
243
|
-
// Full ID: exact match
|
|
244
299
|
return subscriptionId === entryActionId;
|
|
245
300
|
}
|
|
246
|
-
// Action name only: suffix match (matches "anything#actionName")
|
|
247
301
|
return entryActionId.endsWith(`#${subscriptionId}`);
|
|
248
302
|
}
|
|
249
303
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
304
|
+
// Batch rapid notifications into one microtask to prevent render storms
|
|
305
|
+
function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
306
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
307
|
+
return () => {
|
|
308
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
309
|
+
timeout = setTimeout(() => {
|
|
310
|
+
timeout = null;
|
|
311
|
+
listeners.forEach((listener) => listener());
|
|
312
|
+
}, 0);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
253
315
|
|
|
254
|
-
/**
|
|
255
|
-
* Configuration for creating an event controller
|
|
256
|
-
*/
|
|
257
316
|
export interface EventControllerConfig {
|
|
258
317
|
initialLocation?: NavigationLocation;
|
|
259
318
|
}
|
|
@@ -271,61 +330,68 @@ export interface EventControllerConfig {
|
|
|
271
330
|
export function createEventController(
|
|
272
331
|
config?: EventControllerConfig,
|
|
273
332
|
): EventController {
|
|
274
|
-
// ========================================================================
|
|
275
|
-
// Source of Truth
|
|
276
|
-
// ========================================================================
|
|
277
|
-
|
|
278
|
-
// Current navigation in progress (null = idle)
|
|
279
333
|
let currentNavigation: NavigationEntry | null = null;
|
|
280
334
|
|
|
281
|
-
// All in-flight actions (keyed by unique instance ID)
|
|
282
335
|
const inflightActions = new Map<string, ActionEntry>();
|
|
283
336
|
|
|
284
|
-
// Committed location (updated when navigation completes)
|
|
285
337
|
let location: NavigationLocation =
|
|
286
338
|
config?.initialLocation ??
|
|
287
339
|
(typeof window !== "undefined"
|
|
288
340
|
? new URL(window.location.href)
|
|
289
341
|
: new URL("/", "http://localhost"));
|
|
290
342
|
|
|
291
|
-
// Track if any concurrent actions occurred (for consolidation)
|
|
292
343
|
let hadAnyConcurrentActions = false;
|
|
293
344
|
|
|
294
|
-
// Track segments revalidated by concurrent actions
|
|
295
345
|
const concurrentRevalidatedSegments = new Set<string>();
|
|
296
346
|
|
|
297
|
-
//
|
|
347
|
+
// Monotonic dispatch counter: every startAction() takes the next value
|
|
348
|
+
// (private), so a larger sequence means the action was initiated later.
|
|
349
|
+
let actionDispatchSeq = 0;
|
|
350
|
+
|
|
351
|
+
// Concurrent location-state arbitration, scoped PER COHORT (history entry).
|
|
352
|
+
// Each cohort owns a slotKey->winningDispatchSeq map plus a refcount of its
|
|
353
|
+
// inflight actions; claimLocationState() consults its own cohort's map so
|
|
354
|
+
// same-key writes within one entry resolve to the last-initiated action
|
|
355
|
+
// regardless of settle order, while actions on different entries never
|
|
356
|
+
// compete. A cohort's map is freed once its LAST action's cleanup runs — the
|
|
357
|
+
// same brief post-settle grace (doSettle's 100ms timer) as other action
|
|
358
|
+
// teardown, not when every action everywhere settles — so a long-running
|
|
359
|
+
// action in one cohort can never retain arbitration keys from other cohorts
|
|
360
|
+
// that have since drained. It is never cleared in clearConsolidation, which
|
|
361
|
+
// fires
|
|
362
|
+
// per-action on divert/error and would let a later-settling earlier action
|
|
363
|
+
// wrongly reclaim a key a sibling already won.
|
|
364
|
+
const cohortArbitration = new Map<
|
|
365
|
+
string,
|
|
366
|
+
{ keySeq: Map<string, number>; inflight: number }
|
|
367
|
+
>();
|
|
368
|
+
|
|
298
369
|
let activeStreamCount = 0;
|
|
299
370
|
|
|
300
|
-
// Handle data from RSC payload
|
|
301
371
|
let handleData: HandleData = {};
|
|
302
372
|
let handleSegmentOrder: string[] = [];
|
|
373
|
+
let routeSegmentIds: string[] = [];
|
|
303
374
|
|
|
304
|
-
// Merged route params from current match
|
|
305
375
|
let routeParams: Record<string, string> = {};
|
|
306
376
|
|
|
307
|
-
// ========================================================================
|
|
308
|
-
// Listeners
|
|
309
|
-
// ========================================================================
|
|
310
|
-
|
|
311
377
|
const stateListeners = new Set<StateListener>();
|
|
312
378
|
const actionListeners = new Map<string, Set<ActionStateListener>>();
|
|
313
379
|
const handleListeners = new Set<HandleListener>();
|
|
314
380
|
|
|
315
|
-
|
|
316
|
-
let notifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
381
|
+
const notifyStateListeners = makeDebouncedNotifier(stateListeners);
|
|
317
382
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
383
|
+
// Memoized derived snapshot. Every state mutation already funnels through
|
|
384
|
+
// notify(), so invalidating here (synchronously, before the debounced fire)
|
|
385
|
+
// means a getState() call between two mutations reuses the same object — an
|
|
386
|
+
// unchanged state returns the SAME reference — while any real change recomputes
|
|
387
|
+
// on the next read. Kept null when dirty.
|
|
388
|
+
let cachedDerivedState: DerivedNavigationState | null = null;
|
|
389
|
+
|
|
390
|
+
function notify(): void {
|
|
391
|
+
cachedDerivedState = null;
|
|
392
|
+
notifyStateListeners();
|
|
326
393
|
}
|
|
327
394
|
|
|
328
|
-
// Debounce per-action notifications
|
|
329
395
|
const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
330
396
|
|
|
331
397
|
function notifyAction(actionId: string) {
|
|
@@ -337,8 +403,6 @@ export function createEventController(
|
|
|
337
403
|
actionId,
|
|
338
404
|
setTimeout(() => {
|
|
339
405
|
actionNotifyTimeouts.delete(actionId);
|
|
340
|
-
// Notify all listeners whose subscription ID matches this action
|
|
341
|
-
// This includes exact matches and suffix matches (e.g., "addToCart" matches "hash#addToCart")
|
|
342
406
|
for (const [subscriptionId, listeners] of actionListeners) {
|
|
343
407
|
if (matchesActionId(subscriptionId, actionId)) {
|
|
344
408
|
const state = getActionState(subscriptionId);
|
|
@@ -349,46 +413,34 @@ export function createEventController(
|
|
|
349
413
|
);
|
|
350
414
|
}
|
|
351
415
|
|
|
352
|
-
|
|
353
|
-
let handleNotifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
354
|
-
|
|
355
|
-
function notifyHandles() {
|
|
356
|
-
if (handleNotifyTimeout !== null) {
|
|
357
|
-
clearTimeout(handleNotifyTimeout);
|
|
358
|
-
}
|
|
359
|
-
handleNotifyTimeout = setTimeout(() => {
|
|
360
|
-
handleNotifyTimeout = null;
|
|
361
|
-
handleListeners.forEach((listener) => listener());
|
|
362
|
-
}, 0);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// ========================================================================
|
|
366
|
-
// Derived State
|
|
367
|
-
// ========================================================================
|
|
416
|
+
const notifyHandles = makeDebouncedNotifier(handleListeners);
|
|
368
417
|
|
|
369
418
|
function getState(): DerivedNavigationState {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
419
|
+
if (cachedDerivedState) return cachedDerivedState;
|
|
420
|
+
|
|
421
|
+
// Skip the spread/filter/map entirely when idle — the common case — and hand
|
|
422
|
+
// back the shared frozen empty list for referential stability.
|
|
423
|
+
const inflightActionsList: InflightAction[] =
|
|
424
|
+
inflightActions.size === 0
|
|
425
|
+
? EMPTY_INFLIGHT_ACTIONS
|
|
426
|
+
: [...inflightActions.values()]
|
|
427
|
+
.filter((a) => a.phase !== "settling")
|
|
428
|
+
.map((a) => ({
|
|
429
|
+
id: a.id,
|
|
430
|
+
actionId: a.actionId,
|
|
431
|
+
payload: a.payload,
|
|
432
|
+
startedAt: a.startedAt,
|
|
433
|
+
}));
|
|
434
|
+
|
|
382
435
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
383
436
|
const isVisibleNavigation =
|
|
384
437
|
currentNavigation !== null &&
|
|
385
438
|
!currentNavigation.options?.skipLoadingState;
|
|
386
439
|
const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
387
440
|
|
|
388
|
-
// Streaming: true if any active streams (navigation or action) or loading
|
|
389
441
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
390
442
|
|
|
391
|
-
|
|
443
|
+
cachedDerivedState = {
|
|
392
444
|
state,
|
|
393
445
|
isStreaming,
|
|
394
446
|
// True when a navigation is active (fetching or streaming, before
|
|
@@ -404,31 +456,28 @@ export function createEventController(
|
|
|
404
456
|
: null,
|
|
405
457
|
inflightActions: inflightActionsList,
|
|
406
458
|
};
|
|
459
|
+
return cachedDerivedState;
|
|
407
460
|
}
|
|
408
461
|
|
|
409
462
|
function getActionState(actionId: string): TrackedActionState {
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
)
|
|
416
|
-
.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
)
|
|
423
|
-
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
424
|
-
|
|
425
|
-
const entry = activeEntry || settlingEntry;
|
|
463
|
+
// Nothing inflight — skip building/scanning the list and return the shared
|
|
464
|
+
// idle snapshot (the same reference use-action falls back to).
|
|
465
|
+
if (inflightActions.size === 0) return DEFAULT_ACTION_STATE;
|
|
466
|
+
|
|
467
|
+
const entry = [...inflightActions.values()]
|
|
468
|
+
.filter((a) => matchesActionId(actionId, a.actionId))
|
|
469
|
+
.reduce<ActionEntry | undefined>((best, a) => {
|
|
470
|
+
if (!best) return a;
|
|
471
|
+
const aActive = a.phase !== "settling";
|
|
472
|
+
const bActive = best.phase !== "settling";
|
|
473
|
+
if (aActive !== bActive) return aActive ? a : best;
|
|
474
|
+
return a.startedAt > best.startedAt ? a : best;
|
|
475
|
+
}, undefined);
|
|
426
476
|
|
|
427
477
|
if (!entry) {
|
|
428
478
|
return { ...DEFAULT_ACTION_STATE };
|
|
429
479
|
}
|
|
430
480
|
|
|
431
|
-
// Derive state from phase
|
|
432
481
|
let state: ActionLifecycleState;
|
|
433
482
|
switch (entry.phase) {
|
|
434
483
|
case "fetching":
|
|
@@ -543,10 +592,29 @@ export function createEventController(
|
|
|
543
592
|
// Action Operations
|
|
544
593
|
// ========================================================================
|
|
545
594
|
|
|
546
|
-
function startAction(
|
|
595
|
+
function startAction(
|
|
596
|
+
actionId: string,
|
|
597
|
+
args: unknown[],
|
|
598
|
+
cohort?: string,
|
|
599
|
+
): ActionHandle {
|
|
547
600
|
const id = `${actionId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
601
|
+
// Private to this handle: never exposed on the returned ActionHandle.
|
|
602
|
+
const dispatchSeq = actionDispatchSeq++;
|
|
548
603
|
const abort = new AbortController();
|
|
549
604
|
|
|
605
|
+
// Register this action under its cohort (originating history entry). The
|
|
606
|
+
// cohort's arbitration is created on first use and freed when its last
|
|
607
|
+
// action settles (see doSettle). Keyless entries share the "" cohort.
|
|
608
|
+
const cohortId = cohort ?? "";
|
|
609
|
+
let arb = cohortArbitration.get(cohortId);
|
|
610
|
+
if (!arb) {
|
|
611
|
+
arb = { keySeq: new Map<string, number>(), inflight: 0 };
|
|
612
|
+
cohortArbitration.set(cohortId, arb);
|
|
613
|
+
}
|
|
614
|
+
// const so the captured reference stays non-undefined inside doSettle.
|
|
615
|
+
const arbitration = arb;
|
|
616
|
+
arbitration.inflight++;
|
|
617
|
+
|
|
550
618
|
// Track if this action started while others were pending (concurrent)
|
|
551
619
|
const hadConcurrent = inflightActions.size > 0;
|
|
552
620
|
if (hadConcurrent) {
|
|
@@ -570,11 +638,28 @@ export function createEventController(
|
|
|
570
638
|
let settled = false;
|
|
571
639
|
let streamingEnded = false;
|
|
572
640
|
let actionCompleted = false;
|
|
641
|
+
let cohortReleased = false;
|
|
573
642
|
let pendingResult:
|
|
574
643
|
| { type: "success"; value?: unknown }
|
|
575
644
|
| { type: "error"; value: unknown }
|
|
576
645
|
| null = null;
|
|
577
646
|
|
|
647
|
+
// Release this action's hold on its cohort arbitration exactly once: drop
|
|
648
|
+
// the refcount and, only if the map still points at THIS arbitration object,
|
|
649
|
+
// delete it. A newer generation may have replaced it (e.g. abortAllActions
|
|
650
|
+
// cleared the map and a fresh action recreated the same cohort id), so a
|
|
651
|
+
// stale settlement must never delete the newer one by id.
|
|
652
|
+
function releaseCohort() {
|
|
653
|
+
if (cohortReleased) return;
|
|
654
|
+
cohortReleased = true;
|
|
655
|
+
if (
|
|
656
|
+
--arbitration.inflight <= 0 &&
|
|
657
|
+
cohortArbitration.get(cohortId) === arbitration
|
|
658
|
+
) {
|
|
659
|
+
cohortArbitration.delete(cohortId);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
578
663
|
function doSettle() {
|
|
579
664
|
if (settled) return;
|
|
580
665
|
settled = true;
|
|
@@ -582,6 +667,9 @@ export function createEventController(
|
|
|
582
667
|
// Cleanup after brief delay (allow useAction to read result)
|
|
583
668
|
setTimeout(() => {
|
|
584
669
|
inflightActions.delete(id);
|
|
670
|
+
// Free this cohort's arbitration once its last action has settled, so
|
|
671
|
+
// a long-running action elsewhere cannot pin keys from a drained entry.
|
|
672
|
+
releaseCohort();
|
|
585
673
|
// Check for consolidation
|
|
586
674
|
if (inflightActions.size === 0) {
|
|
587
675
|
// All actions done - reset tracking
|
|
@@ -610,6 +698,19 @@ export function createEventController(
|
|
|
610
698
|
doSettle();
|
|
611
699
|
}
|
|
612
700
|
|
|
701
|
+
// streamingEnded is forced here for the "streaming never started" case so
|
|
702
|
+
// tryFinalize can run; otherwise the streaming token's end() finalizes.
|
|
703
|
+
function settleWith(result: NonNullable<typeof pendingResult>) {
|
|
704
|
+
if (!inflightActions.has(id) || settled) return;
|
|
705
|
+
actionCompleted = true;
|
|
706
|
+
entry.completed = true;
|
|
707
|
+
pendingResult = result;
|
|
708
|
+
if (entry.phase === "fetching" || streamingEnded) {
|
|
709
|
+
streamingEnded = true;
|
|
710
|
+
tryFinalize();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
613
714
|
return {
|
|
614
715
|
id,
|
|
615
716
|
abort,
|
|
@@ -645,36 +746,32 @@ export function createEventController(
|
|
|
645
746
|
segmentIds.forEach((id) => concurrentRevalidatedSegments.add(id));
|
|
646
747
|
},
|
|
647
748
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
749
|
+
claimLocationState(state: Record<string, unknown>) {
|
|
750
|
+
const winning: Record<string, unknown> = {};
|
|
751
|
+
// Arbitrate against this action's OWN captured arbitration object, not a
|
|
752
|
+
// live map lookup: a concurrent map clear/replace (abortAllActions, a
|
|
753
|
+
// stale settlement) must not make this action silently stop recording
|
|
754
|
+
// and accept every key.
|
|
755
|
+
const keySeq = arbitration.keySeq;
|
|
756
|
+
for (const key of Object.keys(state)) {
|
|
757
|
+
const prevSeq = keySeq.get(key);
|
|
758
|
+
// Strictly-greater: a later-initiated action wins a key over an
|
|
759
|
+
// earlier one in the same cohort regardless of arrival order. Equal
|
|
760
|
+
// cannot happen (dispatchSeq is unique per action).
|
|
761
|
+
if (prevSeq === undefined || dispatchSeq > prevSeq) {
|
|
762
|
+
keySeq.set(key, dispatchSeq);
|
|
763
|
+
winning[key] = state[key];
|
|
764
|
+
}
|
|
660
765
|
}
|
|
661
|
-
|
|
766
|
+
return winning;
|
|
662
767
|
},
|
|
663
768
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
actionCompleted = true;
|
|
668
|
-
entry.completed = true;
|
|
669
|
-
pendingResult = { type: "error", value: error };
|
|
769
|
+
complete(result?: unknown) {
|
|
770
|
+
settleWith({ type: "success", value: result });
|
|
771
|
+
},
|
|
670
772
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (entry.phase === "fetching" || streamingEnded) {
|
|
674
|
-
streamingEnded = true; // Mark as ended if never started
|
|
675
|
-
tryFinalize();
|
|
676
|
-
}
|
|
677
|
-
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
773
|
+
fail(error: unknown) {
|
|
774
|
+
settleWith({ type: "error", value: error });
|
|
678
775
|
},
|
|
679
776
|
|
|
680
777
|
getRevalidatedSegments(): Set<string> {
|
|
@@ -690,6 +787,10 @@ export function createEventController(
|
|
|
690
787
|
[Symbol.dispose]() {
|
|
691
788
|
// If aborted, another navigation/error took over - don't touch state
|
|
692
789
|
if (abort.signal.aborted) {
|
|
790
|
+
// Aborted actions skip doSettle, so release the cohort hold here to
|
|
791
|
+
// keep the per-cohort refcount balanced (no leak when an action is
|
|
792
|
+
// aborted individually rather than via abortAllActions).
|
|
793
|
+
releaseCohort();
|
|
693
794
|
inflightActions.delete(id);
|
|
694
795
|
notify();
|
|
695
796
|
notifyAction(actionId);
|
|
@@ -724,6 +825,7 @@ export function createEventController(
|
|
|
724
825
|
}
|
|
725
826
|
hadAnyConcurrentActions = false;
|
|
726
827
|
concurrentRevalidatedSegments.clear();
|
|
828
|
+
cohortArbitration.clear();
|
|
727
829
|
notify();
|
|
728
830
|
// Notify all action listeners directly by subscription ID.
|
|
729
831
|
// actionListeners keys are subscription IDs (possibly short names like
|
|
@@ -744,8 +846,13 @@ export function createEventController(
|
|
|
744
846
|
data: HandleData,
|
|
745
847
|
matched?: string[],
|
|
746
848
|
isPartial?: boolean,
|
|
849
|
+
resolvedIds?: string[],
|
|
747
850
|
): void {
|
|
748
|
-
const
|
|
851
|
+
const rawMatched = matched ?? [];
|
|
852
|
+
const newSegmentOrder = filterSegmentOrder(rawMatched);
|
|
853
|
+
// Separate list for useSegments(): "layouts and routes only" — strip
|
|
854
|
+
// parallels (".@") and loader sub-ids (D digit) without reordering.
|
|
855
|
+
const newRouteSegmentIds = filterRouteSegmentIds(rawMatched);
|
|
749
856
|
|
|
750
857
|
if (isPartial && newSegmentOrder.length > 0) {
|
|
751
858
|
// Partial update: merge new data with existing
|
|
@@ -757,10 +864,19 @@ export function createEventController(
|
|
|
757
864
|
handleData[handleName][segmentId] = data[handleName][segmentId];
|
|
758
865
|
}
|
|
759
866
|
}
|
|
760
|
-
|
|
867
|
+
const resolvedIdSet =
|
|
868
|
+
resolvedIds && resolvedIds.length > 0 ? new Set(resolvedIds) : null;
|
|
869
|
+
// Cleanup pass:
|
|
870
|
+
// a) segment dropped from the match list — delete its bucket.
|
|
871
|
+
// b) segment was re-resolved this request but pushed nothing for
|
|
872
|
+
// this handle — its previous bucket is stale.
|
|
873
|
+
// (a) is the existing behavior; (b) requires resolvedIds.
|
|
761
874
|
for (const handleName of Object.keys(handleData)) {
|
|
762
875
|
for (const segmentId of Object.keys(handleData[handleName])) {
|
|
763
|
-
|
|
876
|
+
const droppedFromMatch = !newSegmentOrder.includes(segmentId);
|
|
877
|
+
const reresolvedWithoutPush =
|
|
878
|
+
resolvedIdSet?.has(segmentId) && !data[handleName]?.[segmentId];
|
|
879
|
+
if (droppedFromMatch || reresolvedWithoutPush) {
|
|
764
880
|
delete handleData[handleName][segmentId];
|
|
765
881
|
}
|
|
766
882
|
}
|
|
@@ -770,6 +886,7 @@ export function createEventController(
|
|
|
770
886
|
handleData = data;
|
|
771
887
|
}
|
|
772
888
|
handleSegmentOrder = newSegmentOrder;
|
|
889
|
+
routeSegmentIds = newRouteSegmentIds;
|
|
773
890
|
|
|
774
891
|
notifyHandles();
|
|
775
892
|
}
|
|
@@ -778,9 +895,22 @@ export function createEventController(
|
|
|
778
895
|
return {
|
|
779
896
|
data: handleData,
|
|
780
897
|
segmentOrder: handleSegmentOrder,
|
|
898
|
+
routeSegmentIds,
|
|
781
899
|
};
|
|
782
900
|
}
|
|
783
901
|
|
|
902
|
+
function setRouteSegmentIds(matched: string[]): void {
|
|
903
|
+
const next = filterRouteSegmentIds(matched);
|
|
904
|
+
if (
|
|
905
|
+
next.length === routeSegmentIds.length &&
|
|
906
|
+
next.every((id, i) => id === routeSegmentIds[i])
|
|
907
|
+
) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
routeSegmentIds = next;
|
|
911
|
+
notifyHandles();
|
|
912
|
+
}
|
|
913
|
+
|
|
784
914
|
// ========================================================================
|
|
785
915
|
// Subscriptions
|
|
786
916
|
// ========================================================================
|
|
@@ -849,6 +979,7 @@ export function createEventController(
|
|
|
849
979
|
// Handles
|
|
850
980
|
setHandleData,
|
|
851
981
|
getHandleState,
|
|
982
|
+
setRouteSegmentIds,
|
|
852
983
|
|
|
853
984
|
// Params
|
|
854
985
|
setParams,
|
|
@@ -865,40 +996,3 @@ export function createEventController(
|
|
|
865
996
|
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
866
997
|
};
|
|
867
998
|
}
|
|
868
|
-
|
|
869
|
-
// ============================================================================
|
|
870
|
-
// Singleton
|
|
871
|
-
// ============================================================================
|
|
872
|
-
|
|
873
|
-
let controllerInstance: EventController | null = null;
|
|
874
|
-
|
|
875
|
-
/**
|
|
876
|
-
* Initialize the global event controller
|
|
877
|
-
*/
|
|
878
|
-
export function initEventController(
|
|
879
|
-
config?: EventControllerConfig,
|
|
880
|
-
): EventController {
|
|
881
|
-
if (!controllerInstance) {
|
|
882
|
-
controllerInstance = createEventController(config);
|
|
883
|
-
}
|
|
884
|
-
return controllerInstance;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
/**
|
|
888
|
-
* Get the global event controller
|
|
889
|
-
*/
|
|
890
|
-
export function getEventController(): EventController {
|
|
891
|
-
if (!controllerInstance) {
|
|
892
|
-
throw new Error(
|
|
893
|
-
"Event controller not initialized. Call initEventController first.",
|
|
894
|
-
);
|
|
895
|
-
}
|
|
896
|
-
return controllerInstance;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
/**
|
|
900
|
-
* Reset the controller instance (for testing)
|
|
901
|
-
*/
|
|
902
|
-
export function resetEventController(): void {
|
|
903
|
-
controllerInstance = null;
|
|
904
|
-
}
|
|
@@ -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
|