@rangojs/router 0.0.0-experimental.9c9afef3 → 0.0.0-experimental.a014d2b7
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 +245 -49
- package/dist/bin/rango.js +440 -133
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3373 -1176
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- 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 +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +87 -18
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +771 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +77 -7
- package/skills/prerender/SKILL.md +172 -125
- package/skills/rango/SKILL.md +251 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +70 -5
- package/skills/router-setup/SKILL.md +65 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +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 +1 -66
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +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 +131 -30
- package/src/browser/navigation-client.ts +186 -100
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +157 -74
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +165 -112
- package/src/browser/prefetch/cache.ts +205 -62
- package/src/browser/prefetch/fetch.ts +347 -39
- package/src/browser/prefetch/queue.ts +42 -8
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +102 -15
- package/src/browser/react/NavigationProvider.tsx +295 -119
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- 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 +23 -69
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +114 -24
- package/src/browser/scroll-restoration.ts +37 -22
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -72
- package/src/browser/types.ts +102 -12
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +165 -36
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +125 -24
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +260 -94
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +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 +119 -290
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +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 +70 -22
- 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 +99 -13
- package/src/index.ts +139 -19
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +461 -304
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +143 -69
- package/src/route-definition/index.ts +4 -2
- package/src/route-definition/redirect.ts +51 -10
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +37 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +52 -31
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +48 -24
- package/src/router/lazy-includes.ts +15 -52
- package/src/router/loader-resolution.ts +268 -56
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +40 -42
- package/src/router/match-api.ts +124 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +170 -276
- 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 +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +87 -39
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -140
- package/src/router/middleware.ts +266 -169
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +132 -90
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +195 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +115 -35
- package/src/router/router-options.ts +172 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +162 -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 +351 -321
- 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 +163 -59
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +157 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +655 -529
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +122 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +131 -14
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +109 -74
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -115
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +29 -9
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +236 -202
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +224 -52
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +38 -46
- package/src/server/request-context.ts +401 -173
- package/src/ssr/index.tsx +24 -16
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +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 +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +125 -71
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +40 -11
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +136 -15
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +68 -18
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +214 -132
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +57 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +6 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +178 -5
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -317
- package/src/vite/plugins/performance-tracks.ts +64 -170
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +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 +118 -114
- package/src/vite/router-discovery.ts +941 -142
- 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 +81 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/debug-channel.ts +0 -93
- 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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { MatchResult } from "../types.js";
|
|
2
|
+
import type { TransitionWhenContext } from "../types/segments.js";
|
|
3
|
+
import type { getRequestContext } from "../server/request-context.js";
|
|
4
|
+
import { invokeOnError } from "../router/error-handling.js";
|
|
5
|
+
import type { OnErrorCallback } from "../types/error-types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Apply transition({ when }) gates to a payload's segments.
|
|
9
|
+
*
|
|
10
|
+
* The predicates were collected during resolution (keyed by segment id) and
|
|
11
|
+
* stripped from the serialized config; here — after handlers ran and outside any
|
|
12
|
+
* cache scope — we evaluate each and drop the segment's transition when the
|
|
13
|
+
* predicate does not hold, so the navigation streams its loading fallback
|
|
14
|
+
* instead of holding the previous content. A predicate that throws is reported
|
|
15
|
+
* to the router's onError (phase "rendering") and then treated as "do not hold"
|
|
16
|
+
* (conservative), so a buggy predicate degrades to no transition rather than
|
|
17
|
+
* failing the response.
|
|
18
|
+
*
|
|
19
|
+
* Mutating the segments here is safe: the segment cache stores a serialized copy
|
|
20
|
+
* (segment-codec), written during match() BEFORE this gate runs, so dropping a
|
|
21
|
+
* transition never corrupts a cache entry. The flip side is that a cache hit
|
|
22
|
+
* skips resolution, collects no predicate, and replays the cached transition
|
|
23
|
+
* as-is (it was serialized before the gate) — combining transition({ when })
|
|
24
|
+
* with cache() on the same segment freezes the gate to its cached state, so
|
|
25
|
+
* avoid caching a route whose transition decision is request-dependent.
|
|
26
|
+
*
|
|
27
|
+
* Returns the same array (mutated) for inline use at the payload's `segments`
|
|
28
|
+
* field.
|
|
29
|
+
*/
|
|
30
|
+
export function gateTransitions(
|
|
31
|
+
segments: MatchResult["segments"],
|
|
32
|
+
ctx: ReturnType<typeof getRequestContext>,
|
|
33
|
+
onError?: OnErrorCallback,
|
|
34
|
+
): MatchResult["segments"] {
|
|
35
|
+
const predicates = ctx._transitionWhen;
|
|
36
|
+
if (predicates && predicates.length) {
|
|
37
|
+
for (const { id, when } of predicates) {
|
|
38
|
+
let drop: boolean;
|
|
39
|
+
try {
|
|
40
|
+
// Assemble the ShouldRevalidateFn-shaped predicate context from the
|
|
41
|
+
// request context. Source fields (currentUrl/currentParams/fromRouteName)
|
|
42
|
+
// were stashed at match time from the navigation snapshot; action fields
|
|
43
|
+
// at the action-bearing gate call sites. nextUrl/nextParams/toRouteName/
|
|
44
|
+
// method/get/env come straight off ctx (setRequestContextParams ran
|
|
45
|
+
// before the gate). Source/action fields are undefined when absent —
|
|
46
|
+
// never fabricated (see TransitionWhenContext).
|
|
47
|
+
const whenCtx: TransitionWhenContext = {
|
|
48
|
+
currentUrl: ctx._gateCurrentUrl,
|
|
49
|
+
currentParams: ctx._gateCurrentParams,
|
|
50
|
+
fromRouteName:
|
|
51
|
+
ctx._prevRouteKey as TransitionWhenContext["fromRouteName"],
|
|
52
|
+
nextUrl: ctx.url,
|
|
53
|
+
nextParams: ctx.params,
|
|
54
|
+
toRouteName: ctx.routeName,
|
|
55
|
+
actionId: ctx._gateActionId,
|
|
56
|
+
actionUrl: ctx._gateActionUrl,
|
|
57
|
+
actionResult: ctx._gateActionResult,
|
|
58
|
+
formData: ctx._gateFormData,
|
|
59
|
+
method: ctx.request.method,
|
|
60
|
+
get: ctx.get,
|
|
61
|
+
env: ctx.env,
|
|
62
|
+
};
|
|
63
|
+
drop = when(whenCtx) === false;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// A throwing predicate must not fail the response: report it and treat
|
|
66
|
+
// the transition as gated off (do not hold). invokeOnError no-ops when
|
|
67
|
+
// onError is undefined.
|
|
68
|
+
drop = true;
|
|
69
|
+
invokeOnError(
|
|
70
|
+
onError,
|
|
71
|
+
error,
|
|
72
|
+
"rendering",
|
|
73
|
+
{
|
|
74
|
+
request: ctx.request,
|
|
75
|
+
url: ctx.url,
|
|
76
|
+
params: ctx.params,
|
|
77
|
+
segmentId: id,
|
|
78
|
+
},
|
|
79
|
+
"RSC",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (drop) {
|
|
83
|
+
const seg = segments.find((s) => s.id === id);
|
|
84
|
+
if (seg) seg.transition = undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return segments;
|
|
89
|
+
}
|
package/src/rsc/types.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ResolvedSegment, SlotState } from "../types.js";
|
|
9
9
|
import type { HandleData } from "../server/handle-store.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type { RangoInternal } from "../router/router-interfaces.js";
|
|
11
11
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -19,10 +19,19 @@ export interface RscPayload {
|
|
|
19
19
|
metadata?: {
|
|
20
20
|
pathname: string;
|
|
21
21
|
segments: ResolvedSegment[];
|
|
22
|
+
/** Router instance ID. When this changes between navigations, the client
|
|
23
|
+
* discards cached segments and does a full tree replacement (app switch). */
|
|
24
|
+
routerId?: string;
|
|
22
25
|
isPartial?: boolean;
|
|
23
26
|
isError?: boolean;
|
|
24
27
|
matched?: string[];
|
|
25
28
|
diff?: string[];
|
|
29
|
+
/**
|
|
30
|
+
* All segment ids re-resolved on the server, including null-component
|
|
31
|
+
* ones excluded from `segments`/`diff`. Drives client-side handle-bucket
|
|
32
|
+
* cleanup. Superset of `diff`. See MatchResult.resolvedIds.
|
|
33
|
+
*/
|
|
34
|
+
resolvedIds?: string[];
|
|
26
35
|
/** Merged route params from the matched route */
|
|
27
36
|
params?: Record<string, string>;
|
|
28
37
|
slots?: Record<string, SlotState>;
|
|
@@ -34,19 +43,33 @@ export interface RscPayload {
|
|
|
34
43
|
version?: string;
|
|
35
44
|
/** TTL in milliseconds for the client-side in-memory prefetch cache */
|
|
36
45
|
prefetchCacheTTL?: number;
|
|
46
|
+
/** Server-resolved rango state cookie name; the client reads it verbatim. */
|
|
47
|
+
stateCookieName?: string;
|
|
37
48
|
/** Theme configuration for FOUC prevention */
|
|
38
49
|
themeConfig?: ResolvedThemeConfig | null;
|
|
39
50
|
/** Initial theme from cookie (for SSR hydration) */
|
|
40
51
|
initialTheme?: Theme;
|
|
52
|
+
/** URL prefix for all routes (from createRouter({ basename })). */
|
|
53
|
+
basename?: string;
|
|
41
54
|
/** Whether connection warmup is enabled */
|
|
42
55
|
warmupEnabled?: boolean;
|
|
43
|
-
/**
|
|
44
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Whether the client should hydrate inside React.StrictMode. Carried on
|
|
58
|
+
* the initial full-render payload only; the browser entry reads it once at
|
|
59
|
+
* hydration. Absent on partial (navigation) payloads. Defaults to true on
|
|
60
|
+
* the client when omitted.
|
|
61
|
+
*/
|
|
62
|
+
strictMode?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Server-side redirect with optional state (for partial requests).
|
|
65
|
+
* `external: true` (from redirect(url, { external: true })) tells the client
|
|
66
|
+
* to hard-navigate to an off-host target instead of validating same-origin.
|
|
67
|
+
*/
|
|
68
|
+
redirect?: { url: string; external?: boolean };
|
|
45
69
|
/** Server-set location state to include in history.pushState */
|
|
46
70
|
locationState?: Record<string, unknown>;
|
|
47
71
|
};
|
|
48
72
|
returnValue?: { ok: boolean; data: unknown };
|
|
49
|
-
formState?: unknown;
|
|
50
73
|
}
|
|
51
74
|
|
|
52
75
|
/**
|
|
@@ -65,10 +88,7 @@ export interface RSCDependencies {
|
|
|
65
88
|
payload: T,
|
|
66
89
|
options?: {
|
|
67
90
|
temporaryReferences?: unknown;
|
|
68
|
-
|
|
69
|
-
readable?: ReadableStream;
|
|
70
|
-
writable?: WritableStream;
|
|
71
|
-
};
|
|
91
|
+
onError?: (error: unknown) => string | void;
|
|
72
92
|
},
|
|
73
93
|
) => ReadableStream<Uint8Array>;
|
|
74
94
|
|
|
@@ -177,7 +197,7 @@ export interface CreateRSCHandlerOptions<
|
|
|
177
197
|
/**
|
|
178
198
|
* The RSC router instance
|
|
179
199
|
*/
|
|
180
|
-
router:
|
|
200
|
+
router: RangoInternal<TEnv, TRoutes>;
|
|
181
201
|
|
|
182
202
|
/**
|
|
183
203
|
* RSC dependencies from @vitejs/plugin-rsc/rsc.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Runtime-safe detection of a test runner (Vitest), used to decide whether a
|
|
2
|
+
// create*() call with no plugin-injected $$id may fall back to a synthetic id (a
|
|
3
|
+
// bare test) or must fail loud (dev / a real build).
|
|
4
|
+
//
|
|
5
|
+
// `process` is absent in some target runtimes (the browser, certain edge/worker
|
|
6
|
+
// RSC environments), so probe it through `globalThis` with optional chaining —
|
|
7
|
+
// NEVER a bare `process.env.VITEST`, which would ReferenceError before the
|
|
8
|
+
// intended error is thrown. Unlike `process.env.NODE_ENV` (folded by the app's
|
|
9
|
+
// build `define`), `VITEST` is not folded, so this stays a small runtime check;
|
|
10
|
+
// it lives only on the create*() error path (id missing), which never runs in a
|
|
11
|
+
// correct production build.
|
|
12
|
+
//
|
|
13
|
+
// Vitest sets `VITEST` in every test process — the node project and the
|
|
14
|
+
// react-server forks alike (the RSC project forces NODE_ENV=production, so NODE_ENV
|
|
15
|
+
// cannot distinguish it from a real build; `VITEST` can). A real build never sets it.
|
|
16
|
+
export function isUnderTestRunner(): boolean {
|
|
17
|
+
return !!globalThis.process?.env?.VITEST;
|
|
18
|
+
}
|
package/src/search-params.ts
CHANGED
|
@@ -7,9 +7,16 @@
|
|
|
7
7
|
* URLSearchParams instance.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
import { encodeKV } from "./encode-kv.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Decimal-number grammar for `"number"` search params: optional sign, digits
|
|
14
|
+
* with optional fraction, optional exponent. Deliberately excludes hex (`0x`),
|
|
15
|
+
* `Infinity`, and empty/whitespace so `Number()`'s lenient coercions
|
|
16
|
+
* (`Number("")===0`, `Number("0x10")===16`, `Number("Infinity")===Infinity`)
|
|
17
|
+
* do not slip non-decimal values into typed search.
|
|
18
|
+
*/
|
|
19
|
+
const DECIMAL_NUMBER_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
13
20
|
|
|
14
21
|
/** Supported scalar types for search params (append ? for optional). */
|
|
15
22
|
export type SearchSchemaValue =
|
|
@@ -23,10 +30,6 @@ export type SearchSchemaValue =
|
|
|
23
30
|
/** A search schema maps param names to their type descriptors. */
|
|
24
31
|
export type SearchSchema = Record<string, SearchSchemaValue>;
|
|
25
32
|
|
|
26
|
-
// ============================================================================
|
|
27
|
-
// Type-Level Schema Resolution
|
|
28
|
-
// ============================================================================
|
|
29
|
-
|
|
30
33
|
/** Strip trailing `?` from a schema value to get the base type. */
|
|
31
34
|
type BaseType<T extends string> = T extends `${infer B}?` ? B : T;
|
|
32
35
|
|
|
@@ -81,11 +84,11 @@ export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
|
|
|
81
84
|
// ============================================================================
|
|
82
85
|
|
|
83
86
|
/** Resolve the global route map from RegisteredRoutes or GeneratedRouteMap. */
|
|
84
|
-
type GlobalRouteMap = keyof
|
|
85
|
-
? keyof
|
|
87
|
+
type GlobalRouteMap = keyof Rango.RegisteredRoutes extends never
|
|
88
|
+
? keyof Rango.GeneratedRouteMap extends never
|
|
86
89
|
? Record<string, string>
|
|
87
|
-
:
|
|
88
|
-
:
|
|
90
|
+
: Rango.GeneratedRouteMap
|
|
91
|
+
: Rango.RegisteredRoutes;
|
|
89
92
|
|
|
90
93
|
/**
|
|
91
94
|
* Extract the resolved search params type for a named route.
|
|
@@ -163,15 +166,15 @@ type ExtractParamsFromPattern<T extends string> =
|
|
|
163
166
|
: { [K in Param]: string }
|
|
164
167
|
: {};
|
|
165
168
|
|
|
166
|
-
// ============================================================================
|
|
167
|
-
// Runtime Parser
|
|
168
|
-
// ============================================================================
|
|
169
|
-
|
|
170
169
|
/**
|
|
171
170
|
* Parse URLSearchParams into a typed object using the given schema.
|
|
172
171
|
*
|
|
173
172
|
* - `"string"` / `"string?"` - kept as-is
|
|
174
|
-
* - `"number"` / `"number?"` -
|
|
173
|
+
* - `"number"` / `"number?"` - parsed as a finite decimal number. Accepts an
|
|
174
|
+
* optional sign, digits with optional fraction, and optional exponent
|
|
175
|
+
* (e.g. `42`, `-3.5`, `1e3`). Empty/whitespace-only, non-decimal forms
|
|
176
|
+
* (`0x10`), and non-finite (`Infinity`) are treated as missing (omitted),
|
|
177
|
+
* NOT coerced to `0`/`16`/`Infinity`.
|
|
175
178
|
* - `"boolean"` / `"boolean?"` - `"true"` / `"1"` -> true, `"false"` / `"0"` / `""` -> false
|
|
176
179
|
*
|
|
177
180
|
* Missing params (both required and optional) are omitted from the result
|
|
@@ -197,11 +200,17 @@ export function parseSearchParams<T extends SearchSchema>(
|
|
|
197
200
|
if (baseType === "string") {
|
|
198
201
|
result[key] = raw;
|
|
199
202
|
} else if (baseType === "number") {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
// Trim, then require a valid decimal numeral and a finite result.
|
|
204
|
+
// Empty/whitespace, hex (0x10), and Infinity are treated as missing
|
|
205
|
+
// (omitted) — not coerced to 0/16/Infinity by Number()'s lenient rules.
|
|
206
|
+
const trimmed = raw.trim();
|
|
207
|
+
if (trimmed !== "" && DECIMAL_NUMBER_RE.test(trimmed)) {
|
|
208
|
+
const num = Number(trimmed);
|
|
209
|
+
if (Number.isFinite(num)) {
|
|
210
|
+
result[key] = num;
|
|
211
|
+
}
|
|
203
212
|
}
|
|
204
|
-
//
|
|
213
|
+
// Anything else treated as missing (undefined)
|
|
205
214
|
} else if (baseType === "boolean") {
|
|
206
215
|
result[key] = raw === "true" || raw === "1";
|
|
207
216
|
}
|
|
@@ -210,21 +219,17 @@ export function parseSearchParams<T extends SearchSchema>(
|
|
|
210
219
|
return result as ResolveSearchSchema<T>;
|
|
211
220
|
}
|
|
212
221
|
|
|
213
|
-
// ============================================================================
|
|
214
|
-
// Runtime Serializer
|
|
215
|
-
// ============================================================================
|
|
216
|
-
|
|
217
222
|
/**
|
|
218
223
|
* Serialize a typed search params object to a query string (without leading `?`).
|
|
219
|
-
* Skips `undefined` and `null` values.
|
|
224
|
+
* Skips `undefined` and `null` values. Preserves insertion order (no sort).
|
|
220
225
|
*/
|
|
221
226
|
export function serializeSearchParams(params: Record<string, unknown>): string {
|
|
222
|
-
|
|
227
|
+
// Pre-filter null/undefined and coerce values to strings here so encodeKV
|
|
228
|
+
// (which never inspects values) reproduces this call site's exact output.
|
|
229
|
+
const pairs: [string, string][] = [];
|
|
223
230
|
for (const [key, value] of Object.entries(params)) {
|
|
224
231
|
if (value === undefined || value === null) continue;
|
|
225
|
-
|
|
226
|
-
`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`,
|
|
227
|
-
);
|
|
232
|
+
pairs.push([key, String(value)]);
|
|
228
233
|
}
|
|
229
|
-
return
|
|
234
|
+
return encodeKV(pairs);
|
|
230
235
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stable Promise wrappers keyed on the component itself. Objects (React
|
|
5
|
+
* elements, functions, lazy payloads) land in a WeakMap so entries GC when
|
|
6
|
+
* the underlying component is released; primitives (string, number, boolean,
|
|
7
|
+
* null) land in a Map so memoization still applies to text-/null-backed
|
|
8
|
+
* segments like those in partial-update flows. Keeping this cache outside
|
|
9
|
+
* the segment eliminates preservation fields on ResolvedSegment — it survives
|
|
10
|
+
* reconciliation naturally because the component ref is what's stable.
|
|
11
|
+
*
|
|
12
|
+
* Browser-only. On the server each SSR render needs a fresh pending promise
|
|
13
|
+
* so Suspense can emit the loading fallback HTML before content streams. A
|
|
14
|
+
* shared already-resolved promise has `.status === "fulfilled"` attached by
|
|
15
|
+
* React on its first observation — subsequent `use()` calls return
|
|
16
|
+
* synchronously without suspending, so the Suspense fallback never makes it
|
|
17
|
+
* into the initial HTML. Route-definition components share refs across
|
|
18
|
+
* requests, so a global cache would leak tracked state between renders.
|
|
19
|
+
*/
|
|
20
|
+
const IS_BROWSER = typeof window !== "undefined";
|
|
21
|
+
const objectContentCache = IS_BROWSER
|
|
22
|
+
? new WeakMap<object, Promise<ReactNode>>()
|
|
23
|
+
: null;
|
|
24
|
+
const primitiveContentCache = IS_BROWSER
|
|
25
|
+
? new Map<unknown, Promise<ReactNode>>()
|
|
26
|
+
: null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return a stable Promise wrapping `component`, memoized on the component ref.
|
|
30
|
+
*
|
|
31
|
+
* A fresh `Promise.resolve(component)` each render would suspend for one
|
|
32
|
+
* microtask and briefly commit the loading fallback inside Suspender — the
|
|
33
|
+
* intercept / parallel-slot flicker this indirection prevents. Reusing the
|
|
34
|
+
* same Promise ref keeps React's `use()` in "known fulfilled" state after
|
|
35
|
+
* the first observation.
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export function getMemoizedContentPromise(
|
|
40
|
+
component: ReactNode,
|
|
41
|
+
): Promise<ReactNode> {
|
|
42
|
+
if (component instanceof Promise) {
|
|
43
|
+
return component as Promise<ReactNode>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!objectContentCache || !primitiveContentCache) {
|
|
47
|
+
return Promise.resolve(component);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (component !== null && typeof component === "object") {
|
|
51
|
+
const cached = objectContentCache.get(component);
|
|
52
|
+
if (cached) {
|
|
53
|
+
return cached;
|
|
54
|
+
}
|
|
55
|
+
const promise = Promise.resolve(component);
|
|
56
|
+
objectContentCache.set(component, promise);
|
|
57
|
+
return promise;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const cached = primitiveContentCache.get(component);
|
|
61
|
+
if (cached) {
|
|
62
|
+
return cached;
|
|
63
|
+
}
|
|
64
|
+
const promise = Promise.resolve(component);
|
|
65
|
+
primitiveContentCache.set(component, promise);
|
|
66
|
+
return promise;
|
|
67
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ResolvedSegment } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cache of aggregate Promise.all results keyed on the first loader's
|
|
5
|
+
* `loaderData` reference. Each entry holds the source refs it was built from
|
|
6
|
+
* plus the resulting Promise/array; lookup scans entries for the matching
|
|
7
|
+
* source array (typically a single entry, since distinct loader groups rarely
|
|
8
|
+
* share a first source). Object first-refs live in a WeakMap (auto-GC);
|
|
9
|
+
* primitive first-refs (strings/numbers/booleans/null) live in a Map so
|
|
10
|
+
* loaders that resolve to primitive data are memoized too. The per-key array
|
|
11
|
+
* is capped (MAX_ENTRIES_PER_KEY, oldest evicted) so a long session under a
|
|
12
|
+
* stable first-ref does not grow it without bound.
|
|
13
|
+
*
|
|
14
|
+
* Keying externally means reconciliation's fresh segment objects no longer
|
|
15
|
+
* drop memoization — the cache survives as long as the underlying loader
|
|
16
|
+
* segments do, and GC collects entries when those loaders are released
|
|
17
|
+
* (object keys only).
|
|
18
|
+
*
|
|
19
|
+
* Browser-only. On the server each SSR render needs a fresh Promise so
|
|
20
|
+
* Suspense can actually suspend and emit the loading fallback HTML before
|
|
21
|
+
* content streams. A shared already-resolved promise has `.status` attached
|
|
22
|
+
* by React on first `use()`; subsequent observations return synchronously
|
|
23
|
+
* and skip the fallback. The zero-loader case is especially prone because
|
|
24
|
+
* every empty-loader site would otherwise share one promise across requests.
|
|
25
|
+
*/
|
|
26
|
+
const IS_BROWSER = typeof window !== "undefined";
|
|
27
|
+
|
|
28
|
+
interface LoaderCacheEntry {
|
|
29
|
+
sources: any[];
|
|
30
|
+
// buildLoaderPromise always returns a Promise, so the cached value is never a
|
|
31
|
+
// bare array. The public getMemoizedLoaderPromise return type stays broader
|
|
32
|
+
// (Promise<any[]> | any[]) to mirror its siblings.
|
|
33
|
+
promise: Promise<any[]>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Cap the per-key entries array. A stable first-ref (e.g. a layout loader whose
|
|
37
|
+
// loaderData object survives reconciliation across navigations) keeps its
|
|
38
|
+
// WeakMap/Map key alive, while a per-route loader whose ref changes each
|
|
39
|
+
// navigation appends a brand-new sources array under that same live key on
|
|
40
|
+
// every navigation. Nothing was ever removed, so the array grew linearly with
|
|
41
|
+
// navigation count, pinning each stale Promise + sources array from GC — a
|
|
42
|
+
// steady client-side leak over a long session. Only the current render's combo
|
|
43
|
+
// needs to stay warm; evict the oldest beyond the cap.
|
|
44
|
+
const MAX_ENTRIES_PER_KEY = 8;
|
|
45
|
+
|
|
46
|
+
const objectLoaderCache = IS_BROWSER
|
|
47
|
+
? new WeakMap<object, LoaderCacheEntry[]>()
|
|
48
|
+
: null;
|
|
49
|
+
const primitiveLoaderCache = IS_BROWSER
|
|
50
|
+
? new Map<unknown, LoaderCacheEntry[]>()
|
|
51
|
+
: null;
|
|
52
|
+
|
|
53
|
+
// In the browser, a single shared empty aggregate is safe (and desirable) —
|
|
54
|
+
// reusing the same resolved promise keeps React's `use()` in a known-fulfilled
|
|
55
|
+
// state across renders. On the server it would leak `.status = "fulfilled"`
|
|
56
|
+
// across requests and skip the Suspense fallback, so we rebuild on each call.
|
|
57
|
+
const SHARED_EMPTY_LOADER_PROMISE: Promise<any[]> | null = IS_BROWSER
|
|
58
|
+
? Promise.resolve([])
|
|
59
|
+
: null;
|
|
60
|
+
|
|
61
|
+
function hasSameReferences(a: any[], b: any[]): boolean {
|
|
62
|
+
if (a.length !== b.length) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < a.length; i++) {
|
|
66
|
+
if (a[i] !== b[i]) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a fresh aggregate Promise.all over the loaders' resolved data refs.
|
|
75
|
+
* Unlike getMemoizedLoaderPromise this never caches, so each call yields a new
|
|
76
|
+
* Promise — correct for sites that await the result immediately (a shared,
|
|
77
|
+
* already-resolved promise would leak React's `.status` across server requests
|
|
78
|
+
* and skip the Suspense fallback).
|
|
79
|
+
*
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
export function buildLoaderPromise(loaders: ResolvedSegment[]): Promise<any[]> {
|
|
83
|
+
if (loaders.length === 0) {
|
|
84
|
+
return Promise.resolve([]);
|
|
85
|
+
}
|
|
86
|
+
return Promise.all(
|
|
87
|
+
loaders.map((loader) =>
|
|
88
|
+
loader.loaderData instanceof Promise
|
|
89
|
+
? loader.loaderData
|
|
90
|
+
: Promise.resolve(loader.loaderData),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isObjectLike(value: unknown): value is object {
|
|
96
|
+
return (
|
|
97
|
+
value !== null && (typeof value === "object" || typeof value === "function")
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Memoize an aggregate Promise.all for a set of loader segments. Reusing the
|
|
103
|
+
* same aggregate across renders — invalidated only when any underlying
|
|
104
|
+
* loader.loaderData ref changes — keeps React's `use()` in "known fulfilled"
|
|
105
|
+
* state and prevents a fresh Promise.all from suspending (and briefly
|
|
106
|
+
* committing the Suspense fallback) on every partial update that doesn't
|
|
107
|
+
* actually change loader data.
|
|
108
|
+
*
|
|
109
|
+
* @internal
|
|
110
|
+
*/
|
|
111
|
+
export function getMemoizedLoaderPromise(
|
|
112
|
+
loaders: ResolvedSegment[],
|
|
113
|
+
): Promise<any[]> | any[] {
|
|
114
|
+
if (loaders.length === 0) {
|
|
115
|
+
return SHARED_EMPTY_LOADER_PROMISE ?? buildLoaderPromise(loaders);
|
|
116
|
+
}
|
|
117
|
+
if (!objectLoaderCache || !primitiveLoaderCache) {
|
|
118
|
+
return buildLoaderPromise(loaders);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const sources = loaders.map((loader) => loader.loaderData);
|
|
122
|
+
const first = sources[0];
|
|
123
|
+
const entries = isObjectLike(first)
|
|
124
|
+
? objectLoaderCache.get(first)
|
|
125
|
+
: primitiveLoaderCache.get(first);
|
|
126
|
+
|
|
127
|
+
if (entries) {
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (hasSameReferences(entry.sources, sources)) {
|
|
130
|
+
return entry.promise;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const promise = buildLoaderPromise(loaders);
|
|
136
|
+
const newEntry: LoaderCacheEntry = { sources, promise };
|
|
137
|
+
if (entries) {
|
|
138
|
+
// Bound the array: drop the oldest entry before appending when at the cap.
|
|
139
|
+
if (entries.length >= MAX_ENTRIES_PER_KEY) {
|
|
140
|
+
entries.shift();
|
|
141
|
+
}
|
|
142
|
+
entries.push(newEntry);
|
|
143
|
+
} else if (isObjectLike(first)) {
|
|
144
|
+
objectLoaderCache.set(first, [newEntry]);
|
|
145
|
+
} else {
|
|
146
|
+
primitiveLoaderCache.set(first, [newEntry]);
|
|
147
|
+
}
|
|
148
|
+
return promise;
|
|
149
|
+
}
|