@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140
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 +17 -0
- package/README.md +432 -7
- package/dist/bin/rango.js +2073 -213
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +7258 -2714
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +140 -67
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +329 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +487 -0
- package/skills/caching/SKILL.md +357 -25
- 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 +246 -0
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +16 -10
- package/skills/document-cache/SKILL.md +87 -62
- package/skills/fonts/SKILL.md +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +557 -79
- package/skills/host-router/SKILL.md +320 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +207 -15
- package/skills/layout/SKILL.md +146 -6
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +616 -54
- package/skills/middleware/SKILL.md +217 -37
- package/skills/migrate-nextjs/SKILL.md +611 -0
- package/skills/migrate-react-router/SKILL.md +927 -0
- package/skills/mime-routes/SKILL.md +42 -11
- package/skills/observability/SKILL.md +194 -0
- package/skills/parallel/SKILL.md +284 -3
- package/skills/ppr/SKILL.md +426 -0
- package/skills/prerender/SKILL.md +437 -52
- package/skills/rango/SKILL.md +369 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +263 -121
- package/skills/route/SKILL.md +350 -21
- package/skills/router-setup/SKILL.md +246 -33
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +126 -222
- 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 +9 -8
- package/skills/typesafety/SKILL.md +532 -103
- package/skills/use-cache/SKILL.md +367 -0
- 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 +77 -44
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +114 -0
- 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 +293 -202
- package/src/browser/history-state.ts +101 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +11 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +385 -576
- package/src/browser/navigation-client.ts +245 -75
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +184 -118
- package/src/browser/navigation-transaction.ts +247 -0
- package/src/browser/network-error-handler.ts +88 -0
- package/src/browser/partial-update.ts +412 -364
- package/src/browser/prefetch/cache.ts +359 -0
- package/src/browser/prefetch/fetch.ts +452 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +209 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +194 -0
- package/src/browser/react/Link.tsx +275 -68
- package/src/browser/react/NavigationProvider.tsx +265 -109
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +70 -0
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +272 -60
- package/src/browser/react/location-state.ts +90 -20
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +35 -66
- package/src/browser/react/use-handle.ts +39 -126
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +39 -13
- package/src/browser/react/use-navigation.ts +53 -69
- package/src/browser/react/use-params.ts +75 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +98 -0
- package/src/browser/react/use-search-params.ts +51 -0
- package/src/browser/react/use-segments.ts +72 -99
- package/src/browser/response-adapter.ts +164 -0
- package/src/browser/rsc-router.tsx +300 -72
- package/src/browser/scroll-restoration.ts +138 -50
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +17 -1
- package/src/browser/server-action-bridge.ts +668 -613
- package/src/browser/types.ts +223 -51
- package/src/browser/validate-redirect-origin.ts +56 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +252 -161
- package/src/build/generate-route-types.ts +41 -1038
- package/src/build/index.ts +12 -7
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +225 -42
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +105 -0
- package/src/build/route-types/codegen.ts +113 -0
- package/src/build/route-types/include-resolution.ts +812 -0
- package/src/build/route-types/param-extraction.ts +51 -0
- package/src/build/route-types/per-module-writer.ts +144 -0
- package/src/build/route-types/router-processing.ts +695 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +223 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +60 -0
- package/src/cache/cache-policy.ts +199 -0
- package/src/cache/cache-runtime.ts +525 -0
- package/src/cache/cache-scope.ts +298 -332
- 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 +2500 -158
- 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 +17 -17
- package/src/cache/document-cache.ts +199 -92
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +111 -0
- package/src/cache/index.ts +29 -35
- package/src/cache/memory-segment-store.ts +363 -30
- package/src/cache/profile-registry.ts +88 -0
- package/src/cache/read-through-swr.ts +178 -0
- package/src/cache/segment-codec.ts +248 -0
- package/src/cache/shell-cache.ts +386 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +156 -211
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1102 -0
- package/src/client.rsc.tsx +43 -21
- package/src/client.tsx +131 -347
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +23 -4
- package/src/components/DefaultDocument.tsx +13 -3
- package/src/context-var.ts +168 -0
- package/src/debug.ts +19 -9
- 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 +106 -10
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +110 -35
- package/src/handles/MetaTags.tsx +83 -59
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +93 -0
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +44 -53
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +20 -65
- package/src/host/errors.ts +21 -30
- package/src/host/index.ts +13 -9
- package/src/host/pattern-matcher.ts +50 -79
- package/src/host/router.ts +151 -121
- package/src/host/testing.ts +45 -32
- package/src/host/types.ts +52 -11
- package/src/host/utils.ts +2 -2
- package/src/href-client.ts +192 -57
- package/src/index.rsc.ts +177 -35
- package/src/index.ts +255 -71
- package/src/internal-debug.ts +9 -2
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +31 -99
- package/src/loader.ts +30 -12
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +41 -0
- package/src/prerender/param-hash.ts +16 -14
- package/src/prerender/store.ts +121 -21
- package/src/prerender.ts +460 -26
- 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 +198 -128
- package/src/root-error-boundary.tsx +42 -48
- package/src/route-content-wrapper.tsx +22 -77
- package/src/route-definition/dsl-helpers.ts +1116 -0
- package/src/route-definition/helper-factories.ts +88 -0
- package/src/route-definition/helpers-types.ts +505 -0
- package/src/route-definition/index.ts +54 -0
- package/src/route-definition/redirect.ts +134 -0
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-definition.ts +1 -1481
- package/src/route-map-builder.ts +82 -144
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +71 -45
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +263 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +54 -27
- package/src/router/find-match.ts +245 -0
- package/src/router/handler-context.ts +377 -125
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +59 -28
- package/src/router/lazy-includes.ts +254 -0
- package/src/router/loader-resolution.ts +421 -157
- package/src/router/logging.ts +106 -6
- package/src/router/manifest.ts +131 -57
- package/src/router/match-api.ts +167 -246
- package/src/router/match-context.ts +4 -24
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +117 -93
- package/src/router/match-middleware/cache-lookup.ts +297 -150
- package/src/router/match-middleware/cache-store.ts +123 -51
- package/src/router/match-middleware/intercept-resolution.ts +44 -43
- package/src/router/match-middleware/segment-resolution.ts +64 -22
- package/src/router/match-pipelines.ts +11 -87
- package/src/router/match-result.ts +121 -50
- package/src/router/metrics.ts +219 -28
- package/src/router/middleware-types.ts +93 -0
- package/src/router/middleware.ts +505 -441
- 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 +311 -142
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +547 -0
- package/src/router/preview-match.ts +102 -0
- package/src/router/request-classification.ts +278 -0
- package/src/router/revalidation.ts +203 -62
- package/src/router/route-snapshot.ts +246 -0
- package/src/router/router-context.ts +45 -48
- package/src/router/router-interfaces.ts +554 -0
- package/src/router/router-options.ts +779 -0
- package/src/router/router-registry.ts +21 -0
- package/src/router/segment-resolution/fresh.ts +772 -0
- package/src/router/segment-resolution/helpers.ts +348 -0
- package/src/router/segment-resolution/loader-cache.ts +250 -0
- package/src/router/segment-resolution/loader-mask.ts +44 -0
- package/src/router/segment-resolution/revalidation.ts +1331 -0
- package/src/router/segment-resolution/static-store.ts +81 -0
- 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 +25 -1354
- package/src/router/segment-wrappers.ts +292 -0
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +75 -0
- package/src/router/telemetry-otel.ts +261 -0
- package/src/router/telemetry.ts +377 -0
- package/src/router/timeout.ts +128 -0
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +240 -61
- package/src/router/types.ts +23 -70
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +781 -2378
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +46 -0
- package/src/rsc/handler.ts +905 -1142
- package/src/rsc/helpers.ts +275 -19
- package/src/rsc/index.ts +2 -25
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +305 -0
- package/src/rsc/manifest-init.ts +77 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +155 -0
- package/src/rsc/progressive-enhancement.ts +502 -0
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +104 -0
- package/src/rsc/response-route-handler.ts +257 -0
- package/src/rsc/rsc-rendering.ts +337 -0
- package/src/rsc/runtime-warnings.ts +55 -0
- package/src/rsc/server-action.ts +522 -0
- package/src/rsc/shell-capture.ts +439 -0
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +95 -12
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +99 -82
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +349 -134
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +452 -85
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +310 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +123 -42
- package/src/server/live.ts +130 -0
- package/src/server/loader-registry.ts +51 -100
- package/src/server/request-context.ts +842 -157
- package/src/server.ts +15 -8
- package/src/ssr/index.tsx +412 -136
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +45 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -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 +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 +40 -72
- package/src/theme/ThemeScript.tsx +12 -14
- package/src/theme/constants.ts +57 -15
- package/src/theme/index.ts +3 -20
- package/src/theme/theme-context.ts +5 -35
- package/src/theme/theme-script.ts +43 -39
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +123 -0
- package/src/types/cache-types.ts +207 -0
- package/src/types/error-types.ts +132 -0
- package/src/types/global-namespace.ts +113 -0
- package/src/types/handler-context.ts +839 -0
- package/src/types/index.ts +81 -0
- package/src/types/loader-types.ts +212 -0
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +138 -0
- package/src/types/route-entry.ts +114 -0
- package/src/types/segments.ts +271 -0
- package/src/types.ts +1 -1795
- package/src/urls/include-helper.ts +162 -0
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +43 -0
- package/src/urls/path-helper-types.ts +413 -0
- package/src/urls/path-helper.ts +275 -0
- package/src/urls/pattern-types.ts +124 -0
- package/src/urls/response-types.ts +109 -0
- package/src/urls/type-extraction.ts +316 -0
- package/src/urls/urls-function.ts +80 -0
- package/src/urls.ts +1 -1341
- package/src/use-loader.tsx +406 -141
- 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 +182 -0
- package/src/vite/discovery/discover-routers.ts +389 -0
- 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 +467 -0
- package/src/vite/discovery/route-types-writer.ts +214 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +161 -0
- package/src/vite/discovery/virtual-module-codegen.ts +183 -0
- package/src/vite/index.ts +23 -2255
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +303 -0
- package/src/vite/plugins/cjs-to-esm.ts +90 -0
- package/src/vite/plugins/client-ref-dedup.ts +120 -0
- package/src/vite/plugins/client-ref-hashing.ts +118 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
- package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
- package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +805 -0
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +313 -0
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +94 -0
- package/src/vite/plugins/version-plugin.ts +263 -0
- package/src/vite/plugins/virtual-entries.ts +234 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +560 -0
- package/src/vite/router-discovery.ts +1638 -0
- package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +132 -0
- 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 +15 -0
- package/src/vite/utils/package-resolution.ts +89 -0
- package/src/vite/utils/prerender-utils.ts +249 -0
- package/src/vite/utils/shared-utils.ts +269 -0
- package/CLAUDE.md +0 -43
- package/dist/vite/index.named-routes.gen.ts +0 -103
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/react/use-client-cache.ts +0 -56
- package/src/browser/request-controller.ts +0 -164
- package/src/browser/shallow.ts +0 -35
- package/src/cache/memory-store.ts +0 -253
- package/src/handles/index.ts +0 -6
- package/src/href-context.ts +0 -33
- package/src/network-error-thrower.tsx +0 -21
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-internal-ids.ts +0 -1167
- package/src/vite/package-resolution.ts +0 -125
- package/src/vite/virtual-entries.ts +0 -114
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -8,7 +8,12 @@ import type {
|
|
|
8
8
|
ResolvedSegment,
|
|
9
9
|
RscMetadata,
|
|
10
10
|
HandleData,
|
|
11
|
+
StreamingToken,
|
|
11
12
|
} from "./types.js";
|
|
13
|
+
import {
|
|
14
|
+
filterSegmentOrder,
|
|
15
|
+
filterRouteSegmentIds,
|
|
16
|
+
} from "./react/filter-segment-order.js";
|
|
12
17
|
|
|
13
18
|
// Polyfill Symbol.dispose for Safari and older browsers
|
|
14
19
|
if (typeof Symbol.dispose === "undefined") {
|
|
@@ -40,7 +45,7 @@ export interface NavigationEntry {
|
|
|
40
45
|
abort: AbortController;
|
|
41
46
|
phase: NavigationPhase;
|
|
42
47
|
startedAt: number;
|
|
43
|
-
options?: NavigateOptions;
|
|
48
|
+
options?: NavigateOptions & { skipLoadingState?: boolean };
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
/**
|
|
@@ -77,6 +82,8 @@ export interface DerivedNavigationState {
|
|
|
77
82
|
state: "idle" | "loading";
|
|
78
83
|
/** Whether any operation is streaming */
|
|
79
84
|
isStreaming: boolean;
|
|
85
|
+
/** Whether a navigation is active (fetching or streaming, before commit) */
|
|
86
|
+
isNavigating: boolean;
|
|
80
87
|
/** Current committed location */
|
|
81
88
|
location: NavigationLocation;
|
|
82
89
|
/** URL being navigated to (null if idle) */
|
|
@@ -109,20 +116,24 @@ export type ActionStateListener = (state: TrackedActionState) => void;
|
|
|
109
116
|
export type HandleListener = () => void;
|
|
110
117
|
|
|
111
118
|
/**
|
|
112
|
-
* 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.
|
|
113
132
|
*/
|
|
114
133
|
export interface HandleState {
|
|
115
134
|
data: HandleData;
|
|
116
135
|
segmentOrder: string[];
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Token for tracking an active stream
|
|
121
|
-
* Call end() when the stream completes
|
|
122
|
-
*/
|
|
123
|
-
export interface StreamingToken {
|
|
124
|
-
/** End this streaming operation */
|
|
125
|
-
end(): void;
|
|
136
|
+
routeSegmentIds: string[];
|
|
126
137
|
}
|
|
127
138
|
|
|
128
139
|
/**
|
|
@@ -157,6 +168,22 @@ export interface ActionHandle extends Disposable {
|
|
|
157
168
|
startStreaming(): StreamingToken;
|
|
158
169
|
/** Record segments that were revalidated */
|
|
159
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>;
|
|
160
187
|
/** Complete the action with result */
|
|
161
188
|
complete(result?: unknown): void;
|
|
162
189
|
/** Fail the action with error */
|
|
@@ -165,8 +192,8 @@ export interface ActionHandle extends Disposable {
|
|
|
165
192
|
readonly settled: boolean;
|
|
166
193
|
/** Check if any concurrent actions were started */
|
|
167
194
|
hadConcurrentActions: boolean;
|
|
168
|
-
/** Get
|
|
169
|
-
|
|
195
|
+
/** Get raw set of segments revalidated by concurrent actions */
|
|
196
|
+
getRevalidatedSegments(): Set<string>;
|
|
170
197
|
/** Clear consolidation tracking */
|
|
171
198
|
clearConsolidation(): void;
|
|
172
199
|
}
|
|
@@ -176,16 +203,25 @@ export interface ActionHandle extends Disposable {
|
|
|
176
203
|
*/
|
|
177
204
|
export interface EventController {
|
|
178
205
|
// Navigation operations
|
|
179
|
-
startNavigation(
|
|
206
|
+
startNavigation(
|
|
207
|
+
url: string,
|
|
208
|
+
options?: NavigateOptions & { skipLoadingState?: boolean },
|
|
209
|
+
): NavigationHandle;
|
|
180
210
|
abortNavigation(): void;
|
|
181
211
|
|
|
182
212
|
// Action operations
|
|
183
|
-
startAction(
|
|
213
|
+
startAction(
|
|
214
|
+
actionId: string,
|
|
215
|
+
args: unknown[],
|
|
216
|
+
/** Originating history entry key; scopes location-state arbitration. */
|
|
217
|
+
cohort?: string,
|
|
218
|
+
): ActionHandle;
|
|
184
219
|
abortAllActions(): void;
|
|
185
220
|
|
|
186
221
|
// State access
|
|
187
222
|
getState(): DerivedNavigationState;
|
|
188
223
|
getActionState(actionId: string): TrackedActionState;
|
|
224
|
+
getLocation(): NavigationLocation;
|
|
189
225
|
|
|
190
226
|
// Location updates (for popstate where navigation doesn't go through startNavigation)
|
|
191
227
|
setLocation(location: NavigationLocation): void;
|
|
@@ -194,7 +230,7 @@ export interface EventController {
|
|
|
194
230
|
subscribe(listener: StateListener): () => void;
|
|
195
231
|
subscribeToAction(
|
|
196
232
|
actionId: string,
|
|
197
|
-
listener: ActionStateListener
|
|
233
|
+
listener: ActionStateListener,
|
|
198
234
|
): () => void;
|
|
199
235
|
subscribeToHandles(listener: HandleListener): () => void;
|
|
200
236
|
|
|
@@ -202,19 +238,37 @@ export interface EventController {
|
|
|
202
238
|
setHandleData(
|
|
203
239
|
data: HandleData,
|
|
204
240
|
matched?: string[],
|
|
205
|
-
isPartial?: boolean
|
|
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[],
|
|
206
250
|
): void;
|
|
207
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;
|
|
260
|
+
|
|
261
|
+
// Params operations
|
|
262
|
+
setParams(params: Record<string, string>): void;
|
|
263
|
+
getParams(): Record<string, string>;
|
|
208
264
|
|
|
209
265
|
// Direct state access for advanced use
|
|
210
266
|
getCurrentNavigation(): NavigationEntry | null;
|
|
211
267
|
getInflightActions(): Map<string, ActionEntry>;
|
|
268
|
+
/** Whether any concurrent actions have occurred (shared across all handles) */
|
|
269
|
+
hadAnyConcurrentActions(): boolean;
|
|
212
270
|
}
|
|
213
271
|
|
|
214
|
-
// ============================================================================
|
|
215
|
-
// Default States
|
|
216
|
-
// ============================================================================
|
|
217
|
-
|
|
218
272
|
const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
219
273
|
state: "idle",
|
|
220
274
|
actionId: null,
|
|
@@ -230,22 +284,28 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
230
284
|
* When subscriptionId has no '#', it's just an action name and matches by suffix.
|
|
231
285
|
* This allows useAction("addToCart") to match "hash#addToCart" or "src/file.ts#addToCart".
|
|
232
286
|
*/
|
|
233
|
-
function matchesActionId(
|
|
287
|
+
function matchesActionId(
|
|
288
|
+
subscriptionId: string,
|
|
289
|
+
entryActionId: string,
|
|
290
|
+
): boolean {
|
|
234
291
|
if (subscriptionId.includes("#")) {
|
|
235
|
-
// Full ID: exact match
|
|
236
292
|
return subscriptionId === entryActionId;
|
|
237
293
|
}
|
|
238
|
-
// Action name only: suffix match (matches "anything#actionName")
|
|
239
294
|
return entryActionId.endsWith(`#${subscriptionId}`);
|
|
240
295
|
}
|
|
241
296
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
297
|
+
// Batch rapid notifications into one microtask to prevent render storms
|
|
298
|
+
function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
299
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
300
|
+
return () => {
|
|
301
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
302
|
+
timeout = setTimeout(() => {
|
|
303
|
+
timeout = null;
|
|
304
|
+
listeners.forEach((listener) => listener());
|
|
305
|
+
}, 0);
|
|
306
|
+
};
|
|
307
|
+
}
|
|
245
308
|
|
|
246
|
-
/**
|
|
247
|
-
* Configuration for creating an event controller
|
|
248
|
-
*/
|
|
249
309
|
export interface EventControllerConfig {
|
|
250
310
|
initialLocation?: NavigationLocation;
|
|
251
311
|
}
|
|
@@ -261,60 +321,58 @@ export interface EventControllerConfig {
|
|
|
261
321
|
* Actions use mergeMap semantics (all run concurrently, consolidate at end).
|
|
262
322
|
*/
|
|
263
323
|
export function createEventController(
|
|
264
|
-
config?: EventControllerConfig
|
|
324
|
+
config?: EventControllerConfig,
|
|
265
325
|
): EventController {
|
|
266
|
-
// ========================================================================
|
|
267
|
-
// Source of Truth
|
|
268
|
-
// ========================================================================
|
|
269
|
-
|
|
270
|
-
// Current navigation in progress (null = idle)
|
|
271
326
|
let currentNavigation: NavigationEntry | null = null;
|
|
272
327
|
|
|
273
|
-
// All in-flight actions (keyed by unique instance ID)
|
|
274
328
|
const inflightActions = new Map<string, ActionEntry>();
|
|
275
329
|
|
|
276
|
-
// Committed location (updated when navigation completes)
|
|
277
330
|
let location: NavigationLocation =
|
|
278
331
|
config?.initialLocation ??
|
|
279
332
|
(typeof window !== "undefined"
|
|
280
333
|
? new URL(window.location.href)
|
|
281
334
|
: new URL("/", "http://localhost"));
|
|
282
335
|
|
|
283
|
-
// Track if any concurrent actions occurred (for consolidation)
|
|
284
336
|
let hadAnyConcurrentActions = false;
|
|
285
337
|
|
|
286
|
-
// Track segments revalidated by concurrent actions
|
|
287
338
|
const concurrentRevalidatedSegments = new Set<string>();
|
|
288
339
|
|
|
289
|
-
//
|
|
340
|
+
// Monotonic dispatch counter: every startAction() takes the next value
|
|
341
|
+
// (private), so a larger sequence means the action was initiated later.
|
|
342
|
+
let actionDispatchSeq = 0;
|
|
343
|
+
|
|
344
|
+
// Concurrent location-state arbitration, scoped PER COHORT (history entry).
|
|
345
|
+
// Each cohort owns a slotKey->winningDispatchSeq map plus a refcount of its
|
|
346
|
+
// inflight actions; claimLocationState() consults its own cohort's map so
|
|
347
|
+
// same-key writes within one entry resolve to the last-initiated action
|
|
348
|
+
// regardless of settle order, while actions on different entries never
|
|
349
|
+
// compete. A cohort's map is freed once its LAST action's cleanup runs — the
|
|
350
|
+
// same brief post-settle grace (doSettle's 100ms timer) as other action
|
|
351
|
+
// teardown, not when every action everywhere settles — so a long-running
|
|
352
|
+
// action in one cohort can never retain arbitration keys from other cohorts
|
|
353
|
+
// that have since drained. It is never cleared in clearConsolidation, which
|
|
354
|
+
// fires
|
|
355
|
+
// per-action on divert/error and would let a later-settling earlier action
|
|
356
|
+
// wrongly reclaim a key a sibling already won.
|
|
357
|
+
const cohortArbitration = new Map<
|
|
358
|
+
string,
|
|
359
|
+
{ keySeq: Map<string, number>; inflight: number }
|
|
360
|
+
>();
|
|
361
|
+
|
|
290
362
|
let activeStreamCount = 0;
|
|
291
363
|
|
|
292
|
-
// Handle data from RSC payload
|
|
293
364
|
let handleData: HandleData = {};
|
|
294
365
|
let handleSegmentOrder: string[] = [];
|
|
366
|
+
let routeSegmentIds: string[] = [];
|
|
295
367
|
|
|
296
|
-
|
|
297
|
-
// Listeners
|
|
298
|
-
// ========================================================================
|
|
368
|
+
let routeParams: Record<string, string> = {};
|
|
299
369
|
|
|
300
370
|
const stateListeners = new Set<StateListener>();
|
|
301
371
|
const actionListeners = new Map<string, Set<ActionStateListener>>();
|
|
302
372
|
const handleListeners = new Set<HandleListener>();
|
|
303
373
|
|
|
304
|
-
|
|
305
|
-
let notifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
306
|
-
|
|
307
|
-
function notify() {
|
|
308
|
-
if (notifyTimeout !== null) {
|
|
309
|
-
clearTimeout(notifyTimeout);
|
|
310
|
-
}
|
|
311
|
-
notifyTimeout = setTimeout(() => {
|
|
312
|
-
notifyTimeout = null;
|
|
313
|
-
stateListeners.forEach((listener) => listener());
|
|
314
|
-
}, 0);
|
|
315
|
-
}
|
|
374
|
+
const notify = makeDebouncedNotifier(stateListeners);
|
|
316
375
|
|
|
317
|
-
// Debounce per-action notifications
|
|
318
376
|
const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
319
377
|
|
|
320
378
|
function notifyAction(actionId: string) {
|
|
@@ -326,37 +384,19 @@ export function createEventController(
|
|
|
326
384
|
actionId,
|
|
327
385
|
setTimeout(() => {
|
|
328
386
|
actionNotifyTimeouts.delete(actionId);
|
|
329
|
-
// Notify all listeners whose subscription ID matches this action
|
|
330
|
-
// This includes exact matches and suffix matches (e.g., "addToCart" matches "hash#addToCart")
|
|
331
387
|
for (const [subscriptionId, listeners] of actionListeners) {
|
|
332
388
|
if (matchesActionId(subscriptionId, actionId)) {
|
|
333
389
|
const state = getActionState(subscriptionId);
|
|
334
390
|
listeners.forEach((listener) => listener(state));
|
|
335
391
|
}
|
|
336
392
|
}
|
|
337
|
-
}, 0)
|
|
393
|
+
}, 0),
|
|
338
394
|
);
|
|
339
395
|
}
|
|
340
396
|
|
|
341
|
-
|
|
342
|
-
let handleNotifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
343
|
-
|
|
344
|
-
function notifyHandles() {
|
|
345
|
-
if (handleNotifyTimeout !== null) {
|
|
346
|
-
clearTimeout(handleNotifyTimeout);
|
|
347
|
-
}
|
|
348
|
-
handleNotifyTimeout = setTimeout(() => {
|
|
349
|
-
handleNotifyTimeout = null;
|
|
350
|
-
handleListeners.forEach((listener) => listener());
|
|
351
|
-
}, 0);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ========================================================================
|
|
355
|
-
// Derived State
|
|
356
|
-
// ========================================================================
|
|
397
|
+
const notifyHandles = makeDebouncedNotifier(handleListeners);
|
|
357
398
|
|
|
358
399
|
function getState(): DerivedNavigationState {
|
|
359
|
-
// Build inflight actions list (for compatibility with existing API)
|
|
360
400
|
const inflightActionsList: InflightAction[] = [...inflightActions.values()]
|
|
361
401
|
.filter((a) => a.phase !== "settling")
|
|
362
402
|
.map((a) => ({
|
|
@@ -366,43 +406,47 @@ export function createEventController(
|
|
|
366
406
|
startedAt: a.startedAt,
|
|
367
407
|
}));
|
|
368
408
|
|
|
369
|
-
// State: loading if navigation OR actions are in progress
|
|
370
409
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
371
|
-
const
|
|
372
|
-
currentNavigation !== null
|
|
410
|
+
const isVisibleNavigation =
|
|
411
|
+
currentNavigation !== null &&
|
|
412
|
+
!currentNavigation.options?.skipLoadingState;
|
|
413
|
+
const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
373
414
|
|
|
374
|
-
// Streaming: true if any active streams (navigation or action) or loading
|
|
375
415
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
376
416
|
|
|
377
417
|
return {
|
|
378
418
|
state,
|
|
379
419
|
isStreaming,
|
|
420
|
+
// True when a navigation is active (fetching or streaming, before
|
|
421
|
+
// commit). Broader than pendingUrl which clears during streaming.
|
|
422
|
+
isNavigating: currentNavigation !== null,
|
|
380
423
|
location,
|
|
381
|
-
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending
|
|
382
|
-
|
|
424
|
+
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
|
|
425
|
+
// Background revalidations (skipLoadingState) don't expose a pending URL.
|
|
426
|
+
pendingUrl:
|
|
427
|
+
currentNavigation?.phase === "fetching" &&
|
|
428
|
+
!currentNavigation.options?.skipLoadingState
|
|
429
|
+
? currentNavigation.url
|
|
430
|
+
: null,
|
|
383
431
|
inflightActions: inflightActionsList,
|
|
384
432
|
};
|
|
385
433
|
}
|
|
386
434
|
|
|
387
435
|
function getActionState(actionId: string): TrackedActionState {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
398
|
-
|
|
399
|
-
const entry = activeEntry || settlingEntry;
|
|
436
|
+
const entry = [...inflightActions.values()]
|
|
437
|
+
.filter((a) => matchesActionId(actionId, a.actionId))
|
|
438
|
+
.reduce<ActionEntry | undefined>((best, a) => {
|
|
439
|
+
if (!best) return a;
|
|
440
|
+
const aActive = a.phase !== "settling";
|
|
441
|
+
const bActive = best.phase !== "settling";
|
|
442
|
+
if (aActive !== bActive) return aActive ? a : best;
|
|
443
|
+
return a.startedAt > best.startedAt ? a : best;
|
|
444
|
+
}, undefined);
|
|
400
445
|
|
|
401
446
|
if (!entry) {
|
|
402
447
|
return { ...DEFAULT_ACTION_STATE };
|
|
403
448
|
}
|
|
404
449
|
|
|
405
|
-
// Derive state from phase
|
|
406
450
|
let state: ActionLifecycleState;
|
|
407
451
|
switch (entry.phase) {
|
|
408
452
|
case "fetching":
|
|
@@ -431,7 +475,7 @@ export function createEventController(
|
|
|
431
475
|
|
|
432
476
|
function startNavigation(
|
|
433
477
|
url: string,
|
|
434
|
-
options?: NavigateOptions
|
|
478
|
+
options?: NavigateOptions & { skipLoadingState?: boolean },
|
|
435
479
|
): NavigationHandle {
|
|
436
480
|
// Cancel existing navigation (switchMap semantics)
|
|
437
481
|
if (currentNavigation) {
|
|
@@ -463,6 +507,7 @@ export function createEventController(
|
|
|
463
507
|
|
|
464
508
|
startStreaming(): StreamingToken {
|
|
465
509
|
let ended = false;
|
|
510
|
+
entry.phase = "streaming";
|
|
466
511
|
activeStreamCount++;
|
|
467
512
|
notify();
|
|
468
513
|
|
|
@@ -516,10 +561,29 @@ export function createEventController(
|
|
|
516
561
|
// Action Operations
|
|
517
562
|
// ========================================================================
|
|
518
563
|
|
|
519
|
-
function startAction(
|
|
564
|
+
function startAction(
|
|
565
|
+
actionId: string,
|
|
566
|
+
args: unknown[],
|
|
567
|
+
cohort?: string,
|
|
568
|
+
): ActionHandle {
|
|
520
569
|
const id = `${actionId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
570
|
+
// Private to this handle: never exposed on the returned ActionHandle.
|
|
571
|
+
const dispatchSeq = actionDispatchSeq++;
|
|
521
572
|
const abort = new AbortController();
|
|
522
573
|
|
|
574
|
+
// Register this action under its cohort (originating history entry). The
|
|
575
|
+
// cohort's arbitration is created on first use and freed when its last
|
|
576
|
+
// action settles (see doSettle). Keyless entries share the "" cohort.
|
|
577
|
+
const cohortId = cohort ?? "";
|
|
578
|
+
let arb = cohortArbitration.get(cohortId);
|
|
579
|
+
if (!arb) {
|
|
580
|
+
arb = { keySeq: new Map<string, number>(), inflight: 0 };
|
|
581
|
+
cohortArbitration.set(cohortId, arb);
|
|
582
|
+
}
|
|
583
|
+
// const so the captured reference stays non-undefined inside doSettle.
|
|
584
|
+
const arbitration = arb;
|
|
585
|
+
arbitration.inflight++;
|
|
586
|
+
|
|
523
587
|
// Track if this action started while others were pending (concurrent)
|
|
524
588
|
const hadConcurrent = inflightActions.size > 0;
|
|
525
589
|
if (hadConcurrent) {
|
|
@@ -543,11 +607,28 @@ export function createEventController(
|
|
|
543
607
|
let settled = false;
|
|
544
608
|
let streamingEnded = false;
|
|
545
609
|
let actionCompleted = false;
|
|
610
|
+
let cohortReleased = false;
|
|
546
611
|
let pendingResult:
|
|
547
612
|
| { type: "success"; value?: unknown }
|
|
548
613
|
| { type: "error"; value: unknown }
|
|
549
614
|
| null = null;
|
|
550
615
|
|
|
616
|
+
// Release this action's hold on its cohort arbitration exactly once: drop
|
|
617
|
+
// the refcount and, only if the map still points at THIS arbitration object,
|
|
618
|
+
// delete it. A newer generation may have replaced it (e.g. abortAllActions
|
|
619
|
+
// cleared the map and a fresh action recreated the same cohort id), so a
|
|
620
|
+
// stale settlement must never delete the newer one by id.
|
|
621
|
+
function releaseCohort() {
|
|
622
|
+
if (cohortReleased) return;
|
|
623
|
+
cohortReleased = true;
|
|
624
|
+
if (
|
|
625
|
+
--arbitration.inflight <= 0 &&
|
|
626
|
+
cohortArbitration.get(cohortId) === arbitration
|
|
627
|
+
) {
|
|
628
|
+
cohortArbitration.delete(cohortId);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
551
632
|
function doSettle() {
|
|
552
633
|
if (settled) return;
|
|
553
634
|
settled = true;
|
|
@@ -555,6 +636,9 @@ export function createEventController(
|
|
|
555
636
|
// Cleanup after brief delay (allow useAction to read result)
|
|
556
637
|
setTimeout(() => {
|
|
557
638
|
inflightActions.delete(id);
|
|
639
|
+
// Free this cohort's arbitration once its last action has settled, so
|
|
640
|
+
// a long-running action elsewhere cannot pin keys from a drained entry.
|
|
641
|
+
releaseCohort();
|
|
558
642
|
// Check for consolidation
|
|
559
643
|
if (inflightActions.size === 0) {
|
|
560
644
|
// All actions done - reset tracking
|
|
@@ -583,6 +667,19 @@ export function createEventController(
|
|
|
583
667
|
doSettle();
|
|
584
668
|
}
|
|
585
669
|
|
|
670
|
+
// streamingEnded is forced here for the "streaming never started" case so
|
|
671
|
+
// tryFinalize can run; otherwise the streaming token's end() finalizes.
|
|
672
|
+
function settleWith(result: NonNullable<typeof pendingResult>) {
|
|
673
|
+
if (!inflightActions.has(id) || settled) return;
|
|
674
|
+
actionCompleted = true;
|
|
675
|
+
entry.completed = true;
|
|
676
|
+
pendingResult = result;
|
|
677
|
+
if (entry.phase === "fetching" || streamingEnded) {
|
|
678
|
+
streamingEnded = true;
|
|
679
|
+
tryFinalize();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
586
683
|
return {
|
|
587
684
|
id,
|
|
588
685
|
abort,
|
|
@@ -618,56 +715,36 @@ export function createEventController(
|
|
|
618
715
|
segmentIds.forEach((id) => concurrentRevalidatedSegments.add(id));
|
|
619
716
|
},
|
|
620
717
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
718
|
+
claimLocationState(state: Record<string, unknown>) {
|
|
719
|
+
const winning: Record<string, unknown> = {};
|
|
720
|
+
// Arbitrate against this action's OWN captured arbitration object, not a
|
|
721
|
+
// live map lookup: a concurrent map clear/replace (abortAllActions, a
|
|
722
|
+
// stale settlement) must not make this action silently stop recording
|
|
723
|
+
// and accept every key.
|
|
724
|
+
const keySeq = arbitration.keySeq;
|
|
725
|
+
for (const key of Object.keys(state)) {
|
|
726
|
+
const prevSeq = keySeq.get(key);
|
|
727
|
+
// Strictly-greater: a later-initiated action wins a key over an
|
|
728
|
+
// earlier one in the same cohort regardless of arrival order. Equal
|
|
729
|
+
// cannot happen (dispatchSeq is unique per action).
|
|
730
|
+
if (prevSeq === undefined || dispatchSeq > prevSeq) {
|
|
731
|
+
keySeq.set(key, dispatchSeq);
|
|
732
|
+
winning[key] = state[key];
|
|
733
|
+
}
|
|
633
734
|
}
|
|
634
|
-
|
|
735
|
+
return winning;
|
|
635
736
|
},
|
|
636
737
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
actionCompleted = true;
|
|
641
|
-
entry.completed = true;
|
|
642
|
-
pendingResult = { type: "error", value: error };
|
|
643
|
-
|
|
644
|
-
// If streaming never started or already ended, finalize immediately
|
|
645
|
-
// Otherwise wait for streaming to end
|
|
646
|
-
if (entry.phase === "fetching" || streamingEnded) {
|
|
647
|
-
streamingEnded = true; // Mark as ended if never started
|
|
648
|
-
tryFinalize();
|
|
649
|
-
}
|
|
650
|
-
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
738
|
+
complete(result?: unknown) {
|
|
739
|
+
settleWith({ type: "success", value: result });
|
|
651
740
|
},
|
|
652
741
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
// Count actions that are still fetching (waiting for server response)
|
|
657
|
-
const stillFetchingCount = [...inflightActions.values()].filter(
|
|
658
|
-
(a) => a.phase === "fetching"
|
|
659
|
-
).length;
|
|
742
|
+
fail(error: unknown) {
|
|
743
|
+
settleWith({ type: "error", value: error });
|
|
744
|
+
},
|
|
660
745
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
if (!hadAnyConcurrentActions) {
|
|
665
|
-
return null; // No concurrent actions occurred
|
|
666
|
-
}
|
|
667
|
-
if (concurrentRevalidatedSegments.size === 0) {
|
|
668
|
-
return null; // No segments to consolidate
|
|
669
|
-
}
|
|
670
|
-
return Array.from(concurrentRevalidatedSegments);
|
|
746
|
+
getRevalidatedSegments(): Set<string> {
|
|
747
|
+
return concurrentRevalidatedSegments;
|
|
671
748
|
},
|
|
672
749
|
|
|
673
750
|
clearConsolidation() {
|
|
@@ -679,6 +756,10 @@ export function createEventController(
|
|
|
679
756
|
[Symbol.dispose]() {
|
|
680
757
|
// If aborted, another navigation/error took over - don't touch state
|
|
681
758
|
if (abort.signal.aborted) {
|
|
759
|
+
// Aborted actions skip doSettle, so release the cohort hold here to
|
|
760
|
+
// keep the per-cohort refcount balanced (no leak when an action is
|
|
761
|
+
// aborted individually rather than via abortAllActions).
|
|
762
|
+
releaseCohort();
|
|
682
763
|
inflightActions.delete(id);
|
|
683
764
|
notify();
|
|
684
765
|
notifyAction(actionId);
|
|
@@ -702,16 +783,27 @@ export function createEventController(
|
|
|
702
783
|
}
|
|
703
784
|
|
|
704
785
|
function abortAllActions() {
|
|
705
|
-
for (const entry of inflightActions
|
|
786
|
+
for (const [id, entry] of inflightActions) {
|
|
787
|
+
// Preserve settling entries — they have already been handled by
|
|
788
|
+
// fail()/complete() and will self-cleanup via the settlement timeout.
|
|
789
|
+
// Clearing them here would prevent debounced notifications from
|
|
790
|
+
// delivering the error/result state to subscribers.
|
|
791
|
+
if (entry.phase === "settling") continue;
|
|
706
792
|
entry.abort.abort();
|
|
793
|
+
inflightActions.delete(id);
|
|
707
794
|
}
|
|
708
|
-
inflightActions.clear();
|
|
709
795
|
hadAnyConcurrentActions = false;
|
|
710
796
|
concurrentRevalidatedSegments.clear();
|
|
797
|
+
cohortArbitration.clear();
|
|
711
798
|
notify();
|
|
712
|
-
// Notify all action listeners
|
|
713
|
-
|
|
714
|
-
|
|
799
|
+
// Notify all action listeners directly by subscription ID.
|
|
800
|
+
// actionListeners keys are subscription IDs (possibly short names like
|
|
801
|
+
// "addToCart"), not full entry actionIds. Passing them to notifyAction
|
|
802
|
+
// would fail the suffix matcher — instead, notify each subscriber with
|
|
803
|
+
// its own state.
|
|
804
|
+
for (const [subscriptionId, listeners] of actionListeners) {
|
|
805
|
+
const state = getActionState(subscriptionId);
|
|
806
|
+
listeners.forEach((listener) => listener(state));
|
|
715
807
|
}
|
|
716
808
|
}
|
|
717
809
|
|
|
@@ -719,24 +811,17 @@ export function createEventController(
|
|
|
719
811
|
// Handle Operations
|
|
720
812
|
// ========================================================================
|
|
721
813
|
|
|
722
|
-
/**
|
|
723
|
-
* Filter segment IDs to only include routes and layouts.
|
|
724
|
-
* Excludes parallels (contain .@) and loaders (contain D followed by digit).
|
|
725
|
-
*/
|
|
726
|
-
function filterSegmentOrder(matched: string[]): string[] {
|
|
727
|
-
return matched.filter((id) => {
|
|
728
|
-
if (id.includes(".@")) return false;
|
|
729
|
-
if (/D\d+\./.test(id)) return false;
|
|
730
|
-
return true;
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
|
|
734
814
|
function setHandleData(
|
|
735
815
|
data: HandleData,
|
|
736
816
|
matched?: string[],
|
|
737
|
-
isPartial?: boolean
|
|
817
|
+
isPartial?: boolean,
|
|
818
|
+
resolvedIds?: string[],
|
|
738
819
|
): void {
|
|
739
|
-
const
|
|
820
|
+
const rawMatched = matched ?? [];
|
|
821
|
+
const newSegmentOrder = filterSegmentOrder(rawMatched);
|
|
822
|
+
// Separate list for useSegments(): "layouts and routes only" — strip
|
|
823
|
+
// parallels (".@") and loader sub-ids (D digit) without reordering.
|
|
824
|
+
const newRouteSegmentIds = filterRouteSegmentIds(rawMatched);
|
|
740
825
|
|
|
741
826
|
if (isPartial && newSegmentOrder.length > 0) {
|
|
742
827
|
// Partial update: merge new data with existing
|
|
@@ -748,10 +833,19 @@ export function createEventController(
|
|
|
748
833
|
handleData[handleName][segmentId] = data[handleName][segmentId];
|
|
749
834
|
}
|
|
750
835
|
}
|
|
751
|
-
|
|
836
|
+
const resolvedIdSet =
|
|
837
|
+
resolvedIds && resolvedIds.length > 0 ? new Set(resolvedIds) : null;
|
|
838
|
+
// Cleanup pass:
|
|
839
|
+
// a) segment dropped from the match list — delete its bucket.
|
|
840
|
+
// b) segment was re-resolved this request but pushed nothing for
|
|
841
|
+
// this handle — its previous bucket is stale.
|
|
842
|
+
// (a) is the existing behavior; (b) requires resolvedIds.
|
|
752
843
|
for (const handleName of Object.keys(handleData)) {
|
|
753
844
|
for (const segmentId of Object.keys(handleData[handleName])) {
|
|
754
|
-
|
|
845
|
+
const droppedFromMatch = !newSegmentOrder.includes(segmentId);
|
|
846
|
+
const reresolvedWithoutPush =
|
|
847
|
+
resolvedIdSet?.has(segmentId) && !data[handleName]?.[segmentId];
|
|
848
|
+
if (droppedFromMatch || reresolvedWithoutPush) {
|
|
755
849
|
delete handleData[handleName][segmentId];
|
|
756
850
|
}
|
|
757
851
|
}
|
|
@@ -761,6 +855,7 @@ export function createEventController(
|
|
|
761
855
|
handleData = data;
|
|
762
856
|
}
|
|
763
857
|
handleSegmentOrder = newSegmentOrder;
|
|
858
|
+
routeSegmentIds = newRouteSegmentIds;
|
|
764
859
|
|
|
765
860
|
notifyHandles();
|
|
766
861
|
}
|
|
@@ -769,9 +864,22 @@ export function createEventController(
|
|
|
769
864
|
return {
|
|
770
865
|
data: handleData,
|
|
771
866
|
segmentOrder: handleSegmentOrder,
|
|
867
|
+
routeSegmentIds,
|
|
772
868
|
};
|
|
773
869
|
}
|
|
774
870
|
|
|
871
|
+
function setRouteSegmentIds(matched: string[]): void {
|
|
872
|
+
const next = filterRouteSegmentIds(matched);
|
|
873
|
+
if (
|
|
874
|
+
next.length === routeSegmentIds.length &&
|
|
875
|
+
next.every((id, i) => id === routeSegmentIds[i])
|
|
876
|
+
) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
routeSegmentIds = next;
|
|
880
|
+
notifyHandles();
|
|
881
|
+
}
|
|
882
|
+
|
|
775
883
|
// ========================================================================
|
|
776
884
|
// Subscriptions
|
|
777
885
|
// ========================================================================
|
|
@@ -783,7 +891,7 @@ export function createEventController(
|
|
|
783
891
|
|
|
784
892
|
function subscribeToAction(
|
|
785
893
|
actionId: string,
|
|
786
|
-
listener: ActionStateListener
|
|
894
|
+
listener: ActionStateListener,
|
|
787
895
|
): () => void {
|
|
788
896
|
let listeners = actionListeners.get(actionId);
|
|
789
897
|
if (!listeners) {
|
|
@@ -805,6 +913,19 @@ export function createEventController(
|
|
|
805
913
|
return () => handleListeners.delete(listener);
|
|
806
914
|
}
|
|
807
915
|
|
|
916
|
+
// ========================================================================
|
|
917
|
+
// Params Operations
|
|
918
|
+
// ========================================================================
|
|
919
|
+
|
|
920
|
+
function setParams(params: Record<string, string>): void {
|
|
921
|
+
routeParams = params;
|
|
922
|
+
notify();
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function getParams(): Record<string, string> {
|
|
926
|
+
return routeParams;
|
|
927
|
+
}
|
|
928
|
+
|
|
808
929
|
// ========================================================================
|
|
809
930
|
// Return Controller
|
|
810
931
|
// ========================================================================
|
|
@@ -821,11 +942,17 @@ export function createEventController(
|
|
|
821
942
|
// State
|
|
822
943
|
getState,
|
|
823
944
|
getActionState,
|
|
945
|
+
getLocation: () => location,
|
|
824
946
|
setLocation,
|
|
825
947
|
|
|
826
948
|
// Handles
|
|
827
949
|
setHandleData,
|
|
828
950
|
getHandleState,
|
|
951
|
+
setRouteSegmentIds,
|
|
952
|
+
|
|
953
|
+
// Params
|
|
954
|
+
setParams,
|
|
955
|
+
getParams,
|
|
829
956
|
|
|
830
957
|
// Subscriptions
|
|
831
958
|
subscribe,
|
|
@@ -835,42 +962,6 @@ export function createEventController(
|
|
|
835
962
|
// Direct access
|
|
836
963
|
getCurrentNavigation: () => currentNavigation,
|
|
837
964
|
getInflightActions: () => inflightActions,
|
|
965
|
+
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
838
966
|
};
|
|
839
967
|
}
|
|
840
|
-
|
|
841
|
-
// ============================================================================
|
|
842
|
-
// Singleton
|
|
843
|
-
// ============================================================================
|
|
844
|
-
|
|
845
|
-
let controllerInstance: EventController | null = null;
|
|
846
|
-
|
|
847
|
-
/**
|
|
848
|
-
* Initialize the global event controller
|
|
849
|
-
*/
|
|
850
|
-
export function initEventController(
|
|
851
|
-
config?: EventControllerConfig
|
|
852
|
-
): EventController {
|
|
853
|
-
if (!controllerInstance) {
|
|
854
|
-
controllerInstance = createEventController(config);
|
|
855
|
-
}
|
|
856
|
-
return controllerInstance;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
/**
|
|
860
|
-
* Get the global event controller
|
|
861
|
-
*/
|
|
862
|
-
export function getEventController(): EventController {
|
|
863
|
-
if (!controllerInstance) {
|
|
864
|
-
throw new Error(
|
|
865
|
-
"Event controller not initialized. Call initEventController first."
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
return controllerInstance;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
/**
|
|
872
|
-
* Reset the controller instance (for testing)
|
|
873
|
-
*/
|
|
874
|
-
export function resetEventController(): void {
|
|
875
|
-
controllerInstance = null;
|
|
876
|
-
}
|