@rangojs/router 0.0.0-experimental.110 → 0.0.0-experimental.112
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/dist/bin/rango.js +41 -37
- package/dist/vite/index.js +144 -191
- package/package.json +17 -14
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/rango/SKILL.md +20 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/event-controller.ts +42 -66
- package/src/browser/navigation-bridge.ts +4 -0
- package/src/browser/navigation-client.ts +12 -15
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +7 -21
- package/src/browser/partial-update.ts +8 -16
- package/src/browser/react/NavigationProvider.tsx +29 -40
- package/src/browser/react/use-params.ts +3 -4
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +16 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +2 -0
- package/src/build/generate-manifest.ts +29 -31
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-types/router-processing.ts +37 -9
- package/src/build/runtime-discovery.ts +9 -20
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +29 -0
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +1 -0
- package/src/response-utils.ts +9 -0
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +231 -259
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/match-result.ts +32 -30
- package/src/router/middleware.ts +46 -78
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/rsc/handler.ts +20 -65
- package/src/rsc/helpers.ts +3 -2
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/response-route-handler.ts +32 -52
- package/src/rsc/rsc-rendering.ts +27 -53
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +13 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/segment-system.tsx +5 -39
- package/src/server/context.ts +76 -35
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +2 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +20 -115
- package/src/urls/urls-function.ts +1 -5
- package/src/vite/discovery/discover-routers.ts +10 -22
- package/src/vite/discovery/route-types-writer.ts +38 -82
- package/src/vite/plugins/cjs-to-esm.ts +3 -7
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-internal-ids.ts +34 -62
- package/src/vite/plugins/version-injector.ts +2 -12
- package/src/vite/router-discovery.ts +71 -26
- package/src/vite/utils/shared-utils.ts +13 -1
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type MagicString from "magic-string";
|
|
2
|
-
import {
|
|
2
|
+
import { makeStubId } from "../expose-id-utils.js";
|
|
3
3
|
import type { CreateExportBinding } from "./types.js";
|
|
4
4
|
import { isExportOnlyFile } from "./export-analysis.js";
|
|
5
5
|
|
|
@@ -33,7 +33,7 @@ export function generateClientLoaderStubs(
|
|
|
33
33
|
|
|
34
34
|
for (const binding of bindings) {
|
|
35
35
|
for (const name of binding.exportNames) {
|
|
36
|
-
const loaderId =
|
|
36
|
+
const loaderId = makeStubId(filePath, name, isBuild);
|
|
37
37
|
lines.push(
|
|
38
38
|
`export const ${name} = { __brand: "loader", $$id: "${loaderId}" };`,
|
|
39
39
|
);
|
|
@@ -54,9 +54,7 @@ export function transformLoaders(
|
|
|
54
54
|
for (const binding of bindings) {
|
|
55
55
|
const exportName = binding.exportNames[0];
|
|
56
56
|
|
|
57
|
-
const loaderId = isBuild
|
|
58
|
-
? hashId(filePath, exportName)
|
|
59
|
-
: `${filePath}#${exportName}`;
|
|
57
|
+
const loaderId = makeStubId(filePath, exportName, isBuild);
|
|
60
58
|
|
|
61
59
|
// Inject $$id as hidden third parameter.
|
|
62
60
|
// createLoader(fn) -> createLoader(fn, undefined, "id")
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
transformHandles,
|
|
40
40
|
transformLocationState,
|
|
41
41
|
generateWholeFileStubs,
|
|
42
|
-
generateExprStubs,
|
|
43
42
|
stubHandlerExprs,
|
|
44
43
|
transformHandlerIds,
|
|
45
44
|
} from "./expose-ids/handler-transform.js";
|
|
@@ -424,17 +423,6 @@ ${lazyImports.join(",\n")}
|
|
|
424
423
|
if (wholeFile) return wholeFile;
|
|
425
424
|
}
|
|
426
425
|
|
|
427
|
-
// --- PrerenderHandler: RSC build module tracking ---
|
|
428
|
-
if (hasPrerenderHandlerCode && isRscEnv && isBuild) {
|
|
429
|
-
const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
|
|
430
|
-
const exportNames = getBindings(code, fnNames).map(
|
|
431
|
-
(b) => b.exportNames[0],
|
|
432
|
-
);
|
|
433
|
-
if (exportNames.length > 0) {
|
|
434
|
-
prerenderHandlerModules.set(id, exportNames);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
426
|
// --- Inline handler extraction to virtual modules ---
|
|
439
427
|
// Runs before stubs/tracking so inline calls become imports, then
|
|
440
428
|
// the existing regex fast path handles both the original file's
|
|
@@ -710,14 +698,27 @@ ${lazyImports.join(",\n")}
|
|
|
710
698
|
}
|
|
711
699
|
}
|
|
712
700
|
|
|
713
|
-
//
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
701
|
+
// RSC build module tracking (prerender + static), consumed via the
|
|
702
|
+
// plugin API for prerender freezing. Export-binding sets are invariant
|
|
703
|
+
// across the inline-extraction loop, so tracking both here is equivalent
|
|
704
|
+
// to the pre-extraction prerender tracking this replaces.
|
|
705
|
+
if (isRscEnv && isBuild) {
|
|
706
|
+
const trackTypes: Array<
|
|
707
|
+
[boolean, HandlerTransformConfig, Map<string, string[]>]
|
|
708
|
+
> = [
|
|
709
|
+
[
|
|
710
|
+
hasPrerenderHandlerCode,
|
|
711
|
+
PRERENDER_CONFIG,
|
|
712
|
+
prerenderHandlerModules,
|
|
713
|
+
],
|
|
714
|
+
[hasStaticHandlerCode, STATIC_CONFIG, staticHandlerModules],
|
|
715
|
+
];
|
|
716
|
+
for (const [has, cfg, trackMap] of trackTypes) {
|
|
717
|
+
if (!has) continue;
|
|
718
|
+
const exportNames = getBindings(code, getFnNames(cfg.fnName)).map(
|
|
719
|
+
(b) => b.exportNames[0],
|
|
720
|
+
);
|
|
721
|
+
if (exportNames.length > 0) trackMap.set(id, exportNames);
|
|
721
722
|
}
|
|
722
723
|
}
|
|
723
724
|
|
|
@@ -758,48 +759,19 @@ ${lazyImports.join(",\n")}
|
|
|
758
759
|
isBuild,
|
|
759
760
|
) || changed;
|
|
760
761
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
) ||
|
|
773
|
-
|
|
774
|
-
// Non-RSC mixed-export file: replace Prerender() calls with stubs
|
|
775
|
-
// on the shared MagicString so sourcemaps stay accurate.
|
|
776
|
-
changed =
|
|
777
|
-
stubHandlerExprs(
|
|
778
|
-
PRERENDER_CONFIG,
|
|
779
|
-
bindings,
|
|
780
|
-
s,
|
|
781
|
-
filePath,
|
|
782
|
-
isBuild,
|
|
783
|
-
) || changed;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
if (hasStaticHandlerCode) {
|
|
787
|
-
const fnNames = getFnNames(STATIC_CONFIG.fnName);
|
|
788
|
-
const bindings = getBindings(code, fnNames);
|
|
789
|
-
if (isRscEnv) {
|
|
790
|
-
changed =
|
|
791
|
-
transformHandlerIds(
|
|
792
|
-
STATIC_CONFIG,
|
|
793
|
-
bindings,
|
|
794
|
-
s,
|
|
795
|
-
filePath,
|
|
796
|
-
isBuild,
|
|
797
|
-
) || changed;
|
|
798
|
-
} else {
|
|
799
|
-
changed =
|
|
800
|
-
stubHandlerExprs(STATIC_CONFIG, bindings, s, filePath, isBuild) ||
|
|
801
|
-
changed;
|
|
802
|
-
}
|
|
762
|
+
// Prerender + Static share the RSC inject-id vs non-RSC stub dispatch.
|
|
763
|
+
// Call sites are disjoint (distinct fnNames), so loop order is irrelevant.
|
|
764
|
+
const finalHandlerConfigs = [
|
|
765
|
+
hasPrerenderHandlerCode && PRERENDER_CONFIG,
|
|
766
|
+
hasStaticHandlerCode && STATIC_CONFIG,
|
|
767
|
+
].filter((c): c is HandlerTransformConfig => !!c);
|
|
768
|
+
for (const cfg of finalHandlerConfigs) {
|
|
769
|
+
const bindings = getBindings(code, getFnNames(cfg.fnName));
|
|
770
|
+
changed =
|
|
771
|
+
(isRscEnv
|
|
772
|
+
? transformHandlerIds(cfg, bindings, s, filePath, isBuild)
|
|
773
|
+
: stubHandlerExprs(cfg, bindings, s, filePath, isBuild)) ||
|
|
774
|
+
changed;
|
|
803
775
|
}
|
|
804
776
|
|
|
805
777
|
if (!changed) return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import * as Vite from "vite";
|
|
4
|
+
import { resolveRscEntryFromConfig } from "../utils/shared-utils.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Plugin that auto-injects VERSION and routes-manifest into custom entry.rsc files.
|
|
@@ -20,18 +21,7 @@ export function createVersionInjectorPlugin(
|
|
|
20
21
|
|
|
21
22
|
configResolved(config) {
|
|
22
23
|
let entryPath = rscEntryPath;
|
|
23
|
-
|
|
24
|
-
// The @cloudflare/vite-plugin reads wrangler config (toml/json/jsonc)
|
|
25
|
-
// and sets optimizeDeps.entries on the RSC environment.
|
|
26
|
-
if (!entryPath) {
|
|
27
|
-
const rscEnvConfig = (config.environments as any)?.["rsc"];
|
|
28
|
-
const entries = rscEnvConfig?.optimizeDeps?.entries;
|
|
29
|
-
if (typeof entries === "string") {
|
|
30
|
-
entryPath = entries;
|
|
31
|
-
} else if (Array.isArray(entries) && entries.length > 0) {
|
|
32
|
-
entryPath = entries[0];
|
|
33
|
-
}
|
|
34
|
-
}
|
|
24
|
+
if (!entryPath) entryPath = resolveRscEntryFromConfig(config);
|
|
35
25
|
if (entryPath) {
|
|
36
26
|
resolvedEntryPath = resolve(config.root, entryPath);
|
|
37
27
|
}
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
import { postprocessBundle } from "./discovery/bundle-postprocess.js";
|
|
53
53
|
import { createDiscoveryGate } from "./discovery/gate-state.js";
|
|
54
54
|
import { resetStagedBuildAssets } from "./utils/prerender-utils.js";
|
|
55
|
+
import { resolveRscEntryFromConfig } from "./utils/shared-utils.js";
|
|
55
56
|
import {
|
|
56
57
|
pickForwardedRunnerConfig,
|
|
57
58
|
selectForwardableResolvePlugins,
|
|
@@ -347,17 +348,10 @@ export function createRouterDiscoveryPlugin(
|
|
|
347
348
|
if (!s.resolvedEntryPath && opts?.routerPathRef?.path) {
|
|
348
349
|
s.resolvedEntryPath = opts.routerPathRef.path;
|
|
349
350
|
}
|
|
350
|
-
// Cloudflare preset:
|
|
351
|
-
// The @cloudflare/vite-plugin reads wrangler config (toml/json/jsonc)
|
|
352
|
-
// and sets optimizeDeps.entries on the RSC environment.
|
|
351
|
+
// Cloudflare preset: entry comes from the resolved RSC env config.
|
|
353
352
|
if (!s.resolvedEntryPath) {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
if (typeof entries === "string") {
|
|
357
|
-
s.resolvedEntryPath = entries;
|
|
358
|
-
} else if (Array.isArray(entries) && entries.length > 0) {
|
|
359
|
-
s.resolvedEntryPath = entries[0];
|
|
360
|
-
}
|
|
353
|
+
const entry = resolveRscEntryFromConfig(config);
|
|
354
|
+
if (entry) s.resolvedEntryPath = entry;
|
|
361
355
|
}
|
|
362
356
|
// Generate combined named-routes.gen.ts from static source parsing.
|
|
363
357
|
// Runs before the dev server starts so the gen file exists immediately for IDE.
|
|
@@ -794,20 +788,15 @@ export function createRouterDiscoveryPlugin(
|
|
|
794
788
|
if (s.mergedRouteTrie && serverMod.setRouteTrie) {
|
|
795
789
|
serverMod.setRouteTrie(s.mergedRouteTrie);
|
|
796
790
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
807
|
-
if (serverMod.setRouterPrecomputedEntries) {
|
|
808
|
-
for (const [routerId, entries] of s.perRouterPrecomputedMap) {
|
|
809
|
-
serverMod.setRouterPrecomputedEntries(routerId, entries);
|
|
810
|
-
}
|
|
791
|
+
const perRouterSetters: Array<[Map<string, any>, string]> = [
|
|
792
|
+
[s.perRouterManifestDataMap, "setRouterManifest"],
|
|
793
|
+
[s.perRouterTrieMap, "setRouterTrie"],
|
|
794
|
+
[s.perRouterPrecomputedMap, "setRouterPrecomputedEntries"],
|
|
795
|
+
];
|
|
796
|
+
for (const [map, fn] of perRouterSetters) {
|
|
797
|
+
const setter = serverMod[fn];
|
|
798
|
+
if (typeof setter !== "function") continue;
|
|
799
|
+
for (const [routerId, value] of map) setter(routerId, value);
|
|
811
800
|
}
|
|
812
801
|
};
|
|
813
802
|
|
|
@@ -1024,6 +1013,16 @@ export function createRouterDiscoveryPlugin(
|
|
|
1024
1013
|
);
|
|
1025
1014
|
s.lastDiscoveryError = null;
|
|
1026
1015
|
}
|
|
1016
|
+
// Cloudflare dev: on a successful cycle drop the workerd runner's
|
|
1017
|
+
// cached worker-entry chain so the next request re-evaluates
|
|
1018
|
+
// createRouter() with the new routes. Fired here in the work path
|
|
1019
|
+
// (not the caller's .then()) so a queued follow-up cycle that
|
|
1020
|
+
// succeeds after an earlier failed cycle still reloads:
|
|
1021
|
+
// runRefreshCycle recurses queued work without awaiting it, so the
|
|
1022
|
+
// original call already resolved on the failed cycle. A failed
|
|
1023
|
+
// cycle throws above and never reaches here, so a broken edit
|
|
1024
|
+
// never reloads the worker onto bad source.
|
|
1025
|
+
if (rscEnv && !rscEnv.runner) forceCloudflareWorkerReload(rscEnv);
|
|
1027
1026
|
} catch (err: any) {
|
|
1028
1027
|
s.lastDiscoveryError = {
|
|
1029
1028
|
message: err?.message ?? String(err),
|
|
@@ -1045,6 +1044,49 @@ export function createRouterDiscoveryPlugin(
|
|
|
1045
1044
|
});
|
|
1046
1045
|
};
|
|
1047
1046
|
|
|
1047
|
+
// Cloudflare dev only. workerd serves every request through the
|
|
1048
|
+
// runner-worker singleton, which re-resolves the worker entry per
|
|
1049
|
+
// request via runner.import("virtual:cloudflare/worker-entry"). The
|
|
1050
|
+
// route table lives in the user's createRouter() instance, captured
|
|
1051
|
+
// when that entry chain (entry -> router -> urls) was last evaluated
|
|
1052
|
+
// and then cached in the runner's evaluatedModules. The route-file
|
|
1053
|
+
// watcher refreshes discovery + types on the Node side, but the worker
|
|
1054
|
+
// keeps serving the cached (stale) router: route-definition modules
|
|
1055
|
+
// have no import.meta.hot boundary, so Vite never sends the worker an
|
|
1056
|
+
// HMR update for them and the entry chain is never evicted.
|
|
1057
|
+
//
|
|
1058
|
+
// Fix: after discovery completes, (1) invalidate the worker env's
|
|
1059
|
+
// Node-side module graph, then (2) send a full-reload to the worker.
|
|
1060
|
+
// Step (2) alone is insufficient: the full-reload handler clears the
|
|
1061
|
+
// runner's evaluatedModules and re-imports entrypoints, but each
|
|
1062
|
+
// re-import fetches the module back through this Node-side graph, which
|
|
1063
|
+
// still holds the pre-edit transform of urls.tsx — so createRouter()
|
|
1064
|
+
// rebuilds the stale route table and the new route 404s/hits the
|
|
1065
|
+
// catch-all. Invalidating the graph forces a fresh transform on
|
|
1066
|
+
// re-fetch (the same mechanism refreshTempRscEnv uses for discovery),
|
|
1067
|
+
// so the re-import re-runs createRouter() with the new routes. This is
|
|
1068
|
+
// the programmatic equivalent of the dev-server "r + enter" restart,
|
|
1069
|
+
// scoped to the worker environment instead of tearing down the server.
|
|
1070
|
+
const forceCloudflareWorkerReload = (rscEnv: any) => {
|
|
1071
|
+
if (!rscEnv?.hot) return;
|
|
1072
|
+
try {
|
|
1073
|
+
const graph = rscEnv.moduleGraph;
|
|
1074
|
+
if (graph?.invalidateAll) {
|
|
1075
|
+
graph.invalidateAll();
|
|
1076
|
+
debugDiscovery?.("hmr: invalidated workerd rsc module graph");
|
|
1077
|
+
}
|
|
1078
|
+
rscEnv.hot.send({ type: "full-reload" });
|
|
1079
|
+
debugDiscovery?.(
|
|
1080
|
+
"hmr: forced workerd rsc env reload (full-reload)",
|
|
1081
|
+
);
|
|
1082
|
+
} catch (err: any) {
|
|
1083
|
+
debugDiscovery?.(
|
|
1084
|
+
"hmr: workerd reload failed: %s",
|
|
1085
|
+
err?.message ?? err,
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1048
1090
|
const scheduleRouteRegeneration = () => {
|
|
1049
1091
|
clearTimeout(routeChangeTimer);
|
|
1050
1092
|
routeChangeTimer = setTimeout(() => {
|
|
@@ -1083,12 +1125,15 @@ export function createRouterDiscoveryPlugin(
|
|
|
1083
1125
|
// routes that the static parser cannot resolve. Resolves the
|
|
1084
1126
|
// discovery gate when complete.
|
|
1085
1127
|
if (s.perRouterManifests.length > 0) {
|
|
1128
|
+
// The cloudflare workerd reload fires inside refreshRuntimeDiscovery
|
|
1129
|
+
// on the successful cycle (see forceCloudflareWorkerReload call
|
|
1130
|
+
// there) so queued follow-up cycles also trigger it.
|
|
1086
1131
|
refreshRuntimeDiscovery().catch((err: any) => {
|
|
1087
1132
|
console.warn(
|
|
1088
1133
|
`[rango] Runtime re-discovery error: ${err.message}`,
|
|
1089
1134
|
);
|
|
1090
|
-
// Even on error, unblock the gate so workerd's reload
|
|
1091
|
-
//
|
|
1135
|
+
// Even on error, unblock the gate so workerd's reload doesn't
|
|
1136
|
+
// hang indefinitely against the previous manifest.
|
|
1092
1137
|
resolveDiscoveryGate();
|
|
1093
1138
|
});
|
|
1094
1139
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Plugin } from "vite";
|
|
1
|
+
import type { Plugin, ResolvedConfig } from "vite";
|
|
2
2
|
import * as Vite from "vite";
|
|
3
3
|
import { getPublishedPackageName } from "./package-resolution.js";
|
|
4
4
|
import { performanceTracksOptimizeDepsPlugin } from "../plugins/performance-tracks.js";
|
|
@@ -10,6 +10,18 @@ import {
|
|
|
10
10
|
VIRTUAL_IDS,
|
|
11
11
|
} from "../plugins/virtual-entries.js";
|
|
12
12
|
|
|
13
|
+
// Cloudflare preset: @cloudflare/vite-plugin sets optimizeDeps.entries (string
|
|
14
|
+
// or array) on the rsc environment. Single source for both the discovery plugin
|
|
15
|
+
// and the version injector so they target the same entry.
|
|
16
|
+
export function resolveRscEntryFromConfig(
|
|
17
|
+
config: ResolvedConfig,
|
|
18
|
+
): string | undefined {
|
|
19
|
+
const entries = (config.environments as any)?.["rsc"]?.optimizeDeps?.entries;
|
|
20
|
+
if (typeof entries === "string") return entries;
|
|
21
|
+
if (Array.isArray(entries) && entries.length > 0) return entries[0];
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
/**
|
|
14
26
|
* Rolldown plugin to provide the version virtual module during dependency
|
|
15
27
|
* optimization. Vite 8 optimizes deps with Rolldown (a Rollup-style plugin
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Discriminated union of post-reconciliation action response scenarios.
|
|
3
|
-
*
|
|
4
|
-
* Error and full-update-unsupported are handled inline in the bridge
|
|
5
|
-
* before reconciliation. This classifier only runs for partial responses
|
|
6
|
-
* that have been successfully reconciled.
|
|
7
|
-
*/
|
|
8
|
-
export 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
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Pure data inputs for classifying a partial action response.
|
|
21
|
-
* All values come from the bridge but no browser APIs or side effects.
|
|
22
|
-
*/
|
|
23
|
-
export interface ClassifierInput {
|
|
24
|
-
/** window.location.pathname captured at action start */
|
|
25
|
-
actionStartPathname: string;
|
|
26
|
-
/** window.location.pathname at classification time */
|
|
27
|
-
currentPathname: string;
|
|
28
|
-
/** window.history.state?.key captured at action start */
|
|
29
|
-
actionStartLocationKey: string | undefined;
|
|
30
|
-
/** window.history.state?.key at classification time */
|
|
31
|
-
currentLocationKey: string | undefined;
|
|
32
|
-
/** Number of segments after reconciliation */
|
|
33
|
-
reconciledSegmentCount: number;
|
|
34
|
-
/** Number of matched segment IDs from server */
|
|
35
|
-
matchedCount: number;
|
|
36
|
-
/** Segment IDs needing consolidation (from concurrent action tracking) */
|
|
37
|
-
consolidationSegments: string[] | null;
|
|
38
|
-
/** Number of other actions still in "fetching" phase */
|
|
39
|
-
otherFetchingActionCount: number;
|
|
40
|
-
/** Current intercept source URL (null when not on intercept route) */
|
|
41
|
-
currentInterceptSource: string | null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Classify a partial action response into one of 5 post-reconciliation
|
|
46
|
-
* scenarios.
|
|
47
|
-
*
|
|
48
|
-
* Called after error and full-update cases are handled inline by the bridge.
|
|
49
|
-
* The classification order matches the priority chain:
|
|
50
|
-
* 1. User navigated away during action
|
|
51
|
-
* 2. HMR missing segments (fewer reconciled than matched)
|
|
52
|
-
* 3. Consolidation needed (concurrent actions finished)
|
|
53
|
-
* 4. Concurrent skip (other actions still fetching)
|
|
54
|
-
* 5. Normal (single action, no issues)
|
|
55
|
-
*
|
|
56
|
-
* This is a pure function with no side effects - the bridge handles
|
|
57
|
-
* all UI updates, store mutations, and network requests based on the
|
|
58
|
-
* returned scenario.
|
|
59
|
-
*/
|
|
60
|
-
export function classifyActionResponse(input: ClassifierInput): ActionScenario {
|
|
61
|
-
// Check if user navigated away during the action
|
|
62
|
-
const userNavigatedAway =
|
|
63
|
-
input.currentPathname !== input.actionStartPathname ||
|
|
64
|
-
input.currentLocationKey !== input.actionStartLocationKey;
|
|
65
|
-
|
|
66
|
-
if (userNavigatedAway) {
|
|
67
|
-
const historyKeyChanged =
|
|
68
|
-
input.currentLocationKey !== input.actionStartLocationKey;
|
|
69
|
-
return {
|
|
70
|
-
type: "navigated-away",
|
|
71
|
-
historyKeyChanged,
|
|
72
|
-
onInterceptRoute: input.currentInterceptSource !== null,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// HMR resilience: segments missing after reconciliation
|
|
77
|
-
if (input.reconciledSegmentCount < input.matchedCount) {
|
|
78
|
-
return { type: "hmr-missing" };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Consolidation needed for concurrent actions
|
|
82
|
-
if (input.consolidationSegments && input.consolidationSegments.length > 0) {
|
|
83
|
-
return {
|
|
84
|
-
type: "consolidation-needed",
|
|
85
|
-
segmentIds: input.consolidationSegments,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Other actions still fetching - skip UI update
|
|
90
|
-
if (input.otherFetchingActionCount > 0) {
|
|
91
|
-
return {
|
|
92
|
-
type: "concurrent-skip",
|
|
93
|
-
otherFetchingCount: input.otherFetchingActionCount,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Normal single-action completion
|
|
98
|
-
return { type: "normal" };
|
|
99
|
-
}
|