@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -4
- package/README.md +296 -887
- package/dist/bin/rango.js +459 -91
- package/dist/testing/vitest.js +36 -2
- package/dist/vite/index.js +1708 -414
- package/package.json +35 -10
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +82 -5
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +14 -9
- package/skills/caching/SKILL.md +221 -12
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +11 -3
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +9 -9
- package/skills/hooks/SKILL.md +73 -900
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +84 -7
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +51 -17
- package/skills/layout/SKILL.md +38 -16
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +48 -20
- package/skills/middleware/SKILL.md +11 -5
- package/skills/migrate-nextjs/SKILL.md +203 -20
- package/skills/migrate-react-router/SKILL.md +59 -675
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +3 -3
- package/skills/observability/SKILL.md +70 -5
- package/skills/parallel/SKILL.md +32 -8
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +59 -28
- package/skills/rango/SKILL.md +124 -50
- package/skills/response-routes/SKILL.md +78 -46
- package/skills/route/SKILL.md +85 -6
- package/skills/router-setup/SKILL.md +41 -6
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +28 -3
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +68 -654
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +131 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -918
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +47 -17
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +178 -100
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +81 -68
- package/src/browser/navigation-client.ts +115 -70
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +153 -88
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +157 -144
- package/src/browser/prefetch/cache.ts +148 -81
- package/src/browser/prefetch/fetch.ts +231 -51
- package/src/browser/prefetch/queue.ts +25 -7
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +40 -7
- package/src/browser/react/NavigationProvider.tsx +140 -99
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +17 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +14 -15
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- 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 +0 -2
- package/src/browser/react/use-router.ts +6 -4
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +74 -8
- package/src/browser/rsc-router.tsx +97 -22
- package/src/browser/scroll-restoration.ts +15 -8
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +216 -38
- package/src/browser/types.ts +94 -22
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/generate-route-types.ts +1 -1
- package/src/build/index.ts +11 -5
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +152 -22
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +12 -1
- package/src/build/route-types/include-resolution.ts +455 -61
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +15 -2
- package/src/build/route-types/router-processing.ts +77 -41
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +454 -101
- package/src/cache/cache-scope.ts +159 -54
- package/src/cache/cache-tag.ts +149 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2170 -377
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +126 -41
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +243 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +13 -21
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +194 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1132 -0
- package/src/client.rsc.tsx +39 -22
- package/src/client.tsx +28 -58
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +13 -1
- package/src/decode-loader-results.ts +18 -2
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +0 -3
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +57 -40
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +7 -0
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +7 -12
- package/src/index.rsc.ts +88 -8
- package/src/index.ts +90 -16
- package/src/internal-debug.ts +11 -10
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- 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 +75 -7
- package/src/redirect-origin.ts +114 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +25 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +13 -49
- package/src/route-definition/dsl-helpers.ts +60 -53
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +46 -46
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +6 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +0 -5
- package/src/router/content-negotiation.ts +58 -23
- package/src/router/error-handling.ts +44 -17
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +6 -1
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +35 -2
- package/src/router/lazy-includes.ts +79 -56
- package/src/router/loader-resolution.ts +151 -73
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +76 -52
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +181 -178
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +115 -194
- package/src/router/match-middleware/cache-store.ts +61 -50
- 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 +36 -67
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +231 -120
- package/src/router/navigation-snapshot.ts +7 -56
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +99 -152
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +111 -66
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +47 -42
- package/src/router/revalidation.ts +75 -81
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +70 -8
- package/src/router/router-options.ts +126 -4
- package/src/router/segment-resolution/fresh.ts +104 -80
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +155 -39
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +215 -304
- 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 +35 -15
- 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 +54 -35
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +9 -23
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -64
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +13 -5
- package/src/router.ts +119 -48
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +267 -152
- package/src/rsc/helpers.ts +78 -4
- package/src/rsc/index.ts +1 -4
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +29 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +11 -15
- package/src/rsc/progressive-enhancement.ts +120 -13
- package/src/rsc/redirect-guard.ts +100 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +58 -141
- package/src/rsc/rsc-rendering.ts +492 -49
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +268 -82
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +45 -3
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +31 -26
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +260 -95
- package/src/server/context.ts +99 -9
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +125 -2
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +379 -138
- package/src/ssr/index.tsx +491 -182
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +14 -31
- package/src/testing/dispatch.ts +533 -160
- package/src/testing/e2e/fixture.ts +45 -11
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/e2e/parity.ts +85 -4
- package/src/testing/e2e/server.ts +12 -0
- package/src/testing/flight-matchers.ts +7 -14
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +36 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +30 -0
- package/src/testing/flight.ts +145 -70
- package/src/testing/generated-routes.ts +26 -50
- package/src/testing/index.ts +18 -19
- package/src/testing/internal/context.ts +184 -68
- 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 +134 -115
- package/src/testing/run-loader.ts +140 -51
- package/src/testing/run-middleware.ts +59 -33
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
- package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
- package/src/testing/vitest.ts +138 -16
- package/src/theme/ThemeProvider.tsx +56 -84
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +45 -15
- package/src/types/index.ts +2 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +8 -22
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +100 -13
- package/src/urls/include-helper.ts +10 -12
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -8
- package/src/urls/path-helper-types.ts +52 -14
- package/src/urls/path-helper.ts +5 -54
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +76 -42
- package/src/urls/urls-function.ts +0 -14
- package/src/use-loader.tsx +0 -186
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +34 -43
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/discovery/prerender-collection.ts +33 -46
- package/src/vite/discovery/state.ts +12 -1
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/index.ts +9 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +143 -10
- package/src/vite/plugins/cjs-to-esm.ts +8 -12
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +85 -56
- package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
- package/src/vite/plugins/expose-internal-ids.ts +10 -1
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +22 -27
- package/src/vite/plugins/version-plugin.ts +6 -66
- package/src/vite/plugins/virtual-entries.ts +137 -26
- package/src/vite/rango.ts +146 -135
- package/src/vite/router-discovery.ts +189 -48
- package/src/vite/utils/ast-handler-extract.ts +11 -20
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +4 -75
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +71 -44
- package/src/vite/utils/shared-utils.ts +55 -37
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -10,7 +10,10 @@ import type {
|
|
|
10
10
|
HandleData,
|
|
11
11
|
StreamingToken,
|
|
12
12
|
} from "./types.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
filterSegmentOrder,
|
|
15
|
+
filterRouteSegmentIds,
|
|
16
|
+
} from "./react/filter-segment-order.js";
|
|
14
17
|
|
|
15
18
|
// Polyfill Symbol.dispose for Safari and older browsers
|
|
16
19
|
if (typeof Symbol.dispose === "undefined") {
|
|
@@ -165,6 +168,22 @@ export interface ActionHandle extends Disposable {
|
|
|
165
168
|
startStreaming(): StreamingToken;
|
|
166
169
|
/** Record segments that were revalidated */
|
|
167
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>;
|
|
168
187
|
/** Complete the action with result */
|
|
169
188
|
complete(result?: unknown): void;
|
|
170
189
|
/** Fail the action with error */
|
|
@@ -191,7 +210,12 @@ export interface EventController {
|
|
|
191
210
|
abortNavigation(): void;
|
|
192
211
|
|
|
193
212
|
// Action operations
|
|
194
|
-
startAction(
|
|
213
|
+
startAction(
|
|
214
|
+
actionId: string,
|
|
215
|
+
args: unknown[],
|
|
216
|
+
/** Originating history entry key; scopes location-state arbitration. */
|
|
217
|
+
cohort?: string,
|
|
218
|
+
): ActionHandle;
|
|
195
219
|
abortAllActions(): void;
|
|
196
220
|
|
|
197
221
|
// State access
|
|
@@ -225,6 +249,14 @@ export interface EventController {
|
|
|
225
249
|
resolvedIds?: string[],
|
|
226
250
|
): void;
|
|
227
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;
|
|
228
260
|
|
|
229
261
|
// Params operations
|
|
230
262
|
setParams(params: Record<string, string>): void;
|
|
@@ -237,10 +269,6 @@ export interface EventController {
|
|
|
237
269
|
hadAnyConcurrentActions(): boolean;
|
|
238
270
|
}
|
|
239
271
|
|
|
240
|
-
// ============================================================================
|
|
241
|
-
// Default States
|
|
242
|
-
// ============================================================================
|
|
243
|
-
|
|
244
272
|
const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
245
273
|
state: "idle",
|
|
246
274
|
actionId: null,
|
|
@@ -249,6 +277,13 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
|
249
277
|
result: null,
|
|
250
278
|
};
|
|
251
279
|
|
|
280
|
+
// Shared empty inflight-actions list. getState() hands back this exact reference
|
|
281
|
+
// whenever no action is inflight (the overwhelmingly common case), so the derived
|
|
282
|
+
// snapshot's `inflightActions` is referentially stable across notifies instead of
|
|
283
|
+
// a fresh [] each call. Read-only by contract (consumers only read length/spread),
|
|
284
|
+
// same as the shared DEFAULT_ACTION_STATE.
|
|
285
|
+
const EMPTY_INFLIGHT_ACTIONS: InflightAction[] = [];
|
|
286
|
+
|
|
252
287
|
/**
|
|
253
288
|
* Check if a subscription ID matches an action's full ID.
|
|
254
289
|
*
|
|
@@ -261,16 +296,12 @@ function matchesActionId(
|
|
|
261
296
|
entryActionId: string,
|
|
262
297
|
): boolean {
|
|
263
298
|
if (subscriptionId.includes("#")) {
|
|
264
|
-
// Full ID: exact match
|
|
265
299
|
return subscriptionId === entryActionId;
|
|
266
300
|
}
|
|
267
|
-
// Action name only: suffix match (matches "anything#actionName")
|
|
268
301
|
return entryActionId.endsWith(`#${subscriptionId}`);
|
|
269
302
|
}
|
|
270
303
|
|
|
271
|
-
//
|
|
272
|
-
// setTimeout(0) batching prevents render storms. Each notifier owns its timer
|
|
273
|
-
// so listener kinds coalesce independently.
|
|
304
|
+
// Batch rapid notifications into one microtask to prevent render storms
|
|
274
305
|
function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
275
306
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
276
307
|
return () => {
|
|
@@ -282,13 +313,6 @@ function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
|
282
313
|
};
|
|
283
314
|
}
|
|
284
315
|
|
|
285
|
-
// ============================================================================
|
|
286
|
-
// Implementation
|
|
287
|
-
// ============================================================================
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Configuration for creating an event controller
|
|
291
|
-
*/
|
|
292
316
|
export interface EventControllerConfig {
|
|
293
317
|
initialLocation?: NavigationLocation;
|
|
294
318
|
}
|
|
@@ -306,51 +330,68 @@ export interface EventControllerConfig {
|
|
|
306
330
|
export function createEventController(
|
|
307
331
|
config?: EventControllerConfig,
|
|
308
332
|
): EventController {
|
|
309
|
-
// ========================================================================
|
|
310
|
-
// Source of Truth
|
|
311
|
-
// ========================================================================
|
|
312
|
-
|
|
313
|
-
// Current navigation in progress (null = idle)
|
|
314
333
|
let currentNavigation: NavigationEntry | null = null;
|
|
315
334
|
|
|
316
|
-
// All in-flight actions (keyed by unique instance ID)
|
|
317
335
|
const inflightActions = new Map<string, ActionEntry>();
|
|
318
336
|
|
|
319
|
-
// Committed location (updated when navigation completes)
|
|
320
337
|
let location: NavigationLocation =
|
|
321
338
|
config?.initialLocation ??
|
|
322
339
|
(typeof window !== "undefined"
|
|
323
340
|
? new URL(window.location.href)
|
|
324
341
|
: new URL("/", "http://localhost"));
|
|
325
342
|
|
|
326
|
-
// Track if any concurrent actions occurred (for consolidation)
|
|
327
343
|
let hadAnyConcurrentActions = false;
|
|
328
344
|
|
|
329
|
-
// Track segments revalidated by concurrent actions
|
|
330
345
|
const concurrentRevalidatedSegments = new Set<string>();
|
|
331
346
|
|
|
332
|
-
//
|
|
347
|
+
// Monotonic dispatch counter: every startAction() takes the next value
|
|
348
|
+
// (private), so a larger sequence means the action was initiated later.
|
|
349
|
+
let actionDispatchSeq = 0;
|
|
350
|
+
|
|
351
|
+
// Concurrent location-state arbitration, scoped PER COHORT (history entry).
|
|
352
|
+
// Each cohort owns a slotKey->winningDispatchSeq map plus a refcount of its
|
|
353
|
+
// inflight actions; claimLocationState() consults its own cohort's map so
|
|
354
|
+
// same-key writes within one entry resolve to the last-initiated action
|
|
355
|
+
// regardless of settle order, while actions on different entries never
|
|
356
|
+
// compete. A cohort's map is freed once its LAST action's cleanup runs — the
|
|
357
|
+
// same brief post-settle grace (doSettle's 100ms timer) as other action
|
|
358
|
+
// teardown, not when every action everywhere settles — so a long-running
|
|
359
|
+
// action in one cohort can never retain arbitration keys from other cohorts
|
|
360
|
+
// that have since drained. It is never cleared in clearConsolidation, which
|
|
361
|
+
// fires
|
|
362
|
+
// per-action on divert/error and would let a later-settling earlier action
|
|
363
|
+
// wrongly reclaim a key a sibling already won.
|
|
364
|
+
const cohortArbitration = new Map<
|
|
365
|
+
string,
|
|
366
|
+
{ keySeq: Map<string, number>; inflight: number }
|
|
367
|
+
>();
|
|
368
|
+
|
|
333
369
|
let activeStreamCount = 0;
|
|
334
370
|
|
|
335
|
-
// Handle data from RSC payload
|
|
336
371
|
let handleData: HandleData = {};
|
|
337
372
|
let handleSegmentOrder: string[] = [];
|
|
338
373
|
let routeSegmentIds: string[] = [];
|
|
339
374
|
|
|
340
|
-
// Merged route params from current match
|
|
341
375
|
let routeParams: Record<string, string> = {};
|
|
342
376
|
|
|
343
|
-
// ========================================================================
|
|
344
|
-
// Listeners
|
|
345
|
-
// ========================================================================
|
|
346
|
-
|
|
347
377
|
const stateListeners = new Set<StateListener>();
|
|
348
378
|
const actionListeners = new Map<string, Set<ActionStateListener>>();
|
|
349
379
|
const handleListeners = new Set<HandleListener>();
|
|
350
380
|
|
|
351
|
-
const
|
|
381
|
+
const notifyStateListeners = makeDebouncedNotifier(stateListeners);
|
|
382
|
+
|
|
383
|
+
// Memoized derived snapshot. Every state mutation already funnels through
|
|
384
|
+
// notify(), so invalidating here (synchronously, before the debounced fire)
|
|
385
|
+
// means a getState() call between two mutations reuses the same object — an
|
|
386
|
+
// unchanged state returns the SAME reference — while any real change recomputes
|
|
387
|
+
// on the next read. Kept null when dirty.
|
|
388
|
+
let cachedDerivedState: DerivedNavigationState | null = null;
|
|
389
|
+
|
|
390
|
+
function notify(): void {
|
|
391
|
+
cachedDerivedState = null;
|
|
392
|
+
notifyStateListeners();
|
|
393
|
+
}
|
|
352
394
|
|
|
353
|
-
// Debounce per-action notifications
|
|
354
395
|
const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
355
396
|
|
|
356
397
|
function notifyAction(actionId: string) {
|
|
@@ -362,8 +403,6 @@ export function createEventController(
|
|
|
362
403
|
actionId,
|
|
363
404
|
setTimeout(() => {
|
|
364
405
|
actionNotifyTimeouts.delete(actionId);
|
|
365
|
-
// Notify all listeners whose subscription ID matches this action
|
|
366
|
-
// This includes exact matches and suffix matches (e.g., "addToCart" matches "hash#addToCart")
|
|
367
406
|
for (const [subscriptionId, listeners] of actionListeners) {
|
|
368
407
|
if (matchesActionId(subscriptionId, actionId)) {
|
|
369
408
|
const state = getActionState(subscriptionId);
|
|
@@ -376,33 +415,32 @@ export function createEventController(
|
|
|
376
415
|
|
|
377
416
|
const notifyHandles = makeDebouncedNotifier(handleListeners);
|
|
378
417
|
|
|
379
|
-
// ========================================================================
|
|
380
|
-
// Derived State
|
|
381
|
-
// ========================================================================
|
|
382
|
-
|
|
383
418
|
function getState(): DerivedNavigationState {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
419
|
+
if (cachedDerivedState) return cachedDerivedState;
|
|
420
|
+
|
|
421
|
+
// Skip the spread/filter/map entirely when idle — the common case — and hand
|
|
422
|
+
// back the shared frozen empty list for referential stability.
|
|
423
|
+
const inflightActionsList: InflightAction[] =
|
|
424
|
+
inflightActions.size === 0
|
|
425
|
+
? EMPTY_INFLIGHT_ACTIONS
|
|
426
|
+
: [...inflightActions.values()]
|
|
427
|
+
.filter((a) => a.phase !== "settling")
|
|
428
|
+
.map((a) => ({
|
|
429
|
+
id: a.id,
|
|
430
|
+
actionId: a.actionId,
|
|
431
|
+
payload: a.payload,
|
|
432
|
+
startedAt: a.startedAt,
|
|
433
|
+
}));
|
|
434
|
+
|
|
396
435
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
397
436
|
const isVisibleNavigation =
|
|
398
437
|
currentNavigation !== null &&
|
|
399
438
|
!currentNavigation.options?.skipLoadingState;
|
|
400
439
|
const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
401
440
|
|
|
402
|
-
// Streaming: true if any active streams (navigation or action) or loading
|
|
403
441
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
404
442
|
|
|
405
|
-
|
|
443
|
+
cachedDerivedState = {
|
|
406
444
|
state,
|
|
407
445
|
isStreaming,
|
|
408
446
|
// True when a navigation is active (fetching or streaming, before
|
|
@@ -418,11 +456,14 @@ export function createEventController(
|
|
|
418
456
|
: null,
|
|
419
457
|
inflightActions: inflightActionsList,
|
|
420
458
|
};
|
|
459
|
+
return cachedDerivedState;
|
|
421
460
|
}
|
|
422
461
|
|
|
423
462
|
function getActionState(actionId: string): TrackedActionState {
|
|
424
|
-
//
|
|
425
|
-
//
|
|
463
|
+
// Nothing inflight — skip building/scanning the list and return the shared
|
|
464
|
+
// idle snapshot (the same reference use-action falls back to).
|
|
465
|
+
if (inflightActions.size === 0) return DEFAULT_ACTION_STATE;
|
|
466
|
+
|
|
426
467
|
const entry = [...inflightActions.values()]
|
|
427
468
|
.filter((a) => matchesActionId(actionId, a.actionId))
|
|
428
469
|
.reduce<ActionEntry | undefined>((best, a) => {
|
|
@@ -437,7 +478,6 @@ export function createEventController(
|
|
|
437
478
|
return { ...DEFAULT_ACTION_STATE };
|
|
438
479
|
}
|
|
439
480
|
|
|
440
|
-
// Derive state from phase
|
|
441
481
|
let state: ActionLifecycleState;
|
|
442
482
|
switch (entry.phase) {
|
|
443
483
|
case "fetching":
|
|
@@ -552,10 +592,29 @@ export function createEventController(
|
|
|
552
592
|
// Action Operations
|
|
553
593
|
// ========================================================================
|
|
554
594
|
|
|
555
|
-
function startAction(
|
|
595
|
+
function startAction(
|
|
596
|
+
actionId: string,
|
|
597
|
+
args: unknown[],
|
|
598
|
+
cohort?: string,
|
|
599
|
+
): ActionHandle {
|
|
556
600
|
const id = `${actionId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
601
|
+
// Private to this handle: never exposed on the returned ActionHandle.
|
|
602
|
+
const dispatchSeq = actionDispatchSeq++;
|
|
557
603
|
const abort = new AbortController();
|
|
558
604
|
|
|
605
|
+
// Register this action under its cohort (originating history entry). The
|
|
606
|
+
// cohort's arbitration is created on first use and freed when its last
|
|
607
|
+
// action settles (see doSettle). Keyless entries share the "" cohort.
|
|
608
|
+
const cohortId = cohort ?? "";
|
|
609
|
+
let arb = cohortArbitration.get(cohortId);
|
|
610
|
+
if (!arb) {
|
|
611
|
+
arb = { keySeq: new Map<string, number>(), inflight: 0 };
|
|
612
|
+
cohortArbitration.set(cohortId, arb);
|
|
613
|
+
}
|
|
614
|
+
// const so the captured reference stays non-undefined inside doSettle.
|
|
615
|
+
const arbitration = arb;
|
|
616
|
+
arbitration.inflight++;
|
|
617
|
+
|
|
559
618
|
// Track if this action started while others were pending (concurrent)
|
|
560
619
|
const hadConcurrent = inflightActions.size > 0;
|
|
561
620
|
if (hadConcurrent) {
|
|
@@ -579,11 +638,28 @@ export function createEventController(
|
|
|
579
638
|
let settled = false;
|
|
580
639
|
let streamingEnded = false;
|
|
581
640
|
let actionCompleted = false;
|
|
641
|
+
let cohortReleased = false;
|
|
582
642
|
let pendingResult:
|
|
583
643
|
| { type: "success"; value?: unknown }
|
|
584
644
|
| { type: "error"; value: unknown }
|
|
585
645
|
| null = null;
|
|
586
646
|
|
|
647
|
+
// Release this action's hold on its cohort arbitration exactly once: drop
|
|
648
|
+
// the refcount and, only if the map still points at THIS arbitration object,
|
|
649
|
+
// delete it. A newer generation may have replaced it (e.g. abortAllActions
|
|
650
|
+
// cleared the map and a fresh action recreated the same cohort id), so a
|
|
651
|
+
// stale settlement must never delete the newer one by id.
|
|
652
|
+
function releaseCohort() {
|
|
653
|
+
if (cohortReleased) return;
|
|
654
|
+
cohortReleased = true;
|
|
655
|
+
if (
|
|
656
|
+
--arbitration.inflight <= 0 &&
|
|
657
|
+
cohortArbitration.get(cohortId) === arbitration
|
|
658
|
+
) {
|
|
659
|
+
cohortArbitration.delete(cohortId);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
587
663
|
function doSettle() {
|
|
588
664
|
if (settled) return;
|
|
589
665
|
settled = true;
|
|
@@ -591,6 +667,9 @@ export function createEventController(
|
|
|
591
667
|
// Cleanup after brief delay (allow useAction to read result)
|
|
592
668
|
setTimeout(() => {
|
|
593
669
|
inflightActions.delete(id);
|
|
670
|
+
// Free this cohort's arbitration once its last action has settled, so
|
|
671
|
+
// a long-running action elsewhere cannot pin keys from a drained entry.
|
|
672
|
+
releaseCohort();
|
|
594
673
|
// Check for consolidation
|
|
595
674
|
if (inflightActions.size === 0) {
|
|
596
675
|
// All actions done - reset tracking
|
|
@@ -667,6 +746,26 @@ export function createEventController(
|
|
|
667
746
|
segmentIds.forEach((id) => concurrentRevalidatedSegments.add(id));
|
|
668
747
|
},
|
|
669
748
|
|
|
749
|
+
claimLocationState(state: Record<string, unknown>) {
|
|
750
|
+
const winning: Record<string, unknown> = {};
|
|
751
|
+
// Arbitrate against this action's OWN captured arbitration object, not a
|
|
752
|
+
// live map lookup: a concurrent map clear/replace (abortAllActions, a
|
|
753
|
+
// stale settlement) must not make this action silently stop recording
|
|
754
|
+
// and accept every key.
|
|
755
|
+
const keySeq = arbitration.keySeq;
|
|
756
|
+
for (const key of Object.keys(state)) {
|
|
757
|
+
const prevSeq = keySeq.get(key);
|
|
758
|
+
// Strictly-greater: a later-initiated action wins a key over an
|
|
759
|
+
// earlier one in the same cohort regardless of arrival order. Equal
|
|
760
|
+
// cannot happen (dispatchSeq is unique per action).
|
|
761
|
+
if (prevSeq === undefined || dispatchSeq > prevSeq) {
|
|
762
|
+
keySeq.set(key, dispatchSeq);
|
|
763
|
+
winning[key] = state[key];
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return winning;
|
|
767
|
+
},
|
|
768
|
+
|
|
670
769
|
complete(result?: unknown) {
|
|
671
770
|
settleWith({ type: "success", value: result });
|
|
672
771
|
},
|
|
@@ -688,6 +787,10 @@ export function createEventController(
|
|
|
688
787
|
[Symbol.dispose]() {
|
|
689
788
|
// If aborted, another navigation/error took over - don't touch state
|
|
690
789
|
if (abort.signal.aborted) {
|
|
790
|
+
// Aborted actions skip doSettle, so release the cohort hold here to
|
|
791
|
+
// keep the per-cohort refcount balanced (no leak when an action is
|
|
792
|
+
// aborted individually rather than via abortAllActions).
|
|
793
|
+
releaseCohort();
|
|
691
794
|
inflightActions.delete(id);
|
|
692
795
|
notify();
|
|
693
796
|
notifyAction(actionId);
|
|
@@ -722,6 +825,7 @@ export function createEventController(
|
|
|
722
825
|
}
|
|
723
826
|
hadAnyConcurrentActions = false;
|
|
724
827
|
concurrentRevalidatedSegments.clear();
|
|
828
|
+
cohortArbitration.clear();
|
|
725
829
|
notify();
|
|
726
830
|
// Notify all action listeners directly by subscription ID.
|
|
727
831
|
// actionListeners keys are subscription IDs (possibly short names like
|
|
@@ -748,9 +852,7 @@ export function createEventController(
|
|
|
748
852
|
const newSegmentOrder = filterSegmentOrder(rawMatched);
|
|
749
853
|
// Separate list for useSegments(): "layouts and routes only" — strip
|
|
750
854
|
// parallels (".@") and loader sub-ids (D digit) without reordering.
|
|
751
|
-
const newRouteSegmentIds = rawMatched
|
|
752
|
-
(id) => !id.includes(".@") && !/D\d+\./.test(id),
|
|
753
|
-
);
|
|
855
|
+
const newRouteSegmentIds = filterRouteSegmentIds(rawMatched);
|
|
754
856
|
|
|
755
857
|
if (isPartial && newSegmentOrder.length > 0) {
|
|
756
858
|
// Partial update: merge new data with existing
|
|
@@ -797,6 +899,18 @@ export function createEventController(
|
|
|
797
899
|
};
|
|
798
900
|
}
|
|
799
901
|
|
|
902
|
+
function setRouteSegmentIds(matched: string[]): void {
|
|
903
|
+
const next = filterRouteSegmentIds(matched);
|
|
904
|
+
if (
|
|
905
|
+
next.length === routeSegmentIds.length &&
|
|
906
|
+
next.every((id, i) => id === routeSegmentIds[i])
|
|
907
|
+
) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
routeSegmentIds = next;
|
|
911
|
+
notifyHandles();
|
|
912
|
+
}
|
|
913
|
+
|
|
800
914
|
// ========================================================================
|
|
801
915
|
// Subscriptions
|
|
802
916
|
// ========================================================================
|
|
@@ -865,6 +979,7 @@ export function createEventController(
|
|
|
865
979
|
// Handles
|
|
866
980
|
setHandleData,
|
|
867
981
|
getHandleState,
|
|
982
|
+
setRouteSegmentIds,
|
|
868
983
|
|
|
869
984
|
// Params
|
|
870
985
|
setParams,
|
|
@@ -881,40 +996,3 @@ export function createEventController(
|
|
|
881
996
|
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
882
997
|
};
|
|
883
998
|
}
|
|
884
|
-
|
|
885
|
-
// ============================================================================
|
|
886
|
-
// Singleton
|
|
887
|
-
// ============================================================================
|
|
888
|
-
|
|
889
|
-
let controllerInstance: EventController | null = null;
|
|
890
|
-
|
|
891
|
-
/**
|
|
892
|
-
* Initialize the global event controller
|
|
893
|
-
*/
|
|
894
|
-
export function initEventController(
|
|
895
|
-
config?: EventControllerConfig,
|
|
896
|
-
): EventController {
|
|
897
|
-
if (!controllerInstance) {
|
|
898
|
-
controllerInstance = createEventController(config);
|
|
899
|
-
}
|
|
900
|
-
return controllerInstance;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* Get the global event controller
|
|
905
|
-
*/
|
|
906
|
-
export function getEventController(): EventController {
|
|
907
|
-
if (!controllerInstance) {
|
|
908
|
-
throw new Error(
|
|
909
|
-
"Event controller not initialized. Call initEventController first.",
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
|
-
return controllerInstance;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* Reset the controller instance (for testing)
|
|
917
|
-
*/
|
|
918
|
-
export function resetEventController(): void {
|
|
919
|
-
controllerInstance = null;
|
|
920
|
-
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client seat of `invalidateClientCache()` (the `default` export condition).
|
|
3
|
+
*
|
|
4
|
+
* Makes the current client behave as if a server action had just completed:
|
|
5
|
+
* the history cache is marked stale (SWR), the prefetch map is flushed, the
|
|
6
|
+
* state rotates, and sibling tabs are broadcast to — the same
|
|
7
|
+
* `markCacheAsStaleAndBroadcast()` path the server-action bridge uses. This is
|
|
8
|
+
* the gentler mark-stale (not hard-clear) behavior, so Back renders the cached
|
|
9
|
+
* entry instantly and revalidates.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getRegisteredStore } from "./navigation-store-handle.js";
|
|
13
|
+
import { clearPrefetchCache } from "./prefetch/cache.js";
|
|
14
|
+
|
|
15
|
+
export function invalidateClientCache(): void {
|
|
16
|
+
if (typeof document === "undefined") {
|
|
17
|
+
// SSR pass of a client component also resolves the default condition. A
|
|
18
|
+
// render-time call must not take down the page; no-op with a dev warning.
|
|
19
|
+
if (process.env.NODE_ENV !== "production") {
|
|
20
|
+
console.warn(
|
|
21
|
+
"[rango] invalidateClientCache() was called during a server render; " +
|
|
22
|
+
"it is a no-op outside the browser.",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const store = getRegisteredStore();
|
|
29
|
+
if (store) {
|
|
30
|
+
store.markCacheAsStaleAndBroadcast();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Pre-boot: no store registered yet. clearPrefetchCache() (which rotates the
|
|
35
|
+
// state) is complete at this point — there is no history cache to mark and no
|
|
36
|
+
// sibling state worth broadcasting.
|
|
37
|
+
clearPrefetchCache();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Client no-op for `keepClientCache()`. It is a server action directive (the
|
|
42
|
+
* `react-server` condition sets a response header the action bridge reads);
|
|
43
|
+
* there is nothing to suppress from the client side.
|
|
44
|
+
*/
|
|
45
|
+
export function keepClientCache(): void {
|
|
46
|
+
if (process.env.NODE_ENV !== "production") {
|
|
47
|
+
console.warn(
|
|
48
|
+
"[rango] keepClientCache() has no effect on the client; it is a server " +
|
|
49
|
+
"action directive. Call it from inside a server action.",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/browser/logging.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Module-level debug flag for guarding call sites whose debug arguments do
|
|
5
|
+
* non-trivial work (joins, maps, spreads, object literals). Because the Vite
|
|
6
|
+
* transform folds INTERNAL_RANGO_DEBUG to a literal, an `if (IS_BROWSER_DEBUG)`
|
|
7
|
+
* guard is minifier-DCE-able — the whole block drops from the production bundle.
|
|
8
|
+
* Prefer this over isBrowserDebugEnabled() at hot call sites: a const guard folds
|
|
9
|
+
* where a function call may not.
|
|
10
|
+
*/
|
|
11
|
+
export const IS_BROWSER_DEBUG: boolean = INTERNAL_RANGO_DEBUG;
|
|
12
|
+
|
|
3
13
|
interface BrowserLogContext {
|
|
4
14
|
requestId: string;
|
|
5
15
|
txId: string;
|
|
@@ -53,3 +63,21 @@ export function debugLog(msg: string, ...args: unknown[]): void {
|
|
|
53
63
|
console.log(msg, ...args);
|
|
54
64
|
}
|
|
55
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Boot-sequence debug log: one line per initial-document step (flight decode,
|
|
69
|
+
* handle stream, bridge wiring, initial tree build, hydration commit), each
|
|
70
|
+
* stamped with performance.now() so the gap BEFORE hydrateRoot is visible.
|
|
71
|
+
* The initial document path was otherwise silent — FE debug only started
|
|
72
|
+
* talking at the first soft navigation, so a boot stall (e.g. an await that
|
|
73
|
+
* holds initBrowserApp, and with it hydrateRoot) was invisible.
|
|
74
|
+
*/
|
|
75
|
+
export function bootLog(step: string, details?: Record<string, unknown>): void {
|
|
76
|
+
if (!INTERNAL_RANGO_DEBUG) return;
|
|
77
|
+
const prefix = `[Browser][boot] ${step} @ ${Math.round(performance.now())}ms`;
|
|
78
|
+
if (details) {
|
|
79
|
+
console.log(prefix, details);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
console.log(prefix);
|
|
83
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResolvedSegment } from "./types.js";
|
|
2
|
-
import { debugLog } from "./logging.js";
|
|
2
|
+
import { debugLog, IS_BROWSER_DEBUG } from "./logging.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Merge partial loader data from server with cached loader data.
|
|
@@ -19,9 +19,11 @@ export function mergeSegmentLoaders(
|
|
|
19
19
|
const serverLoaderIds = fromServer.loaderIds || [];
|
|
20
20
|
const cachedLoaderIds = fromCache.loaderIds || [];
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
if (IS_BROWSER_DEBUG) {
|
|
23
|
+
debugLog(
|
|
24
|
+
`[Browser] Merging partial loaders: server has ${serverLoaderIds.join(", ")}, cache has ${cachedLoaderIds.join(", ")}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
25
27
|
|
|
26
28
|
return {
|
|
27
29
|
...fromCache,
|