@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650
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/README.md +24 -9
- package/dist/bin/rango.js +157 -63
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1584 -639
- package/package.json +71 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +235 -28
- package/skills/host-router/SKILL.md +122 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +29 -5
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +170 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +11 -7
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +250 -25
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +114 -47
- package/skills/route/SKILL.md +42 -5
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +78 -42
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -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 +121 -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 +120 -0
- package/skills/typesafety/SKILL.md +316 -26
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +37 -143
- 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 +30 -59
- package/src/browser/navigation-client.ts +96 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +60 -127
- package/src/browser/prefetch/cache.ts +82 -72
- package/src/browser/prefetch/fetch.ts +108 -33
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +41 -48
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +20 -5
- 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 +52 -1
- package/src/browser/rsc-router.tsx +70 -34
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +168 -44
- package/src/browser/types.ts +36 -21
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +89 -10
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +122 -22
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2255 -238
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +25 -61
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- 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 +63 -9
- package/src/index.ts +64 -9
- package/src/internal-debug.ts +2 -4
- 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/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +244 -281
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -15
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +4 -41
- package/src/router/intercept-resolution.ts +14 -19
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +91 -46
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +150 -271
- package/src/router/match-middleware/cache-store.ts +3 -33
- 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 +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +5 -112
- package/src/router/middleware.ts +118 -133
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +62 -67
- package/src/router/prerender-match.ts +99 -63
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +50 -56
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +68 -35
- package/src/router/router-options.ts +55 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +44 -63
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +203 -285
- 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 +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +87 -48
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +80 -41
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +83 -78
- package/src/rsc/helpers.ts +93 -5
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +12 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -62
- package/src/rsc/rsc-rendering.ts +41 -60
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +62 -67
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +10 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +199 -142
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +165 -87
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -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 +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -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 +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -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 +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +26 -34
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +39 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +185 -10
- package/src/vite/plugins/cjs-to-esm.ts +3 -18
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +3 -54
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +57 -67
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +35 -17
- package/src/vite/rango.ts +148 -115
- package/src/vite/router-discovery.ts +220 -68
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -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 +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -34
- package/src/vite/utils/shared-utils.ts +95 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
classifyActionResponse,
|
|
3
|
-
type ActionScenario,
|
|
4
|
-
} from "./action-response-classifier.js";
|
|
5
1
|
import type { ActionEntry } from "./event-controller.js";
|
|
6
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Post-reconciliation action outcome (discriminated union). Error and
|
|
5
|
+
* full-update-unsupported cases are handled inline in the bridge before
|
|
6
|
+
* reconciliation; this only covers successfully-reconciled partial responses.
|
|
7
|
+
*/
|
|
8
|
+
type ActionScenario =
|
|
9
|
+
| {
|
|
10
|
+
type: "navigated-away";
|
|
11
|
+
historyKeyChanged: boolean;
|
|
12
|
+
onInterceptRoute: boolean;
|
|
13
|
+
}
|
|
14
|
+
| { type: "hmr-missing" }
|
|
15
|
+
| { type: "consolidation-needed"; segmentIds: string[] }
|
|
16
|
+
| { type: "concurrent-skip"; otherFetchingCount: number }
|
|
17
|
+
| { type: "normal" };
|
|
18
|
+
|
|
7
19
|
/**
|
|
8
20
|
* Plain data inputs for classifying a post-reconciliation action outcome.
|
|
9
21
|
* No browser objects or controller references — all values are snapshots.
|
|
@@ -33,20 +45,15 @@ export interface ActionOutcomeInput {
|
|
|
33
45
|
currentInterceptSource: string | null;
|
|
34
46
|
}
|
|
35
47
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* Returns segment IDs that need re-fetching when concurrent actions
|
|
40
|
-
* have each revalidated different parts of the tree, or null if
|
|
41
|
-
* consolidation is not needed.
|
|
42
|
-
*/
|
|
48
|
+
// Segment IDs to re-fetch when concurrent actions each revalidated different
|
|
49
|
+
// parts of the tree; null when consolidation does not apply. Returns null while
|
|
50
|
+
// any action is still fetching — consolidation must wait for all to land.
|
|
43
51
|
function computeConsolidationSegments(
|
|
44
52
|
input: ActionOutcomeInput,
|
|
45
53
|
): string[] | null {
|
|
46
54
|
if (!input.hadAnyConcurrentActions) return null;
|
|
47
55
|
if (input.revalidatedSegments.size === 0) return null;
|
|
48
56
|
|
|
49
|
-
// Can't consolidate while any action is still waiting for a server response
|
|
50
57
|
const stillFetchingCount = [...input.inflightActions.values()].filter(
|
|
51
58
|
(a) => a.phase === "fetching",
|
|
52
59
|
).length;
|
|
@@ -55,9 +62,6 @@ function computeConsolidationSegments(
|
|
|
55
62
|
return Array.from(input.revalidatedSegments);
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
/**
|
|
59
|
-
* Count other actions still in "fetching" phase (excluding this handle).
|
|
60
|
-
*/
|
|
61
65
|
function countOtherFetchingActions(input: ActionOutcomeInput): number {
|
|
62
66
|
let count = 0;
|
|
63
67
|
for (const [, a] of input.inflightActions) {
|
|
@@ -69,29 +73,42 @@ function countOtherFetchingActions(input: ActionOutcomeInput): number {
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
/**
|
|
72
|
-
* Classify a post-reconciliation action outcome
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* then delegates to the pure classifyActionResponse function.
|
|
77
|
-
*
|
|
78
|
-
* The server-action-bridge calls this after reconciliation to decide
|
|
79
|
-
* whether to render, skip, consolidate, or refetch.
|
|
76
|
+
* Classify a post-reconciliation action outcome. Ordered priority chain: each
|
|
77
|
+
* case assumes the earlier ones are false (e.g. concurrent-skip only applies on
|
|
78
|
+
* the still-current route, consolidation only once no action is still fetching).
|
|
79
|
+
* The bridge calls this to decide whether to render, skip, consolidate, or refetch.
|
|
80
80
|
*/
|
|
81
81
|
export function classifyActionOutcome(
|
|
82
82
|
input: ActionOutcomeInput,
|
|
83
83
|
): ActionScenario {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
84
|
+
if (
|
|
85
|
+
input.currentPathname !== input.actionStartPathname ||
|
|
86
|
+
input.currentLocationKey !== input.actionStartLocationKey
|
|
87
|
+
) {
|
|
88
|
+
return {
|
|
89
|
+
type: "navigated-away",
|
|
90
|
+
historyKeyChanged:
|
|
91
|
+
input.currentLocationKey !== input.actionStartLocationKey,
|
|
92
|
+
onInterceptRoute: input.currentInterceptSource !== null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (input.reconciledSegmentCount < input.matchedCount) {
|
|
97
|
+
return { type: "hmr-missing" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const consolidationSegments = computeConsolidationSegments(input);
|
|
101
|
+
if (consolidationSegments && consolidationSegments.length > 0) {
|
|
102
|
+
return { type: "consolidation-needed", segmentIds: consolidationSegments };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const otherFetchingActionCount = countOtherFetchingActions(input);
|
|
106
|
+
if (otherFetchingActionCount > 0) {
|
|
107
|
+
return {
|
|
108
|
+
type: "concurrent-skip",
|
|
109
|
+
otherFetchingCount: otherFetchingActionCount,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
96
112
|
|
|
97
|
-
|
|
113
|
+
return { type: "normal" };
|
|
114
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action fence: a refcounted flag that is raised while a server action is in
|
|
3
|
+
* flight and lowered when it resolves.
|
|
4
|
+
*
|
|
5
|
+
* It replaces the eager cache clear the action bridge used to do at action
|
|
6
|
+
* start. While the fence is up, the decision of whether the action invalidated
|
|
7
|
+
* anything is deferred to the response:
|
|
8
|
+
*
|
|
9
|
+
* - prefetch consumption is suspended (a queued prefetch result is not served),
|
|
10
|
+
* - a genuine navigation during the flight fetches with `cache: "no-store"` so
|
|
11
|
+
* it cannot be served stale bytes from the browser's Vary-keyed HTTP cache,
|
|
12
|
+
* - popstate reads are treated as stale-while-revalidate.
|
|
13
|
+
*
|
|
14
|
+
* Nothing is wiped, rotated, or broadcast while the fence is up — that is what
|
|
15
|
+
* keeps a sibling tab from seeing a pre-commit signal. Refcounted so concurrent
|
|
16
|
+
* actions compose: each action raises and lowers its own reference, and the
|
|
17
|
+
* fence is down only when the count reaches zero.
|
|
18
|
+
*
|
|
19
|
+
* The refcount is a single module-level counter, not keyed by routerId.
|
|
20
|
+
* Consumers (navigation-client.ts, prefetch/fetch.ts, navigation-bridge.ts)
|
|
21
|
+
* read it unscoped, so an action in one router would suppress another router's
|
|
22
|
+
* navigation/prefetch caches. This is correct only because two routers cannot
|
|
23
|
+
* coexist in one live document: an SPA navigation crossing a host-router
|
|
24
|
+
* boundary forces a full document reload (src/router/request-classification.ts
|
|
25
|
+
* app-switch terminal), so there is always exactly one live router per
|
|
26
|
+
* document. A future multi-router-in-one-document feature must not silently
|
|
27
|
+
* inherit this global, cross-router cache suppression.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
let fenceCount = 0;
|
|
31
|
+
|
|
32
|
+
export function enterActionFence(): void {
|
|
33
|
+
fenceCount++;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function exitActionFence(): void {
|
|
37
|
+
if (fenceCount > 0) fenceCount--;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isActionFenceActive(): boolean {
|
|
41
|
+
return fenceCount > 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Test-only: reset the refcount between cases. */
|
|
45
|
+
export function __resetActionFence(): void {
|
|
46
|
+
fenceCount = 0;
|
|
47
|
+
}
|
package/src/browser/app-shell.ts
CHANGED
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* App-shell metadata: the
|
|
5
|
-
*
|
|
6
|
-
* from the initial RSC payload and
|
|
7
|
-
*
|
|
4
|
+
* App-shell metadata: the per-router fields describing the "envelope" around
|
|
5
|
+
* the current app's segment tree — the rootLayout (Document), basename,
|
|
6
|
+
* version, and router identity. Set once from the initial RSC payload and read
|
|
7
|
+
* through the AppShellRef when rendering segments.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* fallback), so carrying it here would be unreliable.
|
|
16
|
-
* - prefetchCacheTTL: the not-found full-render payload does not serialize
|
|
17
|
-
* it, so a cross-app nav into a 404 would silently erase the setting.
|
|
18
|
-
* Mutable shell fields must be serialized on EVERY full-render path,
|
|
19
|
-
* otherwise absent fields are indistinguishable from "new app has no
|
|
20
|
-
* value" and the old app's value is dropped.
|
|
21
|
-
*
|
|
22
|
-
* A new document navigation (hard reload) applies these fields from the
|
|
23
|
-
* target app's initial payload.
|
|
9
|
+
* This is a per-document value. A navigation that crosses a host-router app
|
|
10
|
+
* boundary is a full document load (the server returns X-RSC-Reload for it;
|
|
11
|
+
* see request-classification.ts, mode "app-switch"), so the target app's shell
|
|
12
|
+
* — along with everything else document-level (theme, warmup, prefetch-TTL) —
|
|
13
|
+
* is established fresh by the target app's own load. The shell is never swapped
|
|
14
|
+
* in place within a session.
|
|
24
15
|
*/
|
|
25
16
|
export interface AppShell {
|
|
26
17
|
/** Router identity. Used to namespace per-app client state (e.g. the
|
|
@@ -33,20 +24,16 @@ export interface AppShell {
|
|
|
33
24
|
}
|
|
34
25
|
|
|
35
26
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
27
|
+
* Container for the active app shell. Read-through via `get()` so closures
|
|
28
|
+
* (e.g. the segment renderer) capture the ref and read the shell at call time
|
|
29
|
+
* rather than closing over a stale snapshot.
|
|
38
30
|
*/
|
|
39
31
|
export interface AppShellRef {
|
|
40
32
|
get(): AppShell;
|
|
41
|
-
update(next: AppShell): void;
|
|
42
33
|
}
|
|
43
34
|
|
|
44
35
|
export function createAppShellRef(initial: AppShell): AppShellRef {
|
|
45
|
-
let current = initial;
|
|
46
36
|
return {
|
|
47
|
-
get: () =>
|
|
48
|
-
update: (next) => {
|
|
49
|
-
current = next;
|
|
50
|
-
},
|
|
37
|
+
get: () => initial,
|
|
51
38
|
};
|
|
52
39
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rango state cookie: value codec and attribute serialization.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the client (the document.cookie writer in rango-state.ts) and the
|
|
5
|
+
* server (the Set-Cookie writer in invalidateClientCache). Environment-agnostic:
|
|
6
|
+
* no window/document access and no name composition.
|
|
7
|
+
*
|
|
8
|
+
* Name RESOLUTION deliberately lives elsewhere (router init, see
|
|
9
|
+
* router/state-cookie-name.ts). Keeping composition out of this shared module
|
|
10
|
+
* is what lets the client read the server-resolved name verbatim and compose
|
|
11
|
+
* nothing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Default prefix when `stateCookiePrefix` is unset or empty after sanitization. */
|
|
15
|
+
export const DEFAULT_STATE_COOKIE_PREFIX = "rango-state";
|
|
16
|
+
|
|
17
|
+
/** Internal response header carrying the keepClientCache() directive. */
|
|
18
|
+
export const KEEP_CACHE_HEADER = "x-rango-keep-cache";
|
|
19
|
+
|
|
20
|
+
// Per-client signal headers (lower-case) that a SHARED response cache must never
|
|
21
|
+
// store or replay (Finding #3): a `Set-Cookie` (rango state rotation, or any
|
|
22
|
+
// cookie a loader set) and the keepClientCache() directive. Strip-all-Set-Cookie
|
|
23
|
+
// is deliberate — a shared store can't know the resolved cookie name, and any
|
|
24
|
+
// per-client cookie in a cacheable document is the hazard. Single source for the
|
|
25
|
+
// predicate, presence check, and strip below.
|
|
26
|
+
const PER_CLIENT_SIGNAL_HEADERS: readonly string[] = [
|
|
27
|
+
"set-cookie",
|
|
28
|
+
KEEP_CACHE_HEADER,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** True for a per-client signal header. */
|
|
32
|
+
export function isPerClientSignalHeader(name: string): boolean {
|
|
33
|
+
return PER_CLIENT_SIGNAL_HEADERS.includes(name.toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True if `headers` carries any per-client signal. */
|
|
37
|
+
export function hasPerClientSignal(headers: Headers): boolean {
|
|
38
|
+
return PER_CLIENT_SIGNAL_HEADERS.some((name) => headers.has(name));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Remove every per-client signal header from `headers` in place. */
|
|
42
|
+
export function stripPerClientSignals(headers: Headers): void {
|
|
43
|
+
for (const name of PER_CLIENT_SIGNAL_HEADERS) headers.delete(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read the raw, UNDECODED value of a single cookie from a cookie string (a
|
|
48
|
+
* `document.cookie` jar or a request `Cookie` header). Shared by both seats so
|
|
49
|
+
* the client mirror and the server monotonic-guard read the SAME jar entry —
|
|
50
|
+
* their agreement is the guard's contract. First match wins (the entry the
|
|
51
|
+
* client's mirror holds); exact name boundary via the `=`; an empty value
|
|
52
|
+
* (`name=`) returns null (treated as absent, not a usable prior value).
|
|
53
|
+
*/
|
|
54
|
+
export function getRawCookieValue(
|
|
55
|
+
cookieString: string | null,
|
|
56
|
+
name: string,
|
|
57
|
+
): string | null {
|
|
58
|
+
if (!cookieString) return null;
|
|
59
|
+
const target = name + "=";
|
|
60
|
+
for (const part of cookieString.split(";")) {
|
|
61
|
+
const trimmed = part.trim();
|
|
62
|
+
if (trimmed.startsWith(target)) {
|
|
63
|
+
const value = trimmed.slice(target.length);
|
|
64
|
+
return value === "" ? null : value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Encode a state value for the wire: `encodeURIComponent(version):timestamp`.
|
|
72
|
+
* Only the build-derived version is encoded (it is arbitrary); the `:`
|
|
73
|
+
* separator and numeric timestamp stay raw, so the `{version}:{timestamp}`
|
|
74
|
+
* shape survives and `:` is a legal cookie-value octet.
|
|
75
|
+
*/
|
|
76
|
+
export function encodeStateValue(version: string, timestamp: number): string {
|
|
77
|
+
return `${encodeURIComponent(version)}:${timestamp}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Parsed state value. `version` is decoded; `timestamp` is the raw integer. */
|
|
81
|
+
export interface StateValue {
|
|
82
|
+
version: string;
|
|
83
|
+
timestamp: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Decode a wire value back into `{version, timestamp}`. Returns null for a
|
|
88
|
+
* malformed value (no `:`, empty version, or non-numeric timestamp) so callers
|
|
89
|
+
* mint fresh instead of trusting garbage.
|
|
90
|
+
*/
|
|
91
|
+
export function decodeStateValue(raw: string): StateValue | null {
|
|
92
|
+
const colon = raw.indexOf(":");
|
|
93
|
+
if (colon <= 0) return null;
|
|
94
|
+
const timestamp = Number(raw.slice(colon + 1));
|
|
95
|
+
if (!Number.isFinite(timestamp)) return null;
|
|
96
|
+
let version: string;
|
|
97
|
+
try {
|
|
98
|
+
version = decodeURIComponent(raw.slice(0, colon));
|
|
99
|
+
} catch {
|
|
100
|
+
// A malformed percent-escape (e.g. "%:1") must mint fresh, not throw —
|
|
101
|
+
// a thrown URIError here would 500 the server seat or fail client boot.
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return { version, timestamp };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Mint a fresh state value whose timestamp is strictly greater than the previous
|
|
109
|
+
* one, so a re-mint inside the same millisecond (or a backward clock step) still
|
|
110
|
+
* differs from the current value. `prevRaw` is the current wire value (the
|
|
111
|
+
* client's in-memory mirror, or the server's inbound header/cookie) or null; its
|
|
112
|
+
* timestamp is the floor. Shared by both seats so the monotonic guard lives once.
|
|
113
|
+
*/
|
|
114
|
+
export function mintStateValue(
|
|
115
|
+
version: string,
|
|
116
|
+
prevRaw: string | null,
|
|
117
|
+
): string {
|
|
118
|
+
const prevTs = prevRaw ? (decodeStateValue(prevRaw)?.timestamp ?? 0) : 0;
|
|
119
|
+
const ts = Math.max(Date.now(), prevTs + 1);
|
|
120
|
+
return encodeStateValue(version, ts);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Attribute string for the state cookie. Session cookie (no Max-Age/Expires),
|
|
125
|
+
* Path=/ (whole app), SameSite=Lax (sent on top-level navigations), and Secure
|
|
126
|
+
* only on https so the document.cookie write does not silently fail on plain
|
|
127
|
+
* http dev. Never HttpOnly (the client reads and writes it).
|
|
128
|
+
*/
|
|
129
|
+
export function stateCookieAttributes(secure: boolean): string {
|
|
130
|
+
return `; Path=/; SameSite=Lax${secure ? "; Secure" : ""}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Serialize a full `name=value` cookie string with the state attributes. */
|
|
134
|
+
export function serializeStateCookie(
|
|
135
|
+
name: string,
|
|
136
|
+
value: string,
|
|
137
|
+
secure: boolean,
|
|
138
|
+
): string {
|
|
139
|
+
return `${name}=${value}${stateCookieAttributes(secure)}`;
|
|
140
|
+
}
|
|
@@ -237,10 +237,6 @@ export interface EventController {
|
|
|
237
237
|
hadAnyConcurrentActions(): boolean;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
// ============================================================================
|
|
241
|
-
// Default States
|
|
242
|
-
// ============================================================================
|
|
243
|
-
|
|
244
240
|
const DEFAULT_ACTION_STATE: TrackedActionState = {
|
|
245
241
|
state: "idle",
|
|
246
242
|
actionId: null,
|
|
@@ -261,20 +257,23 @@ function matchesActionId(
|
|
|
261
257
|
entryActionId: string,
|
|
262
258
|
): boolean {
|
|
263
259
|
if (subscriptionId.includes("#")) {
|
|
264
|
-
// Full ID: exact match
|
|
265
260
|
return subscriptionId === entryActionId;
|
|
266
261
|
}
|
|
267
|
-
// Action name only: suffix match (matches "anything#actionName")
|
|
268
262
|
return entryActionId.endsWith(`#${subscriptionId}`);
|
|
269
263
|
}
|
|
270
264
|
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
265
|
+
// Batch rapid notifications into one microtask to prevent render storms
|
|
266
|
+
function makeDebouncedNotifier(listeners: Set<() => void>): () => void {
|
|
267
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
268
|
+
return () => {
|
|
269
|
+
if (timeout !== null) clearTimeout(timeout);
|
|
270
|
+
timeout = setTimeout(() => {
|
|
271
|
+
timeout = null;
|
|
272
|
+
listeners.forEach((listener) => listener());
|
|
273
|
+
}, 0);
|
|
274
|
+
};
|
|
275
|
+
}
|
|
274
276
|
|
|
275
|
-
/**
|
|
276
|
-
* Configuration for creating an event controller
|
|
277
|
-
*/
|
|
278
277
|
export interface EventControllerConfig {
|
|
279
278
|
initialLocation?: NavigationLocation;
|
|
280
279
|
}
|
|
@@ -292,62 +291,34 @@ export interface EventControllerConfig {
|
|
|
292
291
|
export function createEventController(
|
|
293
292
|
config?: EventControllerConfig,
|
|
294
293
|
): EventController {
|
|
295
|
-
// ========================================================================
|
|
296
|
-
// Source of Truth
|
|
297
|
-
// ========================================================================
|
|
298
|
-
|
|
299
|
-
// Current navigation in progress (null = idle)
|
|
300
294
|
let currentNavigation: NavigationEntry | null = null;
|
|
301
295
|
|
|
302
|
-
// All in-flight actions (keyed by unique instance ID)
|
|
303
296
|
const inflightActions = new Map<string, ActionEntry>();
|
|
304
297
|
|
|
305
|
-
// Committed location (updated when navigation completes)
|
|
306
298
|
let location: NavigationLocation =
|
|
307
299
|
config?.initialLocation ??
|
|
308
300
|
(typeof window !== "undefined"
|
|
309
301
|
? new URL(window.location.href)
|
|
310
302
|
: new URL("/", "http://localhost"));
|
|
311
303
|
|
|
312
|
-
// Track if any concurrent actions occurred (for consolidation)
|
|
313
304
|
let hadAnyConcurrentActions = false;
|
|
314
305
|
|
|
315
|
-
// Track segments revalidated by concurrent actions
|
|
316
306
|
const concurrentRevalidatedSegments = new Set<string>();
|
|
317
307
|
|
|
318
|
-
// Active streaming count (independent of navigation/action lifecycle)
|
|
319
308
|
let activeStreamCount = 0;
|
|
320
309
|
|
|
321
|
-
// Handle data from RSC payload
|
|
322
310
|
let handleData: HandleData = {};
|
|
323
311
|
let handleSegmentOrder: string[] = [];
|
|
324
312
|
let routeSegmentIds: string[] = [];
|
|
325
313
|
|
|
326
|
-
// Merged route params from current match
|
|
327
314
|
let routeParams: Record<string, string> = {};
|
|
328
315
|
|
|
329
|
-
// ========================================================================
|
|
330
|
-
// Listeners
|
|
331
|
-
// ========================================================================
|
|
332
|
-
|
|
333
316
|
const stateListeners = new Set<StateListener>();
|
|
334
317
|
const actionListeners = new Map<string, Set<ActionStateListener>>();
|
|
335
318
|
const handleListeners = new Set<HandleListener>();
|
|
336
319
|
|
|
337
|
-
|
|
338
|
-
let notifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
339
|
-
|
|
340
|
-
function notify() {
|
|
341
|
-
if (notifyTimeout !== null) {
|
|
342
|
-
clearTimeout(notifyTimeout);
|
|
343
|
-
}
|
|
344
|
-
notifyTimeout = setTimeout(() => {
|
|
345
|
-
notifyTimeout = null;
|
|
346
|
-
stateListeners.forEach((listener) => listener());
|
|
347
|
-
}, 0);
|
|
348
|
-
}
|
|
320
|
+
const notify = makeDebouncedNotifier(stateListeners);
|
|
349
321
|
|
|
350
|
-
// Debounce per-action notifications
|
|
351
322
|
const actionNotifyTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
352
323
|
|
|
353
324
|
function notifyAction(actionId: string) {
|
|
@@ -359,8 +330,6 @@ export function createEventController(
|
|
|
359
330
|
actionId,
|
|
360
331
|
setTimeout(() => {
|
|
361
332
|
actionNotifyTimeouts.delete(actionId);
|
|
362
|
-
// Notify all listeners whose subscription ID matches this action
|
|
363
|
-
// This includes exact matches and suffix matches (e.g., "addToCart" matches "hash#addToCart")
|
|
364
333
|
for (const [subscriptionId, listeners] of actionListeners) {
|
|
365
334
|
if (matchesActionId(subscriptionId, actionId)) {
|
|
366
335
|
const state = getActionState(subscriptionId);
|
|
@@ -371,25 +340,9 @@ export function createEventController(
|
|
|
371
340
|
);
|
|
372
341
|
}
|
|
373
342
|
|
|
374
|
-
|
|
375
|
-
let handleNotifyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
376
|
-
|
|
377
|
-
function notifyHandles() {
|
|
378
|
-
if (handleNotifyTimeout !== null) {
|
|
379
|
-
clearTimeout(handleNotifyTimeout);
|
|
380
|
-
}
|
|
381
|
-
handleNotifyTimeout = setTimeout(() => {
|
|
382
|
-
handleNotifyTimeout = null;
|
|
383
|
-
handleListeners.forEach((listener) => listener());
|
|
384
|
-
}, 0);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// ========================================================================
|
|
388
|
-
// Derived State
|
|
389
|
-
// ========================================================================
|
|
343
|
+
const notifyHandles = makeDebouncedNotifier(handleListeners);
|
|
390
344
|
|
|
391
345
|
function getState(): DerivedNavigationState {
|
|
392
|
-
// Build inflight actions list (for compatibility with existing API)
|
|
393
346
|
const inflightActionsList: InflightAction[] = [...inflightActions.values()]
|
|
394
347
|
.filter((a) => a.phase !== "settling")
|
|
395
348
|
.map((a) => ({
|
|
@@ -399,15 +352,12 @@ export function createEventController(
|
|
|
399
352
|
startedAt: a.startedAt,
|
|
400
353
|
}));
|
|
401
354
|
|
|
402
|
-
// State: loading if navigation OR actions are in progress
|
|
403
|
-
// Background revalidations (skipLoadingState) don't affect visible state
|
|
404
355
|
const hasActiveActions = inflightActionsList.length > 0;
|
|
405
356
|
const isVisibleNavigation =
|
|
406
357
|
currentNavigation !== null &&
|
|
407
358
|
!currentNavigation.options?.skipLoadingState;
|
|
408
359
|
const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
|
|
409
360
|
|
|
410
|
-
// Streaming: true if any active streams (navigation or action) or loading
|
|
411
361
|
const isStreaming = activeStreamCount > 0 || state === "loading";
|
|
412
362
|
|
|
413
363
|
return {
|
|
@@ -429,28 +379,20 @@ export function createEventController(
|
|
|
429
379
|
}
|
|
430
380
|
|
|
431
381
|
function getActionState(actionId: string): TrackedActionState {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const settlingEntry = [...inflightActions.values()]
|
|
442
|
-
.filter(
|
|
443
|
-
(a) => matchesActionId(actionId, a.actionId) && a.phase === "settling",
|
|
444
|
-
)
|
|
445
|
-
.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
446
|
-
|
|
447
|
-
const entry = activeEntry || settlingEntry;
|
|
382
|
+
const entry = [...inflightActions.values()]
|
|
383
|
+
.filter((a) => matchesActionId(actionId, a.actionId))
|
|
384
|
+
.reduce<ActionEntry | undefined>((best, a) => {
|
|
385
|
+
if (!best) return a;
|
|
386
|
+
const aActive = a.phase !== "settling";
|
|
387
|
+
const bActive = best.phase !== "settling";
|
|
388
|
+
if (aActive !== bActive) return aActive ? a : best;
|
|
389
|
+
return a.startedAt > best.startedAt ? a : best;
|
|
390
|
+
}, undefined);
|
|
448
391
|
|
|
449
392
|
if (!entry) {
|
|
450
393
|
return { ...DEFAULT_ACTION_STATE };
|
|
451
394
|
}
|
|
452
395
|
|
|
453
|
-
// Derive state from phase
|
|
454
396
|
let state: ActionLifecycleState;
|
|
455
397
|
switch (entry.phase) {
|
|
456
398
|
case "fetching":
|
|
@@ -632,6 +574,19 @@ export function createEventController(
|
|
|
632
574
|
doSettle();
|
|
633
575
|
}
|
|
634
576
|
|
|
577
|
+
// streamingEnded is forced here for the "streaming never started" case so
|
|
578
|
+
// tryFinalize can run; otherwise the streaming token's end() finalizes.
|
|
579
|
+
function settleWith(result: NonNullable<typeof pendingResult>) {
|
|
580
|
+
if (!inflightActions.has(id) || settled) return;
|
|
581
|
+
actionCompleted = true;
|
|
582
|
+
entry.completed = true;
|
|
583
|
+
pendingResult = result;
|
|
584
|
+
if (entry.phase === "fetching" || streamingEnded) {
|
|
585
|
+
streamingEnded = true;
|
|
586
|
+
tryFinalize();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
635
590
|
return {
|
|
636
591
|
id,
|
|
637
592
|
abort,
|
|
@@ -668,35 +623,11 @@ export function createEventController(
|
|
|
668
623
|
},
|
|
669
624
|
|
|
670
625
|
complete(result?: unknown) {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
actionCompleted = true;
|
|
674
|
-
entry.completed = true;
|
|
675
|
-
pendingResult = { type: "success", value: result };
|
|
676
|
-
|
|
677
|
-
// If streaming never started or already ended, finalize immediately
|
|
678
|
-
// Otherwise wait for streaming to end
|
|
679
|
-
if (entry.phase === "fetching" || streamingEnded) {
|
|
680
|
-
streamingEnded = true; // Mark as ended if never started
|
|
681
|
-
tryFinalize();
|
|
682
|
-
}
|
|
683
|
-
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
626
|
+
settleWith({ type: "success", value: result });
|
|
684
627
|
},
|
|
685
628
|
|
|
686
629
|
fail(error: unknown) {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
actionCompleted = true;
|
|
690
|
-
entry.completed = true;
|
|
691
|
-
pendingResult = { type: "error", value: error };
|
|
692
|
-
|
|
693
|
-
// If streaming never started or already ended, finalize immediately
|
|
694
|
-
// Otherwise wait for streaming to end
|
|
695
|
-
if (entry.phase === "fetching" || streamingEnded) {
|
|
696
|
-
streamingEnded = true; // Mark as ended if never started
|
|
697
|
-
tryFinalize();
|
|
698
|
-
}
|
|
699
|
-
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
630
|
+
settleWith({ type: "error", value: error });
|
|
700
631
|
},
|
|
701
632
|
|
|
702
633
|
getRevalidatedSegments(): Set<string> {
|
|
@@ -905,40 +836,3 @@ export function createEventController(
|
|
|
905
836
|
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
906
837
|
};
|
|
907
838
|
}
|
|
908
|
-
|
|
909
|
-
// ============================================================================
|
|
910
|
-
// Singleton
|
|
911
|
-
// ============================================================================
|
|
912
|
-
|
|
913
|
-
let controllerInstance: EventController | null = null;
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* Initialize the global event controller
|
|
917
|
-
*/
|
|
918
|
-
export function initEventController(
|
|
919
|
-
config?: EventControllerConfig,
|
|
920
|
-
): EventController {
|
|
921
|
-
if (!controllerInstance) {
|
|
922
|
-
controllerInstance = createEventController(config);
|
|
923
|
-
}
|
|
924
|
-
return controllerInstance;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
/**
|
|
928
|
-
* Get the global event controller
|
|
929
|
-
*/
|
|
930
|
-
export function getEventController(): EventController {
|
|
931
|
-
if (!controllerInstance) {
|
|
932
|
-
throw new Error(
|
|
933
|
-
"Event controller not initialized. Call initEventController first.",
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
return controllerInstance;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
/**
|
|
940
|
-
* Reset the controller instance (for testing)
|
|
941
|
-
*/
|
|
942
|
-
export function resetEventController(): void {
|
|
943
|
-
controllerInstance = null;
|
|
944
|
-
}
|