vorma 0.0.0-pre.0 → 0.83.0
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/LICENSE +28 -0
- package/README.md +48 -0
- package/internal/framework/_typescript/client/index.ts +64 -0
- package/internal/framework/_typescript/client/src/asset_manager.ts +67 -0
- package/internal/framework/_typescript/client/src/client.ts +1201 -0
- package/internal/framework/_typescript/client/src/client_loaders.ts +249 -0
- package/internal/framework/_typescript/client/src/component_loader.ts +105 -0
- package/internal/framework/_typescript/client/src/error_boundary.ts +7 -0
- package/internal/framework/_typescript/client/src/events.ts +54 -0
- package/internal/framework/_typescript/client/src/global_loading_indicator/global_loading_indicator.ts +125 -0
- package/internal/framework/_typescript/client/src/hard_reload.ts +1 -0
- package/internal/framework/_typescript/client/src/head_elements/head_elements.ts +193 -0
- package/internal/framework/_typescript/client/src/history/history.ts +118 -0
- package/internal/framework/_typescript/client/src/history/npm_history_types.ts +83 -0
- package/internal/framework/_typescript/client/src/hmr/hmr.ts +71 -0
- package/internal/framework/_typescript/client/src/init_client.ts +134 -0
- package/internal/framework/_typescript/client/src/links.ts +218 -0
- package/internal/framework/_typescript/client/src/redirects/redirects.ts +203 -0
- package/internal/framework/_typescript/client/src/rendering.ts +135 -0
- package/internal/framework/_typescript/client/src/resolve_public_href.ts +15 -0
- package/internal/framework/_typescript/client/src/scroll_state_manager.ts +100 -0
- package/internal/framework/_typescript/client/src/static_route_defs/route_def_helpers.ts +22 -0
- package/internal/framework/_typescript/client/src/ui_lib_impl_helpers/link_components.ts +131 -0
- package/internal/framework/_typescript/client/src/ui_lib_impl_helpers/route_components.ts +56 -0
- package/internal/framework/_typescript/client/src/ui_lib_impl_helpers/typed_navigate.ts +58 -0
- package/internal/framework/_typescript/client/src/utils/errors.ts +10 -0
- package/internal/framework/_typescript/client/src/utils/logging.ts +7 -0
- package/internal/framework/_typescript/client/src/vorma_app_helpers/vorma_app_helpers.ts +290 -0
- package/internal/framework/_typescript/client/src/vorma_ctx/vorma_ctx.ts +128 -0
- package/internal/framework/_typescript/client/src/window_focus_revalidation/window_focus_revalidation.ts +32 -0
- package/internal/framework/_typescript/client/tsconfig.json +3 -0
- package/internal/framework/_typescript/create/main.ts +378 -0
- package/internal/framework/_typescript/create/package.json +33 -0
- package/internal/framework/_typescript/create/pnpm-lock.yaml +70 -0
- package/internal/framework/_typescript/create/tsconfig.json +3 -0
- package/internal/framework/_typescript/preact/index.tsx +10 -0
- package/internal/framework/_typescript/preact/src/helpers.ts +113 -0
- package/internal/framework/_typescript/preact/src/link.tsx +107 -0
- package/internal/framework/_typescript/preact/src/preact.tsx +191 -0
- package/internal/framework/_typescript/preact/tsconfig.json +7 -0
- package/internal/framework/_typescript/react/index.tsx +10 -0
- package/internal/framework/_typescript/react/src/helpers.ts +118 -0
- package/internal/framework/_typescript/react/src/link.tsx +115 -0
- package/internal/framework/_typescript/react/src/react.tsx +299 -0
- package/internal/framework/_typescript/react/tsconfig.json +6 -0
- package/internal/framework/_typescript/solid/index.tsx +10 -0
- package/internal/framework/_typescript/solid/src/helpers.ts +114 -0
- package/internal/framework/_typescript/solid/src/link.tsx +104 -0
- package/internal/framework/_typescript/solid/src/solid.tsx +204 -0
- package/internal/framework/_typescript/solid/tsconfig.json +7 -0
- package/internal/framework/_typescript/vite/tsconfig.json +3 -0
- package/internal/framework/_typescript/vite/vite.ts +93 -0
- package/internal/site/frontend/assets/vorma-banner.webp +0 -0
- package/kit/_typescript/converters/converters.ts +152 -0
- package/kit/_typescript/cookies/cookies.ts +18 -0
- package/kit/_typescript/csrf/csrf.ts +10 -0
- package/kit/_typescript/debounce/debounce.ts +17 -0
- package/kit/_typescript/fmt/fmt.ts +3 -0
- package/kit/_typescript/json/deep_equals.ts +54 -0
- package/kit/_typescript/json/json.ts +3 -0
- package/kit/_typescript/json/search_param_serializer.ts +49 -0
- package/kit/_typescript/json/stringify_stable.ts +43 -0
- package/kit/_typescript/listeners/listeners.ts +16 -0
- package/kit/_typescript/matcher/find_best_match.ts +205 -0
- package/kit/_typescript/matcher/find_nested_matches.ts +357 -0
- package/kit/_typescript/matcher/parse_segments.ts +30 -0
- package/kit/_typescript/matcher/register.ts +271 -0
- package/kit/_typescript/theme/theme.ts +177 -0
- package/kit/_typescript/tsconfig.json +3 -0
- package/kit/_typescript/url/url.ts +132 -0
- package/npm_dist/internal/framework/_typescript/client/index.d.ts +17 -0
- package/npm_dist/internal/framework/_typescript/client/index.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/index.js +2489 -0
- package/npm_dist/internal/framework/_typescript/client/index.js.map +7 -0
- package/npm_dist/internal/framework/_typescript/client/src/asset_manager.d.ts +6 -0
- package/npm_dist/internal/framework/_typescript/client/src/asset_manager.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/client.d.ts +119 -0
- package/npm_dist/internal/framework/_typescript/client/src/client.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/client_loaders.d.ts +18 -0
- package/npm_dist/internal/framework/_typescript/client/src/client_loaders.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/component_loader.d.ts +10 -0
- package/npm_dist/internal/framework/_typescript/client/src/component_loader.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/error_boundary.d.ts +3 -0
- package/npm_dist/internal/framework/_typescript/client/src/error_boundary.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/events.d.ts +26 -0
- package/npm_dist/internal/framework/_typescript/client/src/events.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/global_loading_indicator/global_loading_indicator.d.ts +12 -0
- package/npm_dist/internal/framework/_typescript/client/src/global_loading_indicator/global_loading_indicator.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/hard_reload.d.ts +2 -0
- package/npm_dist/internal/framework/_typescript/client/src/hard_reload.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/head_elements/head_elements.d.ts +7 -0
- package/npm_dist/internal/framework/_typescript/client/src/head_elements/head_elements.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/history/history.d.ts +14 -0
- package/npm_dist/internal/framework/_typescript/client/src/history/history.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/history/npm_history_types.d.ts +84 -0
- package/npm_dist/internal/framework/_typescript/client/src/history/npm_history_types.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/hmr/hmr.d.ts +3 -0
- package/npm_dist/internal/framework/_typescript/client/src/hmr/hmr.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/init_client.d.ts +9 -0
- package/npm_dist/internal/framework/_typescript/client/src/init_client.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/links.d.ts +33 -0
- package/npm_dist/internal/framework/_typescript/client/src/links.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/redirects/redirects.d.ts +26 -0
- package/npm_dist/internal/framework/_typescript/client/src/redirects/redirects.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/rendering.d.ts +18 -0
- package/npm_dist/internal/framework/_typescript/client/src/rendering.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/resolve_public_href.d.ts +2 -0
- package/npm_dist/internal/framework/_typescript/client/src/resolve_public_href.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/scroll_state_manager.d.ts +22 -0
- package/npm_dist/internal/framework/_typescript/client/src/scroll_state_manager.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/static_route_defs/route_def_helpers.d.ts +12 -0
- package/npm_dist/internal/framework/_typescript/client/src/static_route_defs/route_def_helpers.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/ui_lib_impl_helpers/link_components.d.ts +28 -0
- package/npm_dist/internal/framework/_typescript/client/src/ui_lib_impl_helpers/link_components.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/ui_lib_impl_helpers/route_components.d.ts +18 -0
- package/npm_dist/internal/framework/_typescript/client/src/ui_lib_impl_helpers/route_components.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/ui_lib_impl_helpers/typed_navigate.d.ts +11 -0
- package/npm_dist/internal/framework/_typescript/client/src/ui_lib_impl_helpers/typed_navigate.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/utils/errors.d.ts +3 -0
- package/npm_dist/internal/framework/_typescript/client/src/utils/errors.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/utils/logging.d.ts +3 -0
- package/npm_dist/internal/framework/_typescript/client/src/utils/logging.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/vorma_app_helpers/vorma_app_helpers.d.ts +119 -0
- package/npm_dist/internal/framework/_typescript/client/src/vorma_app_helpers/vorma_app_helpers.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/vorma_ctx/vorma_ctx.d.ts +88 -0
- package/npm_dist/internal/framework/_typescript/client/src/vorma_ctx/vorma_ctx.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/client/src/window_focus_revalidation/window_focus_revalidation.d.ts +10 -0
- package/npm_dist/internal/framework/_typescript/client/src/window_focus_revalidation/window_focus_revalidation.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/create/main.d.ts +3 -0
- package/npm_dist/internal/framework/_typescript/create/main.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/preact/index.d.ts +4 -0
- package/npm_dist/internal/framework/_typescript/preact/index.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/preact/index.js +283 -0
- package/npm_dist/internal/framework/_typescript/preact/index.js.map +7 -0
- package/npm_dist/internal/framework/_typescript/preact/src/helpers.d.ts +21 -0
- package/npm_dist/internal/framework/_typescript/preact/src/helpers.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/preact/src/link.d.ts +11 -0
- package/npm_dist/internal/framework/_typescript/preact/src/link.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/preact/src/preact.d.ts +21 -0
- package/npm_dist/internal/framework/_typescript/preact/src/preact.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/react/index.d.ts +4 -0
- package/npm_dist/internal/framework/_typescript/react/index.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/react/index.js +370 -0
- package/npm_dist/internal/framework/_typescript/react/index.js.map +7 -0
- package/npm_dist/internal/framework/_typescript/react/src/helpers.d.ts +21 -0
- package/npm_dist/internal/framework/_typescript/react/src/helpers.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/react/src/link.d.ts +11 -0
- package/npm_dist/internal/framework/_typescript/react/src/link.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/react/src/react.d.ts +20 -0
- package/npm_dist/internal/framework/_typescript/react/src/react.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/solid/index.d.ts +4 -0
- package/npm_dist/internal/framework/_typescript/solid/index.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/solid/index.js +314 -0
- package/npm_dist/internal/framework/_typescript/solid/index.js.map +7 -0
- package/npm_dist/internal/framework/_typescript/solid/src/helpers.d.ts +22 -0
- package/npm_dist/internal/framework/_typescript/solid/src/helpers.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/solid/src/link.d.ts +11 -0
- package/npm_dist/internal/framework/_typescript/solid/src/link.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/solid/src/solid.d.ts +22 -0
- package/npm_dist/internal/framework/_typescript/solid/src/solid.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/vite/vite.d.ts +11 -0
- package/npm_dist/internal/framework/_typescript/vite/vite.d.ts.map +1 -0
- package/npm_dist/internal/framework/_typescript/vite/vite.js +82 -0
- package/npm_dist/internal/framework/_typescript/vite/vite.js.map +7 -0
- package/npm_dist/kit/_typescript/chunk-YBAPNBS2.js +202 -0
- package/npm_dist/kit/_typescript/chunk-YBAPNBS2.js.map +7 -0
- package/npm_dist/kit/_typescript/converters/converters.d.ts +26 -0
- package/npm_dist/kit/_typescript/converters/converters.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/converters/converters.js +99 -0
- package/npm_dist/kit/_typescript/converters/converters.js.map +7 -0
- package/npm_dist/kit/_typescript/cookies/cookies.d.ts +13 -0
- package/npm_dist/kit/_typescript/cookies/cookies.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/cookies/cookies.js +13 -0
- package/npm_dist/kit/_typescript/cookies/cookies.js.map +7 -0
- package/npm_dist/kit/_typescript/csrf/csrf.d.ts +5 -0
- package/npm_dist/kit/_typescript/csrf/csrf.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/csrf/csrf.js +11 -0
- package/npm_dist/kit/_typescript/csrf/csrf.js.map +7 -0
- package/npm_dist/kit/_typescript/debounce/debounce.d.ts +4 -0
- package/npm_dist/kit/_typescript/debounce/debounce.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/debounce/debounce.js +16 -0
- package/npm_dist/kit/_typescript/debounce/debounce.js.map +7 -0
- package/npm_dist/kit/_typescript/fmt/fmt.d.ts +2 -0
- package/npm_dist/kit/_typescript/fmt/fmt.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/fmt/fmt.js +8 -0
- package/npm_dist/kit/_typescript/fmt/fmt.js.map +7 -0
- package/npm_dist/kit/_typescript/json/deep_equals.d.ts +7 -0
- package/npm_dist/kit/_typescript/json/deep_equals.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/json/json.d.ts +4 -0
- package/npm_dist/kit/_typescript/json/json.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/json/json.js +110 -0
- package/npm_dist/kit/_typescript/json/json.js.map +7 -0
- package/npm_dist/kit/_typescript/json/search_param_serializer.d.ts +2 -0
- package/npm_dist/kit/_typescript/json/search_param_serializer.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/json/stringify_stable.d.ts +7 -0
- package/npm_dist/kit/_typescript/json/stringify_stable.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/listeners/listeners.d.ts +2 -0
- package/npm_dist/kit/_typescript/listeners/listeners.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/listeners/listeners.js +20 -0
- package/npm_dist/kit/_typescript/listeners/listeners.js.map +7 -0
- package/npm_dist/kit/_typescript/matcher/find_best_match.d.ts +10 -0
- package/npm_dist/kit/_typescript/matcher/find_best_match.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/matcher/find_best_match.js +146 -0
- package/npm_dist/kit/_typescript/matcher/find_best_match.js.map +7 -0
- package/npm_dist/kit/_typescript/matcher/find_nested_matches.d.ts +14 -0
- package/npm_dist/kit/_typescript/matcher/find_nested_matches.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/matcher/find_nested_matches.js +248 -0
- package/npm_dist/kit/_typescript/matcher/find_nested_matches.js.map +7 -0
- package/npm_dist/kit/_typescript/matcher/parse_segments.d.ts +2 -0
- package/npm_dist/kit/_typescript/matcher/parse_segments.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/matcher/register.d.ts +54 -0
- package/npm_dist/kit/_typescript/matcher/register.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/matcher/register.js +21 -0
- package/npm_dist/kit/_typescript/matcher/register.js.map +7 -0
- package/npm_dist/kit/_typescript/theme/theme.d.ts +24 -0
- package/npm_dist/kit/_typescript/theme/theme.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/theme/theme.js +133 -0
- package/npm_dist/kit/_typescript/theme/theme.js.map +7 -0
- package/npm_dist/kit/_typescript/url/url.d.ts +30 -0
- package/npm_dist/kit/_typescript/url/url.d.ts.map +1 -0
- package/npm_dist/kit/_typescript/url/url.js +100 -0
- package/npm_dist/kit/_typescript/url/url.js.map +7 -0
- package/package.json +135 -3
- package/tsconfig.base.json +17 -0
- package/index.js +0 -1
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import { debounce } from "vorma/kit/debounce";
|
|
4
|
+
import { jsonDeepEquals } from "vorma/kit/json";
|
|
5
|
+
import { findNestedMatches, type Match } from "vorma/kit/matcher/find-nested";
|
|
6
|
+
import { getIsGETRequest } from "vorma/kit/url";
|
|
7
|
+
import { AssetManager } from "./asset_manager.ts";
|
|
8
|
+
import {
|
|
9
|
+
completeClientLoaders,
|
|
10
|
+
findPartialMatchesOnClient,
|
|
11
|
+
setClientLoadersState,
|
|
12
|
+
type ClientLoadersResult,
|
|
13
|
+
} from "./client_loaders.ts";
|
|
14
|
+
import {
|
|
15
|
+
dispatchBuildIDEvent,
|
|
16
|
+
dispatchStatusEvent,
|
|
17
|
+
type StatusEventDetail,
|
|
18
|
+
} from "./events.ts";
|
|
19
|
+
import { HistoryManager } from "./history/history.ts";
|
|
20
|
+
import type { historyInstance } from "./history/npm_history_types.ts";
|
|
21
|
+
import {
|
|
22
|
+
effectuateRedirectDataResult,
|
|
23
|
+
getBuildIDFromResponse,
|
|
24
|
+
handleRedirects,
|
|
25
|
+
type RedirectData,
|
|
26
|
+
} from "./redirects/redirects.ts";
|
|
27
|
+
import { __reRenderApp } from "./rendering.ts";
|
|
28
|
+
import {
|
|
29
|
+
__applyScrollState,
|
|
30
|
+
type ScrollState,
|
|
31
|
+
} from "./scroll_state_manager.ts";
|
|
32
|
+
import { isAbortError } from "./utils/errors.ts";
|
|
33
|
+
import { logError } from "./utils/logging.ts";
|
|
34
|
+
import {
|
|
35
|
+
__vormaClientGlobal,
|
|
36
|
+
type ClientLoaderAwaitedServerData,
|
|
37
|
+
type GetRouteDataOutput,
|
|
38
|
+
} from "./vorma_ctx/vorma_ctx.ts";
|
|
39
|
+
|
|
40
|
+
/////////////////////////////////////////////////////////////////////
|
|
41
|
+
// TYPES
|
|
42
|
+
/////////////////////////////////////////////////////////////////////
|
|
43
|
+
|
|
44
|
+
export type VormaNavigationType =
|
|
45
|
+
| "browserHistory"
|
|
46
|
+
| "userNavigation"
|
|
47
|
+
| "revalidation"
|
|
48
|
+
| "redirect"
|
|
49
|
+
| "prefetch"
|
|
50
|
+
| "action";
|
|
51
|
+
|
|
52
|
+
export type NavigateProps = {
|
|
53
|
+
href: string;
|
|
54
|
+
state?: unknown;
|
|
55
|
+
navigationType: VormaNavigationType;
|
|
56
|
+
scrollStateToRestore?: ScrollState;
|
|
57
|
+
replace?: boolean;
|
|
58
|
+
redirectCount?: number;
|
|
59
|
+
scrollToTop?: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type NavigationResult =
|
|
63
|
+
| ({
|
|
64
|
+
response: Response;
|
|
65
|
+
props: NavigateProps;
|
|
66
|
+
} & (
|
|
67
|
+
| {
|
|
68
|
+
json: GetRouteDataOutput;
|
|
69
|
+
cssBundlePromises: Array<Promise<any>>;
|
|
70
|
+
waitFnPromise: Promise<ClientLoadersResult> | undefined;
|
|
71
|
+
}
|
|
72
|
+
| { redirectData: RedirectData }
|
|
73
|
+
))
|
|
74
|
+
| undefined;
|
|
75
|
+
|
|
76
|
+
export type NavigationControl = {
|
|
77
|
+
abortController: AbortController | undefined;
|
|
78
|
+
promise: Promise<NavigationResult>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/////////////////////////////////////////////////////////////////////
|
|
82
|
+
// NAVIGATION STATE MANAGER
|
|
83
|
+
/////////////////////////////////////////////////////////////////////
|
|
84
|
+
|
|
85
|
+
// Navigation phases represent the lifecycle stages
|
|
86
|
+
type NavigationPhase =
|
|
87
|
+
| "fetching" // Fetching route data
|
|
88
|
+
| "waiting" // Waiting for assets/loaders
|
|
89
|
+
| "rendering" // Applying changes to DOM
|
|
90
|
+
| "complete"; // Navigation finished
|
|
91
|
+
|
|
92
|
+
// Navigation intent represents what should happen when complete
|
|
93
|
+
type NavigationIntent =
|
|
94
|
+
| "none" // Prefetch -- don't navigate unless upgraded
|
|
95
|
+
| "navigate" // Normal navigation -- update URL and render
|
|
96
|
+
| "revalidate"; // Revalidation -- only update if still on same page
|
|
97
|
+
|
|
98
|
+
interface NavigationEntry {
|
|
99
|
+
control: NavigationControl;
|
|
100
|
+
type: VormaNavigationType;
|
|
101
|
+
intent: NavigationIntent;
|
|
102
|
+
phase: NavigationPhase;
|
|
103
|
+
startTime: number;
|
|
104
|
+
targetUrl: string; // URL this navigation is targeting
|
|
105
|
+
originUrl: string; // URL when navigation started (for revalidation)
|
|
106
|
+
scrollToTop?: boolean;
|
|
107
|
+
replace?: boolean;
|
|
108
|
+
state?: unknown;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface SubmissionEntry {
|
|
112
|
+
control: NavigationControl;
|
|
113
|
+
startTime: number;
|
|
114
|
+
skipGlobalLoadingIndicator?: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
class NavigationStateManager {
|
|
118
|
+
private _navigations = new Map<string, NavigationEntry>();
|
|
119
|
+
private _submissions = new Map<string | symbol, SubmissionEntry>();
|
|
120
|
+
private lastDispatchedStatus: StatusEventDetail | null = null;
|
|
121
|
+
private dispatchStatusEventDebounced: () => void;
|
|
122
|
+
private readonly REVALIDATION_COALESCE_MS = 8;
|
|
123
|
+
|
|
124
|
+
constructor() {
|
|
125
|
+
this.dispatchStatusEventDebounced = debounce(() => {
|
|
126
|
+
this.dispatchStatusEvent();
|
|
127
|
+
}, 8);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async navigate(props: NavigateProps): Promise<{ didNavigate: boolean }> {
|
|
131
|
+
const control = this.beginNavigation(props);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const result = await control.promise;
|
|
135
|
+
if (!result) {
|
|
136
|
+
return { didNavigate: false };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Process based on navigation entry state
|
|
140
|
+
const targetUrl = new URL(props.href, window.location.href).href;
|
|
141
|
+
const entry = this._navigations.get(targetUrl);
|
|
142
|
+
if (!entry) {
|
|
143
|
+
return { didNavigate: false };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (entry.intent === "navigate" || entry.intent === "revalidate") {
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
lastTriggeredNavOrRevalidateTimestampMS = now;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Always call processNavigationResult so the module map and other caches are populated.
|
|
152
|
+
await this.processNavigationResult(result, entry);
|
|
153
|
+
|
|
154
|
+
// After processing, if it was just a prefetch, then we can return
|
|
155
|
+
// and signal that no UI navigation occurred.
|
|
156
|
+
if (entry.intent === "none" && entry.type === "prefetch") {
|
|
157
|
+
return { didNavigate: false };
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const targetUrl = new URL(props.href, window.location.href).href;
|
|
161
|
+
this.deleteNavigation(targetUrl);
|
|
162
|
+
if (!isAbortError(error)) {
|
|
163
|
+
logError("Navigate error:", error);
|
|
164
|
+
}
|
|
165
|
+
return { didNavigate: false };
|
|
166
|
+
}
|
|
167
|
+
return { didNavigate: true };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
beginNavigation(props: NavigateProps): NavigationControl {
|
|
171
|
+
const existing = this._navigations.get(
|
|
172
|
+
new URL(props.href, window.location.href).href,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
switch (props.navigationType) {
|
|
176
|
+
case "userNavigation":
|
|
177
|
+
return this.beginUserNavigation(props, existing);
|
|
178
|
+
case "prefetch":
|
|
179
|
+
return this.beginPrefetch(props, existing);
|
|
180
|
+
case "revalidation":
|
|
181
|
+
return this.beginRevalidation(props);
|
|
182
|
+
case "browserHistory":
|
|
183
|
+
case "redirect":
|
|
184
|
+
default:
|
|
185
|
+
return this.createNavigation(props, "navigate");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private beginUserNavigation(
|
|
190
|
+
props: NavigateProps,
|
|
191
|
+
existing: NavigationEntry | undefined,
|
|
192
|
+
): NavigationControl {
|
|
193
|
+
const targetUrl = new URL(props.href, window.location.href).href;
|
|
194
|
+
|
|
195
|
+
// Abort all other navigations
|
|
196
|
+
this.abortAllNavigationsExcept(targetUrl);
|
|
197
|
+
|
|
198
|
+
if (existing) {
|
|
199
|
+
if (existing.type === "prefetch") {
|
|
200
|
+
// Upgrade prefetch to user navigation
|
|
201
|
+
this.upgradeNavigation(targetUrl, {
|
|
202
|
+
type: "userNavigation",
|
|
203
|
+
intent: "navigate",
|
|
204
|
+
scrollToTop: props.scrollToTop,
|
|
205
|
+
replace: props.replace,
|
|
206
|
+
state: props.state,
|
|
207
|
+
});
|
|
208
|
+
return existing.control;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Already navigating to this URL, return existing
|
|
212
|
+
return existing.control;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return this.createNavigation(props, "navigate");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private beginPrefetch(
|
|
219
|
+
props: NavigateProps,
|
|
220
|
+
existing: NavigationEntry | undefined,
|
|
221
|
+
): NavigationControl {
|
|
222
|
+
const targetUrl = new URL(props.href, window.location.href).href;
|
|
223
|
+
|
|
224
|
+
if (existing) {
|
|
225
|
+
return existing.control;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Don't prefetch current page
|
|
229
|
+
const currentUrl = new URL(window.location.href);
|
|
230
|
+
const targetUrlObj = new URL(targetUrl);
|
|
231
|
+
currentUrl.hash = "";
|
|
232
|
+
targetUrlObj.hash = "";
|
|
233
|
+
if (currentUrl.href === targetUrlObj.href) {
|
|
234
|
+
// Return a no-op control
|
|
235
|
+
return {
|
|
236
|
+
abortController: new AbortController(),
|
|
237
|
+
promise: Promise.resolve(undefined),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return this.createNavigation(props, "none");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private beginRevalidation(props: NavigateProps): NavigationControl {
|
|
245
|
+
// Store current URL to validate against later
|
|
246
|
+
const currentUrl = window.location.href;
|
|
247
|
+
|
|
248
|
+
// Check for recent revalidation to same URL
|
|
249
|
+
const existing = this._navigations.get(currentUrl);
|
|
250
|
+
if (
|
|
251
|
+
existing?.type === "revalidation" &&
|
|
252
|
+
Date.now() - existing.startTime < this.REVALIDATION_COALESCE_MS
|
|
253
|
+
) {
|
|
254
|
+
return existing.control;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Abort other revalidations
|
|
258
|
+
for (const [key, nav] of this._navigations.entries()) {
|
|
259
|
+
if (nav.type === "revalidation") {
|
|
260
|
+
nav.control.abortController?.abort();
|
|
261
|
+
this.deleteNavigation(key);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Create revalidation with current URL
|
|
266
|
+
return this.createNavigation(
|
|
267
|
+
{ ...props, href: currentUrl },
|
|
268
|
+
"revalidate",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private createNavigation(
|
|
273
|
+
props: NavigateProps,
|
|
274
|
+
intent: NavigationIntent,
|
|
275
|
+
): NavigationControl {
|
|
276
|
+
const controller = new AbortController();
|
|
277
|
+
const targetUrl = new URL(props.href, window.location.href).href;
|
|
278
|
+
|
|
279
|
+
const entry: NavigationEntry = {
|
|
280
|
+
control: {
|
|
281
|
+
abortController: controller,
|
|
282
|
+
promise: this.fetchRouteData(controller, props).catch(
|
|
283
|
+
(error) => {
|
|
284
|
+
this.deleteNavigation(targetUrl);
|
|
285
|
+
throw error;
|
|
286
|
+
},
|
|
287
|
+
),
|
|
288
|
+
},
|
|
289
|
+
type: props.navigationType,
|
|
290
|
+
intent,
|
|
291
|
+
phase: "fetching",
|
|
292
|
+
startTime: Date.now(),
|
|
293
|
+
targetUrl,
|
|
294
|
+
originUrl: window.location.href,
|
|
295
|
+
scrollToTop: props.scrollToTop,
|
|
296
|
+
replace: props.replace,
|
|
297
|
+
state: props.state,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
this.setNavigation(targetUrl, entry);
|
|
301
|
+
return entry.control;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private upgradeNavigation(
|
|
305
|
+
href: string,
|
|
306
|
+
updates: Partial<
|
|
307
|
+
Pick<
|
|
308
|
+
NavigationEntry,
|
|
309
|
+
"type" | "intent" | "scrollToTop" | "replace" | "state"
|
|
310
|
+
>
|
|
311
|
+
>,
|
|
312
|
+
): void {
|
|
313
|
+
const existing = this._navigations.get(href);
|
|
314
|
+
if (!existing) return;
|
|
315
|
+
|
|
316
|
+
this.setNavigation(href, {
|
|
317
|
+
...existing,
|
|
318
|
+
...updates,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private transitionPhase(href: string, phase: NavigationPhase): void {
|
|
323
|
+
const existing = this._navigations.get(href);
|
|
324
|
+
if (!existing) return;
|
|
325
|
+
|
|
326
|
+
this.setNavigation(href, {
|
|
327
|
+
...existing,
|
|
328
|
+
phase,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private canSkipServerFetch(targetUrl: string): {
|
|
333
|
+
canSkip: boolean;
|
|
334
|
+
matchResult?: any;
|
|
335
|
+
importURLs?: string[];
|
|
336
|
+
exportKeys?: string[];
|
|
337
|
+
loadersData?: any[];
|
|
338
|
+
} {
|
|
339
|
+
const routeManifest = __vormaClientGlobal.get("routeManifest");
|
|
340
|
+
if (!routeManifest) {
|
|
341
|
+
return { canSkip: false };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const patternRegistry = __vormaClientGlobal.get("patternRegistry");
|
|
345
|
+
if (!patternRegistry) {
|
|
346
|
+
return { canSkip: false };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const patternToWaitFnMap =
|
|
350
|
+
__vormaClientGlobal.get("patternToWaitFnMap") || {};
|
|
351
|
+
|
|
352
|
+
const url = new URL(targetUrl);
|
|
353
|
+
const matchResult = findNestedMatches(patternRegistry, url.pathname);
|
|
354
|
+
if (!matchResult) {
|
|
355
|
+
return { canSkip: false };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const clientModuleMap =
|
|
359
|
+
__vormaClientGlobal.get("clientModuleMap") || {};
|
|
360
|
+
const currentMatchedPatterns =
|
|
361
|
+
__vormaClientGlobal.get("matchedPatterns") || [];
|
|
362
|
+
const currentParams = __vormaClientGlobal.get("params") || {};
|
|
363
|
+
const currentSplatValues = __vormaClientGlobal.get("splatValues") || [];
|
|
364
|
+
const currentLoadersData = __vormaClientGlobal.get("loadersData") || [];
|
|
365
|
+
|
|
366
|
+
// Check if any current server loaders are being removed
|
|
367
|
+
for (const pattern of currentMatchedPatterns) {
|
|
368
|
+
const hasServerLoader = routeManifest[pattern] === 1;
|
|
369
|
+
if (hasServerLoader) {
|
|
370
|
+
const stillMatched = matchResult.matches.some(
|
|
371
|
+
(m: any) => m.registeredPattern.originalPattern === pattern,
|
|
372
|
+
);
|
|
373
|
+
if (!stillMatched) {
|
|
374
|
+
// A server loader is being removed - must fetch from server
|
|
375
|
+
return { canSkip: false };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Block skip if the target introduces a new client loader
|
|
381
|
+
for (const m of matchResult.matches) {
|
|
382
|
+
const pattern = m.registeredPattern.originalPattern;
|
|
383
|
+
const hasClientLoader = !!patternToWaitFnMap[pattern];
|
|
384
|
+
const wasAlreadyMatched = currentMatchedPatterns.includes(pattern);
|
|
385
|
+
if (hasClientLoader && !wasAlreadyMatched) {
|
|
386
|
+
return { canSkip: false };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let outermostLoaderIndex = -1;
|
|
391
|
+
for (let i = matchResult.matches.length - 1; i >= 0; i--) {
|
|
392
|
+
const match: Match | undefined = matchResult.matches[i];
|
|
393
|
+
if (!match) continue;
|
|
394
|
+
|
|
395
|
+
const pattern = match.registeredPattern.originalPattern;
|
|
396
|
+
const hasServerLoader = routeManifest[pattern] === 1;
|
|
397
|
+
const hasClientLoader = !!patternToWaitFnMap[pattern];
|
|
398
|
+
|
|
399
|
+
if (hasServerLoader || hasClientLoader) {
|
|
400
|
+
outermostLoaderIndex = i;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const currentUrlObj = new URL(window.location.href);
|
|
406
|
+
const currentParamsSorted = Array.from(
|
|
407
|
+
currentUrlObj.searchParams.entries(),
|
|
408
|
+
).sort();
|
|
409
|
+
const targetParamsSorted = Array.from(
|
|
410
|
+
url.searchParams.entries(),
|
|
411
|
+
).sort();
|
|
412
|
+
const searchChanged = !jsonDeepEquals(
|
|
413
|
+
currentParamsSorted,
|
|
414
|
+
targetParamsSorted,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
if (searchChanged && outermostLoaderIndex !== -1) {
|
|
418
|
+
return { canSkip: false };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (outermostLoaderIndex !== -1) {
|
|
422
|
+
const outermostMatch = matchResult.matches[outermostLoaderIndex];
|
|
423
|
+
if (outermostMatch) {
|
|
424
|
+
for (const seg of outermostMatch.registeredPattern
|
|
425
|
+
.normalizedSegments) {
|
|
426
|
+
if (seg.segType === "dynamic") {
|
|
427
|
+
const paramName = seg.normalizedVal.substring(1);
|
|
428
|
+
if (
|
|
429
|
+
matchResult.params[paramName] !==
|
|
430
|
+
currentParams[paramName]
|
|
431
|
+
) {
|
|
432
|
+
return { canSkip: false };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const hasSplat =
|
|
438
|
+
outermostMatch.registeredPattern.lastSegType === "splat";
|
|
439
|
+
|
|
440
|
+
if (hasSplat) {
|
|
441
|
+
if (
|
|
442
|
+
!jsonDeepEquals(
|
|
443
|
+
matchResult.splatValues,
|
|
444
|
+
currentSplatValues,
|
|
445
|
+
)
|
|
446
|
+
) {
|
|
447
|
+
return { canSkip: false };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const importURLs: string[] = [];
|
|
454
|
+
const exportKeys: string[] = [];
|
|
455
|
+
const loadersData: any[] = [];
|
|
456
|
+
|
|
457
|
+
for (let i = 0; i < matchResult.matches.length; i++) {
|
|
458
|
+
const match: Match | undefined = matchResult.matches[i];
|
|
459
|
+
if (!match) continue;
|
|
460
|
+
|
|
461
|
+
const pattern = match.registeredPattern.originalPattern;
|
|
462
|
+
|
|
463
|
+
const moduleInfo = clientModuleMap[pattern];
|
|
464
|
+
if (!moduleInfo) {
|
|
465
|
+
return { canSkip: false };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
importURLs.push(moduleInfo.importURL);
|
|
469
|
+
exportKeys.push(moduleInfo.exportKey);
|
|
470
|
+
|
|
471
|
+
const hasServerLoader = routeManifest[pattern] === 1;
|
|
472
|
+
|
|
473
|
+
if (!hasServerLoader) {
|
|
474
|
+
loadersData.push(undefined);
|
|
475
|
+
} else {
|
|
476
|
+
const currentPatternIndex =
|
|
477
|
+
currentMatchedPatterns.indexOf(pattern);
|
|
478
|
+
|
|
479
|
+
if (currentPatternIndex === -1) {
|
|
480
|
+
// New server loader that we don't have data for
|
|
481
|
+
return { canSkip: false };
|
|
482
|
+
}
|
|
483
|
+
loadersData.push(currentLoadersData[currentPatternIndex]);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
canSkip: true,
|
|
489
|
+
matchResult,
|
|
490
|
+
importURLs,
|
|
491
|
+
exportKeys,
|
|
492
|
+
loadersData,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private async fetchRouteData(
|
|
497
|
+
controller: AbortController,
|
|
498
|
+
props: NavigateProps,
|
|
499
|
+
): Promise<NavigationResult> {
|
|
500
|
+
try {
|
|
501
|
+
const url = new URL(props.href, window.location.href);
|
|
502
|
+
|
|
503
|
+
// Check if we can skip the server fetch (not for revalidations)
|
|
504
|
+
if (
|
|
505
|
+
props.navigationType !== "revalidation" &&
|
|
506
|
+
props.navigationType !== "action"
|
|
507
|
+
) {
|
|
508
|
+
const skipCheck = this.canSkipServerFetch(url.href);
|
|
509
|
+
|
|
510
|
+
if (skipCheck.canSkip && skipCheck.matchResult) {
|
|
511
|
+
// We can use client-only navigation
|
|
512
|
+
const { importURLs, exportKeys, loadersData } = skipCheck;
|
|
513
|
+
|
|
514
|
+
// Build the response as if it came from the server
|
|
515
|
+
const json: GetRouteDataOutput = {
|
|
516
|
+
matchedPatterns: skipCheck.matchResult.matches.map(
|
|
517
|
+
(m: any) => m.registeredPattern.originalPattern,
|
|
518
|
+
),
|
|
519
|
+
loadersData: loadersData!,
|
|
520
|
+
importURLs: importURLs!,
|
|
521
|
+
exportKeys: exportKeys!,
|
|
522
|
+
hasRootData: __vormaClientGlobal.get("hasRootData"),
|
|
523
|
+
params: skipCheck.matchResult.params,
|
|
524
|
+
splatValues: skipCheck.matchResult.splatValues,
|
|
525
|
+
deps: [],
|
|
526
|
+
cssBundles: [],
|
|
527
|
+
outermostServerError: undefined,
|
|
528
|
+
outermostServerErrorIdx: undefined,
|
|
529
|
+
errorExportKeys: [],
|
|
530
|
+
title: undefined,
|
|
531
|
+
metaHeadEls: undefined,
|
|
532
|
+
restHeadEls: undefined,
|
|
533
|
+
activeComponents: undefined as unknown as [],
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// Create a response object
|
|
537
|
+
const response = new Response(JSON.stringify(json), {
|
|
538
|
+
status: 200,
|
|
539
|
+
headers: {
|
|
540
|
+
"Content-Type": "application/json",
|
|
541
|
+
"X-Vorma-Build-Id":
|
|
542
|
+
__vormaClientGlobal.get("buildID") || "1",
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const currentClientLoadersData =
|
|
547
|
+
__vormaClientGlobal.get("clientLoadersData") || [];
|
|
548
|
+
const patternToWaitFnMap =
|
|
549
|
+
__vormaClientGlobal.get("patternToWaitFnMap") || {};
|
|
550
|
+
const runningLoaders = new Map<string, Promise<any>>();
|
|
551
|
+
|
|
552
|
+
for (let i = 0; i < json.matchedPatterns.length; i++) {
|
|
553
|
+
const pattern = json.matchedPatterns[i];
|
|
554
|
+
if (!pattern) continue;
|
|
555
|
+
|
|
556
|
+
if (patternToWaitFnMap[pattern]) {
|
|
557
|
+
const currentMatchedPatterns =
|
|
558
|
+
__vormaClientGlobal.get("matchedPatterns") ||
|
|
559
|
+
[];
|
|
560
|
+
const currentPatternIndex =
|
|
561
|
+
currentMatchedPatterns.indexOf(pattern);
|
|
562
|
+
|
|
563
|
+
if (
|
|
564
|
+
currentPatternIndex !== -1 &&
|
|
565
|
+
currentClientLoadersData[
|
|
566
|
+
currentPatternIndex
|
|
567
|
+
] !== undefined
|
|
568
|
+
) {
|
|
569
|
+
runningLoaders.set(
|
|
570
|
+
pattern,
|
|
571
|
+
Promise.resolve(
|
|
572
|
+
currentClientLoadersData[
|
|
573
|
+
currentPatternIndex
|
|
574
|
+
],
|
|
575
|
+
),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const waitFnPromise = completeClientLoaders(
|
|
582
|
+
json,
|
|
583
|
+
__vormaClientGlobal.get("buildID") || "1",
|
|
584
|
+
runningLoaders,
|
|
585
|
+
controller.signal,
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
response,
|
|
590
|
+
props,
|
|
591
|
+
json,
|
|
592
|
+
cssBundlePromises: [],
|
|
593
|
+
waitFnPromise,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
url.searchParams.set(
|
|
599
|
+
"vorma_json",
|
|
600
|
+
__vormaClientGlobal.get("buildID") || "1",
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
if (props.navigationType === "revalidation") {
|
|
604
|
+
const deploymentID = __vormaClientGlobal.get("deploymentID");
|
|
605
|
+
if (deploymentID) {
|
|
606
|
+
url.searchParams.set("dpl", deploymentID);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Start server fetch and immediately process the response to JSON
|
|
611
|
+
const serverPromise = handleRedirects({
|
|
612
|
+
abortController: controller,
|
|
613
|
+
url,
|
|
614
|
+
isPrefetch: props.navigationType === "prefetch",
|
|
615
|
+
redirectCount: props.redirectCount,
|
|
616
|
+
}).then(async (result) => {
|
|
617
|
+
// Read the response body once and return both the original result and parsed JSON
|
|
618
|
+
if (
|
|
619
|
+
result.response &&
|
|
620
|
+
result.response.ok &&
|
|
621
|
+
!result.redirectData?.status
|
|
622
|
+
) {
|
|
623
|
+
const json = await result.response.json();
|
|
624
|
+
return { ...result, json };
|
|
625
|
+
}
|
|
626
|
+
return { ...result, json: undefined };
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// Try to match routes on the client and start parallel loaders
|
|
630
|
+
const pathname = url.pathname;
|
|
631
|
+
const matchResult = await findPartialMatchesOnClient(pathname);
|
|
632
|
+
const patternToWaitFnMap =
|
|
633
|
+
__vormaClientGlobal.get("patternToWaitFnMap");
|
|
634
|
+
const runningLoaders = new Map<string, Promise<any>>();
|
|
635
|
+
|
|
636
|
+
// Start client loaders for already-registered patterns
|
|
637
|
+
if (matchResult) {
|
|
638
|
+
const { params, splatValues, matches } = matchResult;
|
|
639
|
+
|
|
640
|
+
for (let i = 0; i < matches.length; i++) {
|
|
641
|
+
const match = matches[i];
|
|
642
|
+
if (!match) continue;
|
|
643
|
+
|
|
644
|
+
const pattern = match.registeredPattern.originalPattern;
|
|
645
|
+
const loaderFn = patternToWaitFnMap[pattern];
|
|
646
|
+
|
|
647
|
+
if (loaderFn) {
|
|
648
|
+
// Create a promise for this pattern's server data
|
|
649
|
+
const serverDataPromise = serverPromise
|
|
650
|
+
.then(
|
|
651
|
+
({
|
|
652
|
+
response,
|
|
653
|
+
json,
|
|
654
|
+
}): ClientLoaderAwaitedServerData<any, any> => {
|
|
655
|
+
if (!response || !response.ok || !json) {
|
|
656
|
+
return {
|
|
657
|
+
matchedPatterns: [],
|
|
658
|
+
loaderData: undefined,
|
|
659
|
+
rootData: null,
|
|
660
|
+
buildID: "1",
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
const serverIdx =
|
|
664
|
+
json.matchedPatterns?.indexOf(pattern);
|
|
665
|
+
const loaderData =
|
|
666
|
+
serverIdx !== -1 &&
|
|
667
|
+
serverIdx !== undefined
|
|
668
|
+
? json.loadersData[serverIdx]
|
|
669
|
+
: undefined;
|
|
670
|
+
const rootData = json.hasRootData
|
|
671
|
+
? json.loadersData[0]
|
|
672
|
+
: null;
|
|
673
|
+
const buildID =
|
|
674
|
+
getBuildIDFromResponse(response) || "1";
|
|
675
|
+
return {
|
|
676
|
+
matchedPatterns:
|
|
677
|
+
json.matchedPatterns || [],
|
|
678
|
+
loaderData,
|
|
679
|
+
rootData,
|
|
680
|
+
buildID,
|
|
681
|
+
};
|
|
682
|
+
},
|
|
683
|
+
)
|
|
684
|
+
.catch(() => ({
|
|
685
|
+
matchedPatterns: [],
|
|
686
|
+
loaderData: undefined,
|
|
687
|
+
rootData: null,
|
|
688
|
+
buildID: "1",
|
|
689
|
+
}));
|
|
690
|
+
|
|
691
|
+
const loaderPromise = loaderFn({
|
|
692
|
+
params,
|
|
693
|
+
splatValues,
|
|
694
|
+
serverDataPromise,
|
|
695
|
+
signal: controller.signal,
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
runningLoaders.set(pattern, loaderPromise);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Wait for server response
|
|
704
|
+
const { redirectData, response, json } = await serverPromise;
|
|
705
|
+
|
|
706
|
+
const redirected = redirectData?.status === "did";
|
|
707
|
+
const responseNotOK = !response?.ok && response?.status !== 304;
|
|
708
|
+
|
|
709
|
+
if (redirected || !response) {
|
|
710
|
+
// This is a valid end to a navigation attempt (e.g., a redirect occurred
|
|
711
|
+
// or the request was aborted). It's not an error.
|
|
712
|
+
controller.abort();
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (responseNotOK) {
|
|
717
|
+
// This is a server error. Throwing an exception allows our .catch()
|
|
718
|
+
// blocks to handle cleanup and reset the loading state.
|
|
719
|
+
controller.abort();
|
|
720
|
+
throw new Error(`Fetch failed with status ${response.status}`);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (redirectData?.status === "should") {
|
|
724
|
+
controller.abort();
|
|
725
|
+
return { response, redirectData, props };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (!json) {
|
|
729
|
+
controller.abort();
|
|
730
|
+
throw new Error("No JSON response");
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// deps are only present in prod because they stem from the rollup metafile
|
|
734
|
+
// (same for CSS bundles -- vite handles them in dev)
|
|
735
|
+
// so in dev, to get similar behavior, we use the importURLs
|
|
736
|
+
// (which is a subset of what the deps would be in prod)
|
|
737
|
+
const depsToPreload = import.meta.env.DEV
|
|
738
|
+
? [...new Set(json.importURLs)]
|
|
739
|
+
: json.deps;
|
|
740
|
+
for (const dep of depsToPreload ?? []) {
|
|
741
|
+
if (dep) AssetManager.preloadModule(dep);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const buildID = getBuildIDFromResponse(response);
|
|
745
|
+
|
|
746
|
+
// Complete client loader execution
|
|
747
|
+
const waitFnPromise = completeClientLoaders(
|
|
748
|
+
json,
|
|
749
|
+
buildID,
|
|
750
|
+
runningLoaders,
|
|
751
|
+
controller.signal,
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
const cssBundlePromises: Array<Promise<any>> = [];
|
|
755
|
+
for (const bundle of json.cssBundles ?? []) {
|
|
756
|
+
cssBundlePromises.push(AssetManager.preloadCSS(bundle));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return { response, json, props, cssBundlePromises, waitFnPromise };
|
|
760
|
+
} catch (error) {
|
|
761
|
+
if (!isAbortError(error)) {
|
|
762
|
+
logError("Navigation failed", error);
|
|
763
|
+
}
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
private async processNavigationResult(
|
|
769
|
+
result: NavigationResult,
|
|
770
|
+
entry: NavigationEntry,
|
|
771
|
+
): Promise<void> {
|
|
772
|
+
try {
|
|
773
|
+
if (!result) return;
|
|
774
|
+
|
|
775
|
+
if ("redirectData" in result) {
|
|
776
|
+
// Skip redirect effectuation for pure prefetches
|
|
777
|
+
if (entry.type === "prefetch" && entry.intent === "none") {
|
|
778
|
+
this.deleteNavigation(entry.targetUrl);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Clean up before redirect to prevent race conditions
|
|
783
|
+
this.deleteNavigation(entry.targetUrl);
|
|
784
|
+
|
|
785
|
+
await effectuateRedirectDataResult(
|
|
786
|
+
result.redirectData,
|
|
787
|
+
result.props.redirectCount || 0,
|
|
788
|
+
result.props,
|
|
789
|
+
);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Sanity check -- should not happen
|
|
794
|
+
if (!("json" in result)) {
|
|
795
|
+
logError("Invalid navigation result: no JSON or redirect");
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Only update module map and apply CSS if build IDs match
|
|
800
|
+
const currentBuildID = __vormaClientGlobal.get("buildID");
|
|
801
|
+
const responseBuildID = getBuildIDFromResponse(result.response);
|
|
802
|
+
|
|
803
|
+
if (responseBuildID === currentBuildID) {
|
|
804
|
+
// Update module map only when builds match
|
|
805
|
+
const clientModuleMap =
|
|
806
|
+
__vormaClientGlobal.get("clientModuleMap") || {};
|
|
807
|
+
const matchedPatterns = result.json.matchedPatterns || [];
|
|
808
|
+
const importURLs = result.json.importURLs || [];
|
|
809
|
+
const exportKeys = result.json.exportKeys || [];
|
|
810
|
+
const errorExportKeys = result.json.errorExportKeys || [];
|
|
811
|
+
|
|
812
|
+
for (let i = 0; i < matchedPatterns.length; i++) {
|
|
813
|
+
const pattern = matchedPatterns[i];
|
|
814
|
+
const importURL = importURLs[i];
|
|
815
|
+
const exportKey = exportKeys[i];
|
|
816
|
+
const errorExportKey = errorExportKeys[i];
|
|
817
|
+
|
|
818
|
+
if (pattern && importURL) {
|
|
819
|
+
clientModuleMap[pattern] = {
|
|
820
|
+
importURL,
|
|
821
|
+
exportKey: exportKey || "default",
|
|
822
|
+
errorExportKey: errorExportKey || "",
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
__vormaClientGlobal.set("clientModuleMap", clientModuleMap);
|
|
828
|
+
|
|
829
|
+
// Apply CSS bundles immediately, even for prefetches.
|
|
830
|
+
// This ensures that if the user doesn't actually click now,
|
|
831
|
+
// but they do later (and it happens to be eligible for skip),
|
|
832
|
+
// everything still works.
|
|
833
|
+
if (
|
|
834
|
+
result.json.cssBundles &&
|
|
835
|
+
result.json.cssBundles.length > 0
|
|
836
|
+
) {
|
|
837
|
+
AssetManager.applyCSS(result.json.cssBundles);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Validate revalidation is still applicable
|
|
842
|
+
if (entry.type === "revalidation") {
|
|
843
|
+
const currentUrl = window.location.href;
|
|
844
|
+
if (currentUrl !== entry.originUrl) {
|
|
845
|
+
this.deleteNavigation(entry.targetUrl);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Transition to waiting phase
|
|
851
|
+
this.transitionPhase(entry.targetUrl, "waiting");
|
|
852
|
+
|
|
853
|
+
// Skip if navigation was aborted
|
|
854
|
+
if (!this._navigations.has(entry.targetUrl)) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Update build ID if needed
|
|
859
|
+
const oldID = __vormaClientGlobal.get("buildID");
|
|
860
|
+
const newID = getBuildIDFromResponse(result.response);
|
|
861
|
+
if (newID && newID !== oldID) {
|
|
862
|
+
dispatchBuildIDEvent({ newID, oldID });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Wait for client loaders and set state
|
|
866
|
+
const clientLoadersResult = await result.waitFnPromise;
|
|
867
|
+
setClientLoadersState(clientLoadersResult);
|
|
868
|
+
|
|
869
|
+
// Wait for CSS
|
|
870
|
+
if (result.cssBundlePromises.length > 0) {
|
|
871
|
+
try {
|
|
872
|
+
await Promise.all(result.cssBundlePromises);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
logError("Error preloading CSS bundles:", error);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Skip rendering for prefetch without intent
|
|
879
|
+
if (entry.intent === "none") {
|
|
880
|
+
this.transitionPhase(entry.targetUrl, "complete");
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Skip rendering for revalidation if not on target page
|
|
885
|
+
if (
|
|
886
|
+
entry.type === "revalidation" &&
|
|
887
|
+
window.location.href !== entry.originUrl
|
|
888
|
+
) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Transition to rendering phase
|
|
893
|
+
this.transitionPhase(entry.targetUrl, "rendering");
|
|
894
|
+
|
|
895
|
+
// Render the app
|
|
896
|
+
try {
|
|
897
|
+
await __reRenderApp({
|
|
898
|
+
json: result.json,
|
|
899
|
+
navigationType: entry.type,
|
|
900
|
+
runHistoryOptions:
|
|
901
|
+
entry.intent === "navigate"
|
|
902
|
+
? {
|
|
903
|
+
href: entry.targetUrl,
|
|
904
|
+
scrollStateToRestore:
|
|
905
|
+
result.props.scrollStateToRestore,
|
|
906
|
+
replace:
|
|
907
|
+
entry.replace || result.props.replace,
|
|
908
|
+
scrollToTop: entry.scrollToTop,
|
|
909
|
+
state: entry.state,
|
|
910
|
+
}
|
|
911
|
+
: undefined,
|
|
912
|
+
onFinish: () => {
|
|
913
|
+
this.transitionPhase(entry.targetUrl, "complete");
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
} catch (error) {
|
|
917
|
+
this.transitionPhase(entry.targetUrl, "complete");
|
|
918
|
+
if (!isAbortError(error)) {
|
|
919
|
+
logError("Error completing navigation", error);
|
|
920
|
+
}
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
} finally {
|
|
924
|
+
if (!(entry.type === "prefetch" && entry.intent === "none")) {
|
|
925
|
+
this.deleteNavigation(entry.targetUrl);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async submit<T = any>(
|
|
931
|
+
url: string | URL,
|
|
932
|
+
requestInit?: RequestInit,
|
|
933
|
+
options?: SubmitOptions,
|
|
934
|
+
): Promise<{ success: true; data: T } | { success: false; error: string }> {
|
|
935
|
+
const abortController = new AbortController();
|
|
936
|
+
const submissionKey = options?.dedupeKey
|
|
937
|
+
? `submission:${options.dedupeKey}`
|
|
938
|
+
: Symbol("submission");
|
|
939
|
+
|
|
940
|
+
// Abort duplicate submission
|
|
941
|
+
if (typeof submissionKey === "string") {
|
|
942
|
+
const existing = this._submissions.get(submissionKey);
|
|
943
|
+
if (existing) {
|
|
944
|
+
existing.control.abortController?.abort("deduped");
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const entry: SubmissionEntry = {
|
|
949
|
+
control: {
|
|
950
|
+
abortController,
|
|
951
|
+
promise: Promise.resolve() as any,
|
|
952
|
+
},
|
|
953
|
+
startTime: Date.now(),
|
|
954
|
+
skipGlobalLoadingIndicator: options?.skipGlobalLoadingIndicator,
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
this._submissions.set(submissionKey, entry);
|
|
958
|
+
this.scheduleStatusUpdate();
|
|
959
|
+
|
|
960
|
+
try {
|
|
961
|
+
const urlToUse = new URL(url, window.location.href);
|
|
962
|
+
const headers = new Headers(requestInit?.headers);
|
|
963
|
+
const deploymentID = __vormaClientGlobal.get("deploymentID");
|
|
964
|
+
if (deploymentID) {
|
|
965
|
+
headers.set("x-deployment-id", deploymentID);
|
|
966
|
+
}
|
|
967
|
+
const finalRequestInit: RequestInit = {
|
|
968
|
+
...requestInit,
|
|
969
|
+
headers,
|
|
970
|
+
signal: abortController.signal,
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
const { redirectData, response } = await handleRedirects({
|
|
974
|
+
abortController,
|
|
975
|
+
url: urlToUse,
|
|
976
|
+
isPrefetch: false,
|
|
977
|
+
redirectCount: 0,
|
|
978
|
+
requestInit: finalRequestInit,
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
const oldID = __vormaClientGlobal.get("buildID");
|
|
982
|
+
const newID = getBuildIDFromResponse(response);
|
|
983
|
+
if (newID && newID !== oldID) {
|
|
984
|
+
dispatchBuildIDEvent({ newID, oldID });
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (!response || !response.ok) {
|
|
988
|
+
return {
|
|
989
|
+
success: false,
|
|
990
|
+
error: String(response?.status || "unknown"),
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (redirectData?.status === "should") {
|
|
995
|
+
await effectuateRedirectDataResult(redirectData, 0);
|
|
996
|
+
return { success: true, data: undefined as T }; // No data on redirect
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const data = await response.json();
|
|
1000
|
+
|
|
1001
|
+
// Auto-revalidate for mutations
|
|
1002
|
+
const isGET = getIsGETRequest(requestInit);
|
|
1003
|
+
const redirected = redirectData?.status === "did";
|
|
1004
|
+
if (!isGET && !redirected && options?.revalidate !== false) {
|
|
1005
|
+
await revalidate();
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return { success: true, data: data as T };
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
if (isAbortError(error)) {
|
|
1011
|
+
return { success: false, error: "Aborted" };
|
|
1012
|
+
}
|
|
1013
|
+
logError(error);
|
|
1014
|
+
return {
|
|
1015
|
+
success: false,
|
|
1016
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1017
|
+
};
|
|
1018
|
+
} finally {
|
|
1019
|
+
this._submissions.delete(submissionKey);
|
|
1020
|
+
this.scheduleStatusUpdate();
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
private setNavigation(key: string, entry: NavigationEntry): void {
|
|
1025
|
+
this._navigations.set(key, entry);
|
|
1026
|
+
this.scheduleStatusUpdate();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
private deleteNavigation(key: string): boolean {
|
|
1030
|
+
const result = this._navigations.delete(key);
|
|
1031
|
+
if (result) {
|
|
1032
|
+
this.scheduleStatusUpdate();
|
|
1033
|
+
}
|
|
1034
|
+
return result;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
removeNavigation(key: string): void {
|
|
1038
|
+
this.deleteNavigation(key);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
getNavigation(key: string): NavigationEntry | undefined {
|
|
1042
|
+
return this._navigations.get(key);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
hasNavigation(key: string): boolean {
|
|
1046
|
+
return this._navigations.has(key);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
getNavigationsSize(): number {
|
|
1050
|
+
return this._navigations.size;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
getNavigations(): Map<string, NavigationEntry> {
|
|
1054
|
+
return this._navigations;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
private abortAllNavigationsExcept(excludeHref?: string): void {
|
|
1058
|
+
for (const [href, nav] of this._navigations.entries()) {
|
|
1059
|
+
if (href !== excludeHref) {
|
|
1060
|
+
nav.control.abortController?.abort();
|
|
1061
|
+
this.deleteNavigation(href);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
getStatus(): StatusEventDetail {
|
|
1067
|
+
const navigations = Array.from(this._navigations.values());
|
|
1068
|
+
const submissions = Array.from(this._submissions.values());
|
|
1069
|
+
|
|
1070
|
+
const isNavigating = navigations.some(
|
|
1071
|
+
(nav) => nav.intent === "navigate" && nav.phase !== "complete",
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1074
|
+
const isRevalidating = navigations.some(
|
|
1075
|
+
(nav) => nav.type === "revalidation" && nav.phase !== "complete",
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
const isSubmitting = submissions.some(
|
|
1079
|
+
(x) => !x.skipGlobalLoadingIndicator,
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
return { isNavigating, isSubmitting, isRevalidating };
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
clearAll(): void {
|
|
1086
|
+
for (const nav of this._navigations.values()) {
|
|
1087
|
+
nav.control.abortController?.abort();
|
|
1088
|
+
}
|
|
1089
|
+
this._navigations.clear();
|
|
1090
|
+
for (const sub of this._submissions.values()) {
|
|
1091
|
+
sub.control.abortController?.abort();
|
|
1092
|
+
}
|
|
1093
|
+
this._submissions.clear();
|
|
1094
|
+
this.scheduleStatusUpdate();
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
private scheduleStatusUpdate(): void {
|
|
1098
|
+
this.dispatchStatusEventDebounced();
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
private dispatchStatusEvent(): void {
|
|
1102
|
+
const newStatus = this.getStatus();
|
|
1103
|
+
|
|
1104
|
+
if (jsonDeepEquals(this.lastDispatchedStatus, newStatus)) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
this.lastDispatchedStatus = newStatus;
|
|
1108
|
+
dispatchStatusEvent(newStatus);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Global instance
|
|
1113
|
+
export const navigationStateManager = new NavigationStateManager();
|
|
1114
|
+
|
|
1115
|
+
/////////////////////////////////////////////////////////////////////
|
|
1116
|
+
// PUBLIC API
|
|
1117
|
+
/////////////////////////////////////////////////////////////////////
|
|
1118
|
+
|
|
1119
|
+
export async function vormaNavigate(
|
|
1120
|
+
href: string,
|
|
1121
|
+
options?: {
|
|
1122
|
+
replace?: boolean;
|
|
1123
|
+
scrollToTop?: boolean;
|
|
1124
|
+
search?: string;
|
|
1125
|
+
hash?: string;
|
|
1126
|
+
state?: unknown;
|
|
1127
|
+
},
|
|
1128
|
+
): Promise<void> {
|
|
1129
|
+
const url = new URL(href, window.location.href);
|
|
1130
|
+
|
|
1131
|
+
if (options?.search !== undefined) {
|
|
1132
|
+
url.search = options.search;
|
|
1133
|
+
}
|
|
1134
|
+
if (options?.hash !== undefined) {
|
|
1135
|
+
url.hash = options.hash;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
await navigationStateManager.navigate({
|
|
1139
|
+
href: url.href,
|
|
1140
|
+
navigationType: "userNavigation",
|
|
1141
|
+
replace: options?.replace,
|
|
1142
|
+
scrollToTop: options?.scrollToTop,
|
|
1143
|
+
state: options?.state,
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
let lastTriggeredNavOrRevalidateTimestampMS = Date.now();
|
|
1148
|
+
|
|
1149
|
+
export function getLastTriggeredNavOrRevalidateTimestampMS(): number {
|
|
1150
|
+
return lastTriggeredNavOrRevalidateTimestampMS;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
export async function revalidate() {
|
|
1154
|
+
await navigationStateManager.navigate({
|
|
1155
|
+
href: window.location.href,
|
|
1156
|
+
navigationType: "revalidation",
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
export type SubmitOptions = {
|
|
1161
|
+
dedupeKey?: string;
|
|
1162
|
+
revalidate?: boolean;
|
|
1163
|
+
skipGlobalLoadingIndicator?: boolean;
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
export async function submit<T = any>(
|
|
1167
|
+
url: string | URL,
|
|
1168
|
+
requestInit?: RequestInit,
|
|
1169
|
+
options?: SubmitOptions,
|
|
1170
|
+
): Promise<{ success: true; data: T } | { success: false; error: string }> {
|
|
1171
|
+
return navigationStateManager.submit(url, requestInit, options);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
export function beginNavigation(props: NavigateProps): NavigationControl {
|
|
1175
|
+
return navigationStateManager.beginNavigation(props);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
export function getStatus(): StatusEventDetail {
|
|
1179
|
+
return navigationStateManager.getStatus();
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
export function getLocation() {
|
|
1183
|
+
return {
|
|
1184
|
+
pathname: window.location.pathname,
|
|
1185
|
+
search: window.location.search,
|
|
1186
|
+
hash: window.location.hash,
|
|
1187
|
+
state: HistoryManager.getInstance().location.state,
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
export function getBuildID(): string {
|
|
1192
|
+
return __vormaClientGlobal.get("buildID");
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
export function getRootEl(): HTMLDivElement {
|
|
1196
|
+
return document.getElementById("vorma-root") as HTMLDivElement;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
export function getHistoryInstance(): historyInstance {
|
|
1200
|
+
return HistoryManager.getInstance();
|
|
1201
|
+
}
|