@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c
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 -0
- package/README.md +126 -16
- package/dist/bin/rango.js +319 -95
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2724 -1053
- package/package.json +68 -14
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +224 -32
- package/skills/caching/SKILL.md +279 -17
- package/skills/composability/SKILL.md +27 -3
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +11 -9
- package/skills/hooks/SKILL.md +243 -29
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +68 -19
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +190 -23
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +18 -10
- package/skills/migrate-nextjs/SKILL.md +43 -19
- package/skills/migrate-react-router/SKILL.md +8 -2
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +18 -7
- package/skills/prerender/SKILL.md +65 -60
- package/skills/rango/SKILL.md +251 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +115 -48
- package/skills/route/SKILL.md +46 -5
- package/skills/router-setup/SKILL.md +30 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/tailwind/SKILL.md +27 -3
- 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 +129 -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 +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +322 -29
- package/skills/use-cache/SKILL.md +57 -14
- 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 +0 -65
- 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/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +192 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +94 -25
- package/src/browser/navigation-client.ts +121 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +115 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +147 -128
- package/src/browser/prefetch/cache.ts +107 -56
- package/src/browser/prefetch/fetch.ts +204 -34
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +30 -7
- package/src/browser/react/NavigationProvider.tsx +283 -118
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- 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 +17 -14
- 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 +10 -5
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- 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 +91 -24
- package/src/browser/scroll-restoration.ts +30 -17
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +214 -55
- package/src/browser/types.ts +80 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +117 -14
- 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 +117 -23
- 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 +55 -28
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +224 -41
- package/src/cache/cache-scope.ts +188 -82
- 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 +1989 -378
- 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 +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +36 -61
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +96 -12
- package/src/index.ts +94 -14
- 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 +32 -37
- package/src/prerender.ts +61 -6
- 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 +34 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +82 -55
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +27 -43
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +10 -47
- package/src/router/loader-resolution.ts +155 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +9 -24
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -52
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +44 -74
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +247 -166
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +85 -94
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +104 -65
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +83 -35
- package/src/router/router-options.ts +136 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +97 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +272 -320
- 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 +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +162 -64
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +110 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +264 -220
- package/src/rsc/helpers.ts +100 -6
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +117 -11
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +88 -188
- package/src/rsc/rsc-rendering.ts +98 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +281 -117
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +23 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +254 -143
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +163 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +287 -178
- package/src/ssr/index.tsx +21 -16
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +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 +581 -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 +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +29 -12
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +126 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +96 -68
- 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 +44 -0
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +2 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +126 -8
- 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-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- 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 +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- 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 +39 -25
- package/src/vite/rango.ts +109 -118
- package/src/vite/router-discovery.ts +718 -119
- 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 +54 -39
- package/src/vite/utils/shared-utils.ts +90 -41
- 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
|
@@ -46,7 +46,6 @@ export function useNavigation<T>(
|
|
|
46
46
|
throw new Error("useNavigation must be used within NavigationProvider");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Base state for useOptimistic
|
|
50
49
|
const [baseValue, setBaseValue] = useState<T | PublicNavigationState>(() => {
|
|
51
50
|
const publicState = toPublicState(ctx.eventController.getState());
|
|
52
51
|
return selector ? selector(publicState) : publicState;
|
|
@@ -59,7 +58,6 @@ export function useNavigation<T>(
|
|
|
59
58
|
// parent transition (e.g. <Link> click) is still pending.
|
|
60
59
|
const optimisticPinnedRef = useRef(false);
|
|
61
60
|
|
|
62
|
-
// useOptimistic allows immediate updates during transitions/actions
|
|
63
61
|
const [value, setOptimisticValue] = useOptimistic(baseValue);
|
|
64
62
|
|
|
65
63
|
// Store selector in a ref so the subscription callback always uses the
|
|
@@ -72,8 +70,7 @@ export function useNavigation<T>(
|
|
|
72
70
|
|
|
73
71
|
// Subscribe to event controller state changes (only runs on client)
|
|
74
72
|
useEffect(() => {
|
|
75
|
-
|
|
76
|
-
return ctx.eventController.subscribe(() => {
|
|
73
|
+
const update = () => {
|
|
77
74
|
const currentState = ctx.eventController.getState();
|
|
78
75
|
const publicState = toPublicState(currentState);
|
|
79
76
|
const nextSelected = selectorRef.current
|
|
@@ -112,7 +109,15 @@ export function useNavigation<T>(
|
|
|
112
109
|
// Always update base state so UI reflects current state
|
|
113
110
|
setBaseValue(nextSelected);
|
|
114
111
|
}
|
|
115
|
-
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Catch-up: re-read state synchronously on mount before subscribing, so a
|
|
115
|
+
// state change between the seeding render and this effect commit isn't
|
|
116
|
+
// dropped until the next (debounced) notify. Mirrors usePathname /
|
|
117
|
+
// useSearchParams.
|
|
118
|
+
update();
|
|
119
|
+
|
|
120
|
+
return ctx.eventController.subscribe(update);
|
|
116
121
|
}, []);
|
|
117
122
|
|
|
118
123
|
return value as T | PublicNavigationState;
|
|
@@ -4,6 +4,8 @@ import { useContext, useState, useEffect, useRef } from "react";
|
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
5
|
import { shallowEqual } from "./shallow-equal.js";
|
|
6
6
|
|
|
7
|
+
const EMPTY_PARAMS: Record<string, string> = Object.freeze({});
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Hook to access the current route params.
|
|
9
11
|
*
|
|
@@ -27,29 +29,27 @@ import { shallowEqual } from "./shallow-equal.js";
|
|
|
27
29
|
// interface shapes pass the constraint — interfaces lack an implicit
|
|
28
30
|
// index signature and would otherwise be rejected. The generic is a
|
|
29
31
|
// shape annotation, not a runtime check; the body always returns the
|
|
30
|
-
// underlying params map unchanged.
|
|
32
|
+
// underlying params map unchanged. The default and selector input use
|
|
33
|
+
// `string | undefined` because absent optional params are omitted from
|
|
34
|
+
// the params record at runtime — the type must reflect that so callers
|
|
35
|
+
// don't write `p.locale.length` and crash when the segment is absent.
|
|
31
36
|
export function useParams<
|
|
32
|
-
T extends object = Record<string, string>,
|
|
37
|
+
T extends object = Record<string, string | undefined>,
|
|
33
38
|
>(): Readonly<T>;
|
|
34
39
|
export function useParams<T>(
|
|
35
|
-
selector: (params: Record<string, string>) => T,
|
|
40
|
+
selector: (params: Record<string, string | undefined>) => T,
|
|
36
41
|
): T;
|
|
37
42
|
export function useParams<T>(
|
|
38
|
-
selector?: (params: Record<string, string>) => T,
|
|
39
|
-
): T | Record<string, string> {
|
|
43
|
+
selector?: (params: Record<string, string | undefined>) => T,
|
|
44
|
+
): T | Record<string, string | undefined> {
|
|
40
45
|
const ctx = useContext(NavigationStoreContext);
|
|
41
46
|
|
|
42
47
|
const [value, setValue] = useState<T | Record<string, string>>(() => {
|
|
43
|
-
|
|
44
|
-
return selector ? selector({}) : {};
|
|
45
|
-
}
|
|
46
|
-
const params = ctx.eventController.getParams();
|
|
48
|
+
const params = ctx ? ctx.eventController.getParams() : EMPTY_PARAMS;
|
|
47
49
|
return selector ? selector(params) : params;
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
const prevValue = useRef(value);
|
|
51
|
-
// Ref keeps the latest selector without re-subscribing. Event-driven by
|
|
52
|
-
// design: value updates on store events, not on selector identity change.
|
|
53
53
|
const selectorRef = useRef(selector);
|
|
54
54
|
selectorRef.current = selector;
|
|
55
55
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import type { LocalReverseFunction } from "../../reverse.js";
|
|
5
|
+
import { substitutePatternParams } from "../../router/substitute-pattern-params.js";
|
|
6
|
+
import { serializeSearchParams } from "../../search-params.js";
|
|
7
|
+
import { useMount } from "./use-mount.js";
|
|
8
|
+
import { useParams } from "./use-params.js";
|
|
9
|
+
|
|
10
|
+
type RouteEntry = string | { readonly path: string };
|
|
11
|
+
type LocalRouteMap = Readonly<Record<string, RouteEntry>>;
|
|
12
|
+
|
|
13
|
+
function getPattern(entry: RouteEntry | undefined): string | undefined {
|
|
14
|
+
if (entry === undefined) return undefined;
|
|
15
|
+
return typeof entry === "string" ? entry : entry.path;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Join an include mount prefix with a mount-relative pattern.
|
|
20
|
+
*
|
|
21
|
+
* `pattern === "/"` is the index of the local module — under a non-root
|
|
22
|
+
* mount it must collapse so `/` under `/blog` becomes `/blog`, not
|
|
23
|
+
* `/blog/`. This matches `ctx.reverse(".index")` on the server.
|
|
24
|
+
*/
|
|
25
|
+
function joinMount(mount: string, pattern: string): string {
|
|
26
|
+
if (pattern === "/") {
|
|
27
|
+
if (mount === "" || mount === "/") return "/";
|
|
28
|
+
return mount.endsWith("/") ? mount.slice(0, -1) : mount;
|
|
29
|
+
}
|
|
30
|
+
const normalizedMount = mount === "/" ? "" : mount.replace(/\/+$/, "");
|
|
31
|
+
return normalizedMount + pattern;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mount-aware reverse function for a locally-imported `routes` map.
|
|
36
|
+
*
|
|
37
|
+
* The `routes` map you pass IS the scope: `reverse("name")` looks the name up
|
|
38
|
+
* in that map (verbatim), prefixes the result with the surrounding `include()`
|
|
39
|
+
* mount path via `useMount()`, and substitutes params — auto-filling from the
|
|
40
|
+
* current matched route's params, with explicit params overriding. A module's
|
|
41
|
+
* components can therefore reverse their own routes without knowing where the
|
|
42
|
+
* module is mounted: include it under any prefix and the URLs resolve correctly.
|
|
43
|
+
*
|
|
44
|
+
* The leading dot is optional and cosmetic: `reverse("post")` and
|
|
45
|
+
* `reverse(".post")` resolve identically. The dot exists only as a readability
|
|
46
|
+
* convention and for parity with `ctx.reverse(".name")` on the server; here the
|
|
47
|
+
* passed map is the scope, so there is no separate global namespace to
|
|
48
|
+
* disambiguate and the dot carries no meaning.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* "use client";
|
|
53
|
+
* import { Link, useReverse } from "@rangojs/router/client";
|
|
54
|
+
* import { routes as blogRoutes } from "../urls/blog.gen.js";
|
|
55
|
+
*
|
|
56
|
+
* function BlogNav() {
|
|
57
|
+
* const reverse = useReverse(blogRoutes);
|
|
58
|
+
* return (
|
|
59
|
+
* <>
|
|
60
|
+
* <Link to={reverse("index")}>Blog</Link>
|
|
61
|
+
* <Link to={reverse("post", { postId: "hello" })}>Post</Link>
|
|
62
|
+
* </>
|
|
63
|
+
* );
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function useReverse<const TRoutes extends LocalRouteMap>(
|
|
68
|
+
routes: TRoutes,
|
|
69
|
+
): LocalReverseFunction<TRoutes> {
|
|
70
|
+
const mount = useMount();
|
|
71
|
+
const currentParams = useParams();
|
|
72
|
+
|
|
73
|
+
return useCallback(
|
|
74
|
+
((
|
|
75
|
+
name: string,
|
|
76
|
+
explicitParams?: Record<string, string | undefined>,
|
|
77
|
+
search?: Record<string, unknown>,
|
|
78
|
+
): string => {
|
|
79
|
+
// The leading dot is optional. The passed map IS the scope, so a dot to
|
|
80
|
+
// signal "local" is unnecessary — "detail" and ".detail" resolve the same.
|
|
81
|
+
// A dot is accepted (and stripped) for readability / ctx.reverse parity.
|
|
82
|
+
const lookupName = name.startsWith(".") ? name.slice(1) : name;
|
|
83
|
+
const entry = (routes as LocalRouteMap)[lookupName];
|
|
84
|
+
const pattern = getPattern(entry);
|
|
85
|
+
if (pattern === undefined) {
|
|
86
|
+
throw new Error(`Unknown route: "${name}"`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const joined = joinMount(mount, pattern);
|
|
90
|
+
|
|
91
|
+
const mergedParams = explicitParams
|
|
92
|
+
? { ...currentParams, ...explicitParams }
|
|
93
|
+
: currentParams;
|
|
94
|
+
|
|
95
|
+
const substituted = substitutePatternParams(joined, mergedParams, name);
|
|
96
|
+
|
|
97
|
+
if (search) {
|
|
98
|
+
const qs = serializeSearchParams(search);
|
|
99
|
+
if (qs) return `${substituted}?${qs}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return substituted;
|
|
103
|
+
}) as LocalReverseFunction<TRoutes>,
|
|
104
|
+
[routes, mount, currentParams],
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -13,6 +13,11 @@ import type { RouterInstance, RouterNavigateOptions } from "../types.js";
|
|
|
13
13
|
* useRouter() do not re-render on navigation state changes.
|
|
14
14
|
* For reactive navigation state, use useNavigation() instead.
|
|
15
15
|
*
|
|
16
|
+
* Methods read `basename` from the context on each call. It is set once from
|
|
17
|
+
* the initial payload and is stable within a session — a cross-app navigation
|
|
18
|
+
* is a full document load (X-RSC-Reload), so the target app mounts fresh with
|
|
19
|
+
* its own basename.
|
|
20
|
+
*
|
|
16
21
|
* @example
|
|
17
22
|
* ```tsx
|
|
18
23
|
* const router = useRouter();
|
|
@@ -29,7 +34,10 @@ export function useRouter(): RouterInstance {
|
|
|
29
34
|
throw new Error("useRouter must be used within NavigationProvider");
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
// Stable reference: ctx is
|
|
37
|
+
// Stable reference: ctx itself is stable, and reads on each method call
|
|
38
|
+
// pick up live basename values from the context (backed by a live ref
|
|
39
|
+
// in NavigationProvider), so app-switch transitions are reflected without
|
|
40
|
+
// recreating this object.
|
|
33
41
|
return useMemo<RouterInstance>(() => {
|
|
34
42
|
/** Prefix a root-relative path with basename if not already prefixed. */
|
|
35
43
|
function withBasename(url: string): string {
|
|
@@ -52,7 +60,7 @@ export function useRouter(): RouterInstance {
|
|
|
52
60
|
return ctx.refresh();
|
|
53
61
|
},
|
|
54
62
|
|
|
55
|
-
prefetch(url: string): void {
|
|
63
|
+
prefetch(url: string, options?: { key?: ":source" }): void {
|
|
56
64
|
const segmentState = ctx.store?.getSegmentState();
|
|
57
65
|
if (segmentState) {
|
|
58
66
|
prefetchDirect(
|
|
@@ -60,12 +68,26 @@ export function useRouter(): RouterInstance {
|
|
|
60
68
|
segmentState.currentSegmentIds,
|
|
61
69
|
getAppVersion(),
|
|
62
70
|
ctx.store?.getRouterId?.(),
|
|
71
|
+
options?.key,
|
|
63
72
|
);
|
|
64
73
|
}
|
|
65
74
|
},
|
|
66
75
|
|
|
67
76
|
back(): void {
|
|
68
|
-
|
|
77
|
+
// Avoid escaping the host on the first entry of this session.
|
|
78
|
+
// Prefer the Navigation API; fall back to the router-stamped
|
|
79
|
+
// history.state.idx (set by pushHistoryWithIdx) for older browsers.
|
|
80
|
+
const nav = (window as { navigation?: { canGoBack: boolean } })
|
|
81
|
+
.navigation;
|
|
82
|
+
const canGoBack =
|
|
83
|
+
nav && typeof nav.canGoBack === "boolean"
|
|
84
|
+
? nav.canGoBack
|
|
85
|
+
: ((window.history.state as { idx?: number } | null)?.idx ?? 0) > 0;
|
|
86
|
+
if (canGoBack) {
|
|
87
|
+
window.history.back();
|
|
88
|
+
} else {
|
|
89
|
+
ctx.navigate(withBasename("/"), { replace: true });
|
|
90
|
+
}
|
|
69
91
|
},
|
|
70
92
|
|
|
71
93
|
forward(): void {
|
|
@@ -24,9 +24,6 @@ import type { ReadonlyURLSearchParams } from "../types.js";
|
|
|
24
24
|
export function useSearchParams(): ReadonlyURLSearchParams {
|
|
25
25
|
const ctx = useContext(NavigationStoreContext);
|
|
26
26
|
|
|
27
|
-
// Always initialize with empty URLSearchParams to match SSR output
|
|
28
|
-
// and avoid hydration mismatch. The useEffect below syncs from
|
|
29
|
-
// the real URL after hydration.
|
|
30
27
|
const [searchParams, setSearchParams] = useState<ReadonlyURLSearchParams>(
|
|
31
28
|
() => new URLSearchParams(),
|
|
32
29
|
);
|
|
@@ -41,12 +38,10 @@ export function useSearchParams(): ReadonlyURLSearchParams {
|
|
|
41
38
|
const nextSearch = location.searchParams.toString();
|
|
42
39
|
if (nextSearch !== prevSearch.current) {
|
|
43
40
|
prevSearch.current = nextSearch;
|
|
44
|
-
// Create a snapshot so callers cannot mutate the source URLSearchParams
|
|
45
41
|
setSearchParams(new URLSearchParams(nextSearch));
|
|
46
42
|
}
|
|
47
43
|
};
|
|
48
44
|
|
|
49
|
-
// Sync on mount (picks up search params from browser URL)
|
|
50
45
|
update();
|
|
51
46
|
|
|
52
47
|
return ctx.eventController.subscribe(update);
|
|
@@ -25,15 +25,18 @@ function parsePathname(pathname: string): string[] {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Build segments state from event controller
|
|
28
|
+
* Build segments state from event controller. `segmentIds` is the
|
|
29
|
+
* route-only list (parallels and loaders stripped) — distinct from the
|
|
30
|
+
* controller's `segmentOrder` which drives handle collection and includes
|
|
31
|
+
* parallel slot ids.
|
|
29
32
|
*/
|
|
30
33
|
function buildSegmentsState(
|
|
31
34
|
location: URL,
|
|
32
|
-
|
|
35
|
+
routeSegmentIds: string[],
|
|
33
36
|
): SegmentsState {
|
|
34
37
|
return {
|
|
35
38
|
path: parsePathname(location.pathname),
|
|
36
|
-
segmentIds:
|
|
39
|
+
segmentIds: routeSegmentIds,
|
|
37
40
|
location,
|
|
38
41
|
};
|
|
39
42
|
}
|
|
@@ -74,7 +77,7 @@ export function useSegments<T>(
|
|
|
74
77
|
const handleState = ctx.eventController.getHandleState();
|
|
75
78
|
const segmentsState = buildSegmentsState(
|
|
76
79
|
location as URL,
|
|
77
|
-
handleState.
|
|
80
|
+
handleState.routeSegmentIds,
|
|
78
81
|
);
|
|
79
82
|
return selector ? selector(segmentsState) : segmentsState;
|
|
80
83
|
});
|
|
@@ -83,47 +86,36 @@ export function useSegments<T>(
|
|
|
83
86
|
const selectorRef = useRef(selector);
|
|
84
87
|
selectorRef.current = selector;
|
|
85
88
|
|
|
86
|
-
// Track selector identity to detect when the selector function changes.
|
|
87
|
-
// Only then do we eagerly recompute during render to avoid staleness.
|
|
88
|
-
// Without this guard, no-selector mode causes infinite re-renders because
|
|
89
|
-
// buildSegmentsState creates fresh arrays that fail Object.is checks.
|
|
90
89
|
const prevSelectorIdentity = useRef(selector);
|
|
91
90
|
|
|
92
|
-
// Cache SegmentsState to stabilize nested references (path, segmentIds
|
|
93
|
-
// arrays) so selectors returning composite values don't cause spurious
|
|
94
|
-
// render-time setState calls.
|
|
95
91
|
const segmentsCache = useRef<{
|
|
96
92
|
location: URL;
|
|
97
|
-
|
|
93
|
+
routeSegmentIds: string[];
|
|
98
94
|
state: SegmentsState;
|
|
99
95
|
} | null>(null);
|
|
100
96
|
|
|
101
|
-
// Recompute selected value from current store state and apply selector.
|
|
102
|
-
// Shared by the render-time eager check and the subscription callback.
|
|
103
97
|
function recompute(
|
|
104
98
|
sel: ((state: SegmentsState) => T) | undefined,
|
|
105
99
|
): T | SegmentsState {
|
|
106
100
|
const location = ctx!.eventController.getLocation();
|
|
107
101
|
const handleState = ctx!.eventController.getHandleState();
|
|
108
102
|
|
|
109
|
-
// Reuse cached state when inputs haven't changed by reference,
|
|
110
|
-
// keeping array/object references stable for composite selectors.
|
|
111
103
|
const cache = segmentsCache.current;
|
|
112
104
|
let segmentsState: SegmentsState;
|
|
113
105
|
if (
|
|
114
106
|
cache &&
|
|
115
107
|
cache.location === location &&
|
|
116
|
-
cache.
|
|
108
|
+
cache.routeSegmentIds === handleState.routeSegmentIds
|
|
117
109
|
) {
|
|
118
110
|
segmentsState = cache.state;
|
|
119
111
|
} else {
|
|
120
112
|
segmentsState = buildSegmentsState(
|
|
121
113
|
location as URL,
|
|
122
|
-
handleState.
|
|
114
|
+
handleState.routeSegmentIds,
|
|
123
115
|
);
|
|
124
116
|
segmentsCache.current = {
|
|
125
117
|
location: location as URL,
|
|
126
|
-
|
|
118
|
+
routeSegmentIds: handleState.routeSegmentIds,
|
|
127
119
|
state: segmentsState,
|
|
128
120
|
};
|
|
129
121
|
}
|
|
@@ -162,8 +154,6 @@ export function useSegments<T>(
|
|
|
162
154
|
unsubscribeNav();
|
|
163
155
|
unsubscribeHandles();
|
|
164
156
|
};
|
|
165
|
-
// Stable subscription: selector changes are handled via selectorRef,
|
|
166
|
-
// state comparison uses prevState ref. No re-subscribe needed.
|
|
167
157
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
168
158
|
}, []);
|
|
169
159
|
|
|
@@ -2,6 +2,16 @@ import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
|
2
2
|
|
|
3
3
|
type HeaderResult = { url: string } | "blocked" | null;
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Null-body statuses: the Fetch spec forbids pairing these with a body, so
|
|
7
|
+
* `new Response(body, { status })` throws ("Response with null body status
|
|
8
|
+
* cannot have body"). fetch() can still surface one WITH a body straight from
|
|
9
|
+
* the network layer (never the JS constructor): a 304 stale-while-revalidate
|
|
10
|
+
* prefetch revalidated to Not Modified (body from cache), or a 204 soft
|
|
11
|
+
* redirect. teeWithCompletion must not re-run those through `new Response`.
|
|
12
|
+
*/
|
|
13
|
+
const NULL_BODY_STATUS = new Set([101, 204, 205, 304]);
|
|
14
|
+
|
|
5
15
|
/**
|
|
6
16
|
* Extract and validate an RSC response header URL (X-RSC-Reload, X-RSC-Redirect).
|
|
7
17
|
* Returns { url } if valid, "blocked" if present but invalid origin, null if absent.
|
|
@@ -24,21 +34,92 @@ export function emptyResponse(): Response {
|
|
|
24
34
|
return new Response(null, { status: 200 });
|
|
25
35
|
}
|
|
26
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Whether an RSC content response carries a server-stamped router identity
|
|
39
|
+
* (`X-RSC-Router-Id`) that DIFFERS from the id this client expects (its own
|
|
40
|
+
* routerId, also sent as `_rsc_rid`). Pre-decode integrity check: lets a caller
|
|
41
|
+
* refuse a foreign app's payload before `createFromFetch` imports its chunks.
|
|
42
|
+
*
|
|
43
|
+
* True ONLY when both the header and the expected id are present and differ. An
|
|
44
|
+
* absent header (control-only reload/redirect responses are not stamped) or an
|
|
45
|
+
* absent expected id (e.g. before the client is seeded) is a pass-through —
|
|
46
|
+
* never a false reject.
|
|
47
|
+
*/
|
|
48
|
+
export function isForeignRouterId(
|
|
49
|
+
response: Response,
|
|
50
|
+
expectedId: string | undefined,
|
|
51
|
+
): boolean {
|
|
52
|
+
const got = response.headers.get("X-RSC-Router-Id");
|
|
53
|
+
if (!got || !expectedId) return false;
|
|
54
|
+
return got !== expectedId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handle the X-RSC-Reload control header (server requests a full page reload on
|
|
59
|
+
* a version mismatch). Returns a short-circuit response when the header is
|
|
60
|
+
* present -- emptyResponse() if the URL was blocked by origin validation, or a
|
|
61
|
+
* never-resolving promise while the page reloads -- and null when absent, so
|
|
62
|
+
* the caller continues processing (e.g. the X-RSC-Redirect check). Scoped to
|
|
63
|
+
* X-RSC-Reload only; redirect handling differs between callers.
|
|
64
|
+
*/
|
|
65
|
+
export function handleReloadHeader(
|
|
66
|
+
response: Response,
|
|
67
|
+
opts: { onBlocked: () => void; onReload: (url: string) => void },
|
|
68
|
+
): Response | Promise<Response> | null {
|
|
69
|
+
const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
|
|
70
|
+
if (reload === "blocked") {
|
|
71
|
+
opts.onBlocked();
|
|
72
|
+
return emptyResponse();
|
|
73
|
+
}
|
|
74
|
+
if (reload) {
|
|
75
|
+
opts.onReload(reload.url);
|
|
76
|
+
window.location.href = reload.url;
|
|
77
|
+
return new Promise<Response>(() => {});
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
27
82
|
/**
|
|
28
83
|
* Tee a response body for RSC parsing and stream completion tracking.
|
|
29
84
|
* Returns a new Response with one branch; the other is consumed to detect
|
|
30
85
|
* end-of-stream, calling onComplete when done.
|
|
31
86
|
*
|
|
32
|
-
*
|
|
87
|
+
* `onComplete` receives `endedCleanly`: true only on a normal EOF drain, false
|
|
88
|
+
* on a read error or an abort (the signal cancelled the reader). Callers that
|
|
89
|
+
* gate a "fully complete" fast path (e.g. prefetch's `entry.complete`) must
|
|
90
|
+
* treat a non-clean end as incomplete — a broken stream is not complete data.
|
|
91
|
+
*
|
|
92
|
+
* If the response has no body, onComplete fires synchronously with `true`
|
|
93
|
+
* (an empty body is a clean, complete stream).
|
|
33
94
|
* If signal is provided, an abort cancels the tracking reader.
|
|
95
|
+
*
|
|
96
|
+
* `silent` suppresses the stream-error log. Prefetch passes it: a speculative,
|
|
97
|
+
* low-priority prefetch that is aborted or never consumed can error its stream
|
|
98
|
+
* benignly, which is not worth surfacing. The fresh-navigation path keeps the
|
|
99
|
+
* log (default), where a stream error reflects a real failed navigation.
|
|
34
100
|
*/
|
|
35
101
|
export function teeWithCompletion(
|
|
36
102
|
response: Response,
|
|
37
|
-
onComplete: () => void,
|
|
103
|
+
onComplete: (endedCleanly: boolean) => void,
|
|
38
104
|
signal?: AbortSignal,
|
|
105
|
+
silent = false,
|
|
39
106
|
): Response {
|
|
40
|
-
|
|
41
|
-
|
|
107
|
+
// Once-guard: a mid-stream read error runs both the finally block and the
|
|
108
|
+
// rejection's .catch, so onComplete must be settled exactly once across all
|
|
109
|
+
// paths (no-body early return, finally, catch).
|
|
110
|
+
let settled = false;
|
|
111
|
+
const settle = (endedCleanly: boolean) => {
|
|
112
|
+
if (!settled) {
|
|
113
|
+
settled = true;
|
|
114
|
+
onComplete(endedCleanly);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Empty body, or a null-body status fetch() paired with a body: either way the
|
|
119
|
+
// body can't be re-attached via `new Response` below. Settle and pass the
|
|
120
|
+
// original response through; its body (when present) stays readable downstream.
|
|
121
|
+
if (!response.body || NULL_BODY_STATUS.has(response.status)) {
|
|
122
|
+
settle(true);
|
|
42
123
|
return response;
|
|
43
124
|
}
|
|
44
125
|
|
|
@@ -48,21 +129,31 @@ export function teeWithCompletion(
|
|
|
48
129
|
const reader = trackingStream.getReader();
|
|
49
130
|
const onAbort = signal ? reader.cancel.bind(reader) : undefined;
|
|
50
131
|
if (onAbort) signal!.addEventListener("abort", onAbort, { once: true });
|
|
132
|
+
// Only a loop that reaches `done` is a clean EOF. A read error rejects out
|
|
133
|
+
// of the try and still runs the finally, so the finally must NOT assume
|
|
134
|
+
// clean — it gates on this flag (false on error) AND on signal.aborted
|
|
135
|
+
// (an abort cancels the reader so read() resolves { done: true } and the
|
|
136
|
+
// loop breaks here normally, NOT in the .catch — re-check the signal to
|
|
137
|
+
// catch that case).
|
|
138
|
+
let cleanEof = false;
|
|
51
139
|
try {
|
|
52
140
|
while (true) {
|
|
53
141
|
const { done } = await reader.read();
|
|
54
|
-
if (done)
|
|
142
|
+
if (done) {
|
|
143
|
+
cleanEof = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
55
146
|
}
|
|
56
147
|
} finally {
|
|
57
148
|
if (onAbort) signal!.removeEventListener("abort", onAbort);
|
|
58
149
|
reader.releaseLock();
|
|
59
|
-
|
|
150
|
+
settle(cleanEof && !signal?.aborted);
|
|
60
151
|
}
|
|
61
152
|
})().catch((error) => {
|
|
62
|
-
if (!signal?.aborted) {
|
|
153
|
+
if (!silent && !signal?.aborted) {
|
|
63
154
|
console.error("[Browser] Error reading tracking stream:", error);
|
|
64
155
|
}
|
|
65
|
-
|
|
156
|
+
settle(false);
|
|
66
157
|
});
|
|
67
158
|
|
|
68
159
|
return new Response(rscStream, {
|