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.
Files changed (225) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +48 -0
  3. package/internal/framework/_typescript/client/index.ts +64 -0
  4. package/internal/framework/_typescript/client/src/asset_manager.ts +67 -0
  5. package/internal/framework/_typescript/client/src/client.ts +1201 -0
  6. package/internal/framework/_typescript/client/src/client_loaders.ts +249 -0
  7. package/internal/framework/_typescript/client/src/component_loader.ts +105 -0
  8. package/internal/framework/_typescript/client/src/error_boundary.ts +7 -0
  9. package/internal/framework/_typescript/client/src/events.ts +54 -0
  10. package/internal/framework/_typescript/client/src/global_loading_indicator/global_loading_indicator.ts +125 -0
  11. package/internal/framework/_typescript/client/src/hard_reload.ts +1 -0
  12. package/internal/framework/_typescript/client/src/head_elements/head_elements.ts +193 -0
  13. package/internal/framework/_typescript/client/src/history/history.ts +118 -0
  14. package/internal/framework/_typescript/client/src/history/npm_history_types.ts +83 -0
  15. package/internal/framework/_typescript/client/src/hmr/hmr.ts +71 -0
  16. package/internal/framework/_typescript/client/src/init_client.ts +134 -0
  17. package/internal/framework/_typescript/client/src/links.ts +218 -0
  18. package/internal/framework/_typescript/client/src/redirects/redirects.ts +203 -0
  19. package/internal/framework/_typescript/client/src/rendering.ts +135 -0
  20. package/internal/framework/_typescript/client/src/resolve_public_href.ts +15 -0
  21. package/internal/framework/_typescript/client/src/scroll_state_manager.ts +100 -0
  22. package/internal/framework/_typescript/client/src/static_route_defs/route_def_helpers.ts +22 -0
  23. package/internal/framework/_typescript/client/src/ui_lib_impl_helpers/link_components.ts +131 -0
  24. package/internal/framework/_typescript/client/src/ui_lib_impl_helpers/route_components.ts +56 -0
  25. package/internal/framework/_typescript/client/src/ui_lib_impl_helpers/typed_navigate.ts +58 -0
  26. package/internal/framework/_typescript/client/src/utils/errors.ts +10 -0
  27. package/internal/framework/_typescript/client/src/utils/logging.ts +7 -0
  28. package/internal/framework/_typescript/client/src/vorma_app_helpers/vorma_app_helpers.ts +290 -0
  29. package/internal/framework/_typescript/client/src/vorma_ctx/vorma_ctx.ts +128 -0
  30. package/internal/framework/_typescript/client/src/window_focus_revalidation/window_focus_revalidation.ts +32 -0
  31. package/internal/framework/_typescript/client/tsconfig.json +3 -0
  32. package/internal/framework/_typescript/create/main.ts +378 -0
  33. package/internal/framework/_typescript/create/package.json +33 -0
  34. package/internal/framework/_typescript/create/pnpm-lock.yaml +70 -0
  35. package/internal/framework/_typescript/create/tsconfig.json +3 -0
  36. package/internal/framework/_typescript/preact/index.tsx +10 -0
  37. package/internal/framework/_typescript/preact/src/helpers.ts +113 -0
  38. package/internal/framework/_typescript/preact/src/link.tsx +107 -0
  39. package/internal/framework/_typescript/preact/src/preact.tsx +191 -0
  40. package/internal/framework/_typescript/preact/tsconfig.json +7 -0
  41. package/internal/framework/_typescript/react/index.tsx +10 -0
  42. package/internal/framework/_typescript/react/src/helpers.ts +118 -0
  43. package/internal/framework/_typescript/react/src/link.tsx +115 -0
  44. package/internal/framework/_typescript/react/src/react.tsx +299 -0
  45. package/internal/framework/_typescript/react/tsconfig.json +6 -0
  46. package/internal/framework/_typescript/solid/index.tsx +10 -0
  47. package/internal/framework/_typescript/solid/src/helpers.ts +114 -0
  48. package/internal/framework/_typescript/solid/src/link.tsx +104 -0
  49. package/internal/framework/_typescript/solid/src/solid.tsx +204 -0
  50. package/internal/framework/_typescript/solid/tsconfig.json +7 -0
  51. package/internal/framework/_typescript/vite/tsconfig.json +3 -0
  52. package/internal/framework/_typescript/vite/vite.ts +93 -0
  53. package/internal/site/frontend/assets/vorma-banner.webp +0 -0
  54. package/kit/_typescript/converters/converters.ts +152 -0
  55. package/kit/_typescript/cookies/cookies.ts +18 -0
  56. package/kit/_typescript/csrf/csrf.ts +10 -0
  57. package/kit/_typescript/debounce/debounce.ts +17 -0
  58. package/kit/_typescript/fmt/fmt.ts +3 -0
  59. package/kit/_typescript/json/deep_equals.ts +54 -0
  60. package/kit/_typescript/json/json.ts +3 -0
  61. package/kit/_typescript/json/search_param_serializer.ts +49 -0
  62. package/kit/_typescript/json/stringify_stable.ts +43 -0
  63. package/kit/_typescript/listeners/listeners.ts +16 -0
  64. package/kit/_typescript/matcher/find_best_match.ts +205 -0
  65. package/kit/_typescript/matcher/find_nested_matches.ts +357 -0
  66. package/kit/_typescript/matcher/parse_segments.ts +30 -0
  67. package/kit/_typescript/matcher/register.ts +271 -0
  68. package/kit/_typescript/theme/theme.ts +177 -0
  69. package/kit/_typescript/tsconfig.json +3 -0
  70. package/kit/_typescript/url/url.ts +132 -0
  71. package/npm_dist/internal/framework/_typescript/client/index.d.ts +17 -0
  72. package/npm_dist/internal/framework/_typescript/client/index.d.ts.map +1 -0
  73. package/npm_dist/internal/framework/_typescript/client/index.js +2489 -0
  74. package/npm_dist/internal/framework/_typescript/client/index.js.map +7 -0
  75. package/npm_dist/internal/framework/_typescript/client/src/asset_manager.d.ts +6 -0
  76. package/npm_dist/internal/framework/_typescript/client/src/asset_manager.d.ts.map +1 -0
  77. package/npm_dist/internal/framework/_typescript/client/src/client.d.ts +119 -0
  78. package/npm_dist/internal/framework/_typescript/client/src/client.d.ts.map +1 -0
  79. package/npm_dist/internal/framework/_typescript/client/src/client_loaders.d.ts +18 -0
  80. package/npm_dist/internal/framework/_typescript/client/src/client_loaders.d.ts.map +1 -0
  81. package/npm_dist/internal/framework/_typescript/client/src/component_loader.d.ts +10 -0
  82. package/npm_dist/internal/framework/_typescript/client/src/component_loader.d.ts.map +1 -0
  83. package/npm_dist/internal/framework/_typescript/client/src/error_boundary.d.ts +3 -0
  84. package/npm_dist/internal/framework/_typescript/client/src/error_boundary.d.ts.map +1 -0
  85. package/npm_dist/internal/framework/_typescript/client/src/events.d.ts +26 -0
  86. package/npm_dist/internal/framework/_typescript/client/src/events.d.ts.map +1 -0
  87. package/npm_dist/internal/framework/_typescript/client/src/global_loading_indicator/global_loading_indicator.d.ts +12 -0
  88. package/npm_dist/internal/framework/_typescript/client/src/global_loading_indicator/global_loading_indicator.d.ts.map +1 -0
  89. package/npm_dist/internal/framework/_typescript/client/src/hard_reload.d.ts +2 -0
  90. package/npm_dist/internal/framework/_typescript/client/src/hard_reload.d.ts.map +1 -0
  91. package/npm_dist/internal/framework/_typescript/client/src/head_elements/head_elements.d.ts +7 -0
  92. package/npm_dist/internal/framework/_typescript/client/src/head_elements/head_elements.d.ts.map +1 -0
  93. package/npm_dist/internal/framework/_typescript/client/src/history/history.d.ts +14 -0
  94. package/npm_dist/internal/framework/_typescript/client/src/history/history.d.ts.map +1 -0
  95. package/npm_dist/internal/framework/_typescript/client/src/history/npm_history_types.d.ts +84 -0
  96. package/npm_dist/internal/framework/_typescript/client/src/history/npm_history_types.d.ts.map +1 -0
  97. package/npm_dist/internal/framework/_typescript/client/src/hmr/hmr.d.ts +3 -0
  98. package/npm_dist/internal/framework/_typescript/client/src/hmr/hmr.d.ts.map +1 -0
  99. package/npm_dist/internal/framework/_typescript/client/src/init_client.d.ts +9 -0
  100. package/npm_dist/internal/framework/_typescript/client/src/init_client.d.ts.map +1 -0
  101. package/npm_dist/internal/framework/_typescript/client/src/links.d.ts +33 -0
  102. package/npm_dist/internal/framework/_typescript/client/src/links.d.ts.map +1 -0
  103. package/npm_dist/internal/framework/_typescript/client/src/redirects/redirects.d.ts +26 -0
  104. package/npm_dist/internal/framework/_typescript/client/src/redirects/redirects.d.ts.map +1 -0
  105. package/npm_dist/internal/framework/_typescript/client/src/rendering.d.ts +18 -0
  106. package/npm_dist/internal/framework/_typescript/client/src/rendering.d.ts.map +1 -0
  107. package/npm_dist/internal/framework/_typescript/client/src/resolve_public_href.d.ts +2 -0
  108. package/npm_dist/internal/framework/_typescript/client/src/resolve_public_href.d.ts.map +1 -0
  109. package/npm_dist/internal/framework/_typescript/client/src/scroll_state_manager.d.ts +22 -0
  110. package/npm_dist/internal/framework/_typescript/client/src/scroll_state_manager.d.ts.map +1 -0
  111. package/npm_dist/internal/framework/_typescript/client/src/static_route_defs/route_def_helpers.d.ts +12 -0
  112. package/npm_dist/internal/framework/_typescript/client/src/static_route_defs/route_def_helpers.d.ts.map +1 -0
  113. package/npm_dist/internal/framework/_typescript/client/src/ui_lib_impl_helpers/link_components.d.ts +28 -0
  114. package/npm_dist/internal/framework/_typescript/client/src/ui_lib_impl_helpers/link_components.d.ts.map +1 -0
  115. package/npm_dist/internal/framework/_typescript/client/src/ui_lib_impl_helpers/route_components.d.ts +18 -0
  116. package/npm_dist/internal/framework/_typescript/client/src/ui_lib_impl_helpers/route_components.d.ts.map +1 -0
  117. package/npm_dist/internal/framework/_typescript/client/src/ui_lib_impl_helpers/typed_navigate.d.ts +11 -0
  118. package/npm_dist/internal/framework/_typescript/client/src/ui_lib_impl_helpers/typed_navigate.d.ts.map +1 -0
  119. package/npm_dist/internal/framework/_typescript/client/src/utils/errors.d.ts +3 -0
  120. package/npm_dist/internal/framework/_typescript/client/src/utils/errors.d.ts.map +1 -0
  121. package/npm_dist/internal/framework/_typescript/client/src/utils/logging.d.ts +3 -0
  122. package/npm_dist/internal/framework/_typescript/client/src/utils/logging.d.ts.map +1 -0
  123. package/npm_dist/internal/framework/_typescript/client/src/vorma_app_helpers/vorma_app_helpers.d.ts +119 -0
  124. package/npm_dist/internal/framework/_typescript/client/src/vorma_app_helpers/vorma_app_helpers.d.ts.map +1 -0
  125. package/npm_dist/internal/framework/_typescript/client/src/vorma_ctx/vorma_ctx.d.ts +88 -0
  126. package/npm_dist/internal/framework/_typescript/client/src/vorma_ctx/vorma_ctx.d.ts.map +1 -0
  127. package/npm_dist/internal/framework/_typescript/client/src/window_focus_revalidation/window_focus_revalidation.d.ts +10 -0
  128. package/npm_dist/internal/framework/_typescript/client/src/window_focus_revalidation/window_focus_revalidation.d.ts.map +1 -0
  129. package/npm_dist/internal/framework/_typescript/create/main.d.ts +3 -0
  130. package/npm_dist/internal/framework/_typescript/create/main.d.ts.map +1 -0
  131. package/npm_dist/internal/framework/_typescript/preact/index.d.ts +4 -0
  132. package/npm_dist/internal/framework/_typescript/preact/index.d.ts.map +1 -0
  133. package/npm_dist/internal/framework/_typescript/preact/index.js +283 -0
  134. package/npm_dist/internal/framework/_typescript/preact/index.js.map +7 -0
  135. package/npm_dist/internal/framework/_typescript/preact/src/helpers.d.ts +21 -0
  136. package/npm_dist/internal/framework/_typescript/preact/src/helpers.d.ts.map +1 -0
  137. package/npm_dist/internal/framework/_typescript/preact/src/link.d.ts +11 -0
  138. package/npm_dist/internal/framework/_typescript/preact/src/link.d.ts.map +1 -0
  139. package/npm_dist/internal/framework/_typescript/preact/src/preact.d.ts +21 -0
  140. package/npm_dist/internal/framework/_typescript/preact/src/preact.d.ts.map +1 -0
  141. package/npm_dist/internal/framework/_typescript/react/index.d.ts +4 -0
  142. package/npm_dist/internal/framework/_typescript/react/index.d.ts.map +1 -0
  143. package/npm_dist/internal/framework/_typescript/react/index.js +370 -0
  144. package/npm_dist/internal/framework/_typescript/react/index.js.map +7 -0
  145. package/npm_dist/internal/framework/_typescript/react/src/helpers.d.ts +21 -0
  146. package/npm_dist/internal/framework/_typescript/react/src/helpers.d.ts.map +1 -0
  147. package/npm_dist/internal/framework/_typescript/react/src/link.d.ts +11 -0
  148. package/npm_dist/internal/framework/_typescript/react/src/link.d.ts.map +1 -0
  149. package/npm_dist/internal/framework/_typescript/react/src/react.d.ts +20 -0
  150. package/npm_dist/internal/framework/_typescript/react/src/react.d.ts.map +1 -0
  151. package/npm_dist/internal/framework/_typescript/solid/index.d.ts +4 -0
  152. package/npm_dist/internal/framework/_typescript/solid/index.d.ts.map +1 -0
  153. package/npm_dist/internal/framework/_typescript/solid/index.js +314 -0
  154. package/npm_dist/internal/framework/_typescript/solid/index.js.map +7 -0
  155. package/npm_dist/internal/framework/_typescript/solid/src/helpers.d.ts +22 -0
  156. package/npm_dist/internal/framework/_typescript/solid/src/helpers.d.ts.map +1 -0
  157. package/npm_dist/internal/framework/_typescript/solid/src/link.d.ts +11 -0
  158. package/npm_dist/internal/framework/_typescript/solid/src/link.d.ts.map +1 -0
  159. package/npm_dist/internal/framework/_typescript/solid/src/solid.d.ts +22 -0
  160. package/npm_dist/internal/framework/_typescript/solid/src/solid.d.ts.map +1 -0
  161. package/npm_dist/internal/framework/_typescript/vite/vite.d.ts +11 -0
  162. package/npm_dist/internal/framework/_typescript/vite/vite.d.ts.map +1 -0
  163. package/npm_dist/internal/framework/_typescript/vite/vite.js +82 -0
  164. package/npm_dist/internal/framework/_typescript/vite/vite.js.map +7 -0
  165. package/npm_dist/kit/_typescript/chunk-YBAPNBS2.js +202 -0
  166. package/npm_dist/kit/_typescript/chunk-YBAPNBS2.js.map +7 -0
  167. package/npm_dist/kit/_typescript/converters/converters.d.ts +26 -0
  168. package/npm_dist/kit/_typescript/converters/converters.d.ts.map +1 -0
  169. package/npm_dist/kit/_typescript/converters/converters.js +99 -0
  170. package/npm_dist/kit/_typescript/converters/converters.js.map +7 -0
  171. package/npm_dist/kit/_typescript/cookies/cookies.d.ts +13 -0
  172. package/npm_dist/kit/_typescript/cookies/cookies.d.ts.map +1 -0
  173. package/npm_dist/kit/_typescript/cookies/cookies.js +13 -0
  174. package/npm_dist/kit/_typescript/cookies/cookies.js.map +7 -0
  175. package/npm_dist/kit/_typescript/csrf/csrf.d.ts +5 -0
  176. package/npm_dist/kit/_typescript/csrf/csrf.d.ts.map +1 -0
  177. package/npm_dist/kit/_typescript/csrf/csrf.js +11 -0
  178. package/npm_dist/kit/_typescript/csrf/csrf.js.map +7 -0
  179. package/npm_dist/kit/_typescript/debounce/debounce.d.ts +4 -0
  180. package/npm_dist/kit/_typescript/debounce/debounce.d.ts.map +1 -0
  181. package/npm_dist/kit/_typescript/debounce/debounce.js +16 -0
  182. package/npm_dist/kit/_typescript/debounce/debounce.js.map +7 -0
  183. package/npm_dist/kit/_typescript/fmt/fmt.d.ts +2 -0
  184. package/npm_dist/kit/_typescript/fmt/fmt.d.ts.map +1 -0
  185. package/npm_dist/kit/_typescript/fmt/fmt.js +8 -0
  186. package/npm_dist/kit/_typescript/fmt/fmt.js.map +7 -0
  187. package/npm_dist/kit/_typescript/json/deep_equals.d.ts +7 -0
  188. package/npm_dist/kit/_typescript/json/deep_equals.d.ts.map +1 -0
  189. package/npm_dist/kit/_typescript/json/json.d.ts +4 -0
  190. package/npm_dist/kit/_typescript/json/json.d.ts.map +1 -0
  191. package/npm_dist/kit/_typescript/json/json.js +110 -0
  192. package/npm_dist/kit/_typescript/json/json.js.map +7 -0
  193. package/npm_dist/kit/_typescript/json/search_param_serializer.d.ts +2 -0
  194. package/npm_dist/kit/_typescript/json/search_param_serializer.d.ts.map +1 -0
  195. package/npm_dist/kit/_typescript/json/stringify_stable.d.ts +7 -0
  196. package/npm_dist/kit/_typescript/json/stringify_stable.d.ts.map +1 -0
  197. package/npm_dist/kit/_typescript/listeners/listeners.d.ts +2 -0
  198. package/npm_dist/kit/_typescript/listeners/listeners.d.ts.map +1 -0
  199. package/npm_dist/kit/_typescript/listeners/listeners.js +20 -0
  200. package/npm_dist/kit/_typescript/listeners/listeners.js.map +7 -0
  201. package/npm_dist/kit/_typescript/matcher/find_best_match.d.ts +10 -0
  202. package/npm_dist/kit/_typescript/matcher/find_best_match.d.ts.map +1 -0
  203. package/npm_dist/kit/_typescript/matcher/find_best_match.js +146 -0
  204. package/npm_dist/kit/_typescript/matcher/find_best_match.js.map +7 -0
  205. package/npm_dist/kit/_typescript/matcher/find_nested_matches.d.ts +14 -0
  206. package/npm_dist/kit/_typescript/matcher/find_nested_matches.d.ts.map +1 -0
  207. package/npm_dist/kit/_typescript/matcher/find_nested_matches.js +248 -0
  208. package/npm_dist/kit/_typescript/matcher/find_nested_matches.js.map +7 -0
  209. package/npm_dist/kit/_typescript/matcher/parse_segments.d.ts +2 -0
  210. package/npm_dist/kit/_typescript/matcher/parse_segments.d.ts.map +1 -0
  211. package/npm_dist/kit/_typescript/matcher/register.d.ts +54 -0
  212. package/npm_dist/kit/_typescript/matcher/register.d.ts.map +1 -0
  213. package/npm_dist/kit/_typescript/matcher/register.js +21 -0
  214. package/npm_dist/kit/_typescript/matcher/register.js.map +7 -0
  215. package/npm_dist/kit/_typescript/theme/theme.d.ts +24 -0
  216. package/npm_dist/kit/_typescript/theme/theme.d.ts.map +1 -0
  217. package/npm_dist/kit/_typescript/theme/theme.js +133 -0
  218. package/npm_dist/kit/_typescript/theme/theme.js.map +7 -0
  219. package/npm_dist/kit/_typescript/url/url.d.ts +30 -0
  220. package/npm_dist/kit/_typescript/url/url.d.ts.map +1 -0
  221. package/npm_dist/kit/_typescript/url/url.js +100 -0
  222. package/npm_dist/kit/_typescript/url/url.js.map +7 -0
  223. package/package.json +135 -3
  224. package/tsconfig.base.json +17 -0
  225. 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
+ }