@saolabs/client 1.0.0 → 1.0.2

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 (378) hide show
  1. package/README.md +52 -195
  2. package/dist/index.d.ts +20 -5
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +14 -3
  5. package/dist/index.js.map +1 -1
  6. package/dist/plugins.d.ts +2 -2
  7. package/dist/plugins.js +2 -2
  8. package/dist/src/core/app/Application.d.ts +17 -15
  9. package/dist/src/core/app/Application.d.ts.map +1 -1
  10. package/dist/src/core/app/Application.js +41 -18
  11. package/dist/src/core/app/Application.js.map +1 -1
  12. package/dist/src/core/bootstrap/app.d.ts +1 -1
  13. package/dist/src/core/bootstrap/app.d.ts.map +1 -1
  14. package/dist/src/core/bootstrap/app.js +74 -4
  15. package/dist/src/core/bootstrap/app.js.map +1 -1
  16. package/dist/src/core/bootstrap/bundle.d.ts +41 -0
  17. package/dist/src/core/bootstrap/bundle.d.ts.map +1 -0
  18. package/dist/src/core/bootstrap/bundle.js +98 -0
  19. package/dist/src/core/bootstrap/bundle.js.map +1 -0
  20. package/dist/src/core/bootstrap/providers/ApiServiceProvider.d.ts +1 -1
  21. package/dist/src/core/bootstrap/providers/ApiServiceProvider.d.ts.map +1 -1
  22. package/dist/src/core/bootstrap/providers/ApiServiceProvider.js +3 -3
  23. package/dist/src/core/bootstrap/providers/ApiServiceProvider.js.map +1 -1
  24. package/dist/src/core/bootstrap/providers/CoreServiceProvider.d.ts +1 -1
  25. package/dist/src/core/bootstrap/providers/CoreServiceProvider.d.ts.map +1 -1
  26. package/dist/src/core/bootstrap/providers/CoreServiceProvider.js +11 -8
  27. package/dist/src/core/bootstrap/providers/CoreServiceProvider.js.map +1 -1
  28. package/dist/src/core/bootstrap/providers/HelperServiceProvider.d.ts +3 -1
  29. package/dist/src/core/bootstrap/providers/HelperServiceProvider.d.ts.map +1 -1
  30. package/dist/src/core/bootstrap/providers/HelperServiceProvider.js +8 -3
  31. package/dist/src/core/bootstrap/providers/HelperServiceProvider.js.map +1 -1
  32. package/dist/src/core/bootstrap/providers/RouteServiceProvider.d.ts +2 -1
  33. package/dist/src/core/bootstrap/providers/RouteServiceProvider.d.ts.map +1 -1
  34. package/dist/src/core/bootstrap/providers/RouteServiceProvider.js +14 -4
  35. package/dist/src/core/bootstrap/providers/RouteServiceProvider.js.map +1 -1
  36. package/dist/src/core/bootstrap/providers/ServiceProvider.d.ts +4 -4
  37. package/dist/src/core/bootstrap/providers/ServiceProvider.d.ts.map +1 -1
  38. package/dist/src/core/bootstrap/providers/ServiceProvider.js +5 -5
  39. package/dist/src/core/bootstrap/providers/ServiceProvider.js.map +1 -1
  40. package/dist/src/core/bootstrap/providers/ViewServiceProvider.d.ts +1 -1
  41. package/dist/src/core/bootstrap/providers/ViewServiceProvider.d.ts.map +1 -1
  42. package/dist/src/core/bootstrap/providers/ViewServiceProvider.js +3 -3
  43. package/dist/src/core/bootstrap/providers/ViewServiceProvider.js.map +1 -1
  44. package/dist/src/core/bootstrap/providers/default-providers.d.ts +1 -1
  45. package/dist/src/core/bootstrap/providers/default-providers.d.ts.map +1 -1
  46. package/dist/src/core/bootstrap/providers/default-providers.js +44 -11
  47. package/dist/src/core/bootstrap/providers/default-providers.js.map +1 -1
  48. package/dist/src/core/bootstrap/providers/index.d.ts +9 -9
  49. package/dist/src/core/bootstrap/providers/index.d.ts.map +1 -1
  50. package/dist/src/core/bootstrap/providers/index.js +8 -8
  51. package/dist/src/core/bootstrap/providers/index.js.map +1 -1
  52. package/dist/src/core/bootstrap/providers/provider-order.d.ts +1 -1
  53. package/dist/src/core/bootstrap/providers/provider-order.d.ts.map +1 -1
  54. package/dist/src/core/bootstrap/ssr.d.ts +47 -0
  55. package/dist/src/core/bootstrap/ssr.d.ts.map +1 -0
  56. package/dist/src/core/bootstrap/ssr.js +125 -0
  57. package/dist/src/core/bootstrap/ssr.js.map +1 -0
  58. package/dist/src/core/cache/ViewCache.d.ts +1 -1
  59. package/dist/src/core/cache/ViewCache.d.ts.map +1 -1
  60. package/dist/src/core/cache/index.d.ts +1 -1
  61. package/dist/src/core/cache/index.d.ts.map +1 -1
  62. package/dist/src/core/cache/index.js +1 -1
  63. package/dist/src/core/cache/index.js.map +1 -1
  64. package/dist/src/core/contracts/ApplicationInterface.d.ts +13 -11
  65. package/dist/src/core/contracts/ApplicationInterface.d.ts.map +1 -1
  66. package/dist/src/core/contracts/BlockInterface.d.ts +10 -10
  67. package/dist/src/core/contracts/BlockInterface.d.ts.map +1 -1
  68. package/dist/src/core/contracts/BundleInterface.d.ts +38 -0
  69. package/dist/src/core/contracts/BundleInterface.d.ts.map +1 -0
  70. package/dist/src/core/contracts/BundleInterface.js +2 -0
  71. package/dist/src/core/contracts/BundleInterface.js.map +1 -0
  72. package/dist/src/core/contracts/ComponentInterface.d.ts +7 -6
  73. package/dist/src/core/contracts/ComponentInterface.d.ts.map +1 -1
  74. package/dist/src/core/contracts/ElementInterface.d.ts +91 -27
  75. package/dist/src/core/contracts/ElementInterface.d.ts.map +1 -1
  76. package/dist/src/core/contracts/HeadServiceInterface.d.ts +37 -0
  77. package/dist/src/core/contracts/HeadServiceInterface.d.ts.map +1 -0
  78. package/dist/src/core/contracts/HeadServiceInterface.js +3 -0
  79. package/dist/src/core/contracts/HeadServiceInterface.js.map +1 -0
  80. package/dist/src/core/contracts/HelperInterface.d.ts +9 -1
  81. package/dist/src/core/contracts/HelperInterface.d.ts.map +1 -1
  82. package/dist/src/core/contracts/HttpServiceInterface.d.ts +3 -0
  83. package/dist/src/core/contracts/HttpServiceInterface.d.ts.map +1 -1
  84. package/dist/src/core/contracts/MarkerInterface.d.ts +29 -44
  85. package/dist/src/core/contracts/MarkerInterface.d.ts.map +1 -1
  86. package/dist/src/core/contracts/ReactiveInterface.d.ts +9 -9
  87. package/dist/src/core/contracts/ReactiveInterface.d.ts.map +1 -1
  88. package/dist/src/core/contracts/RouterInterface.d.ts +5 -2
  89. package/dist/src/core/contracts/RouterInterface.d.ts.map +1 -1
  90. package/dist/src/core/contracts/SectionInterface.d.ts +48 -0
  91. package/dist/src/core/contracts/SectionInterface.d.ts.map +1 -0
  92. package/dist/src/core/contracts/SectionInterface.js +2 -0
  93. package/dist/src/core/contracts/SectionInterface.js.map +1 -0
  94. package/dist/src/core/contracts/ServiceProviderInterface.d.ts +3 -3
  95. package/dist/src/core/contracts/ServiceProviderInterface.d.ts.map +1 -1
  96. package/dist/src/core/contracts/ViewControllerInterface.d.ts +116 -15
  97. package/dist/src/core/contracts/ViewControllerInterface.d.ts.map +1 -1
  98. package/dist/src/core/contracts/ViewInterface.d.ts +31 -5
  99. package/dist/src/core/contracts/ViewInterface.d.ts.map +1 -1
  100. package/dist/src/core/contracts/ViewManagerInterface.d.ts +47 -5
  101. package/dist/src/core/contracts/ViewManagerInterface.d.ts.map +1 -1
  102. package/dist/src/core/contracts/ViewStateInterface.d.ts +39 -2
  103. package/dist/src/core/contracts/ViewStateInterface.d.ts.map +1 -1
  104. package/dist/src/core/contracts/common.d.ts +3 -3
  105. package/dist/src/core/contracts/common.js +3 -3
  106. package/dist/src/core/contracts/utils.d.ts +19 -19
  107. package/dist/src/core/contracts/utils.d.ts.map +1 -1
  108. package/dist/src/core/contracts/views.d.ts +1 -22
  109. package/dist/src/core/contracts/views.d.ts.map +1 -1
  110. package/dist/src/core/devtools/hook.d.ts +66 -0
  111. package/dist/src/core/devtools/hook.d.ts.map +1 -0
  112. package/dist/src/core/devtools/hook.js +114 -0
  113. package/dist/src/core/devtools/hook.js.map +1 -0
  114. package/dist/src/core/devtools/index.d.ts +34 -0
  115. package/dist/src/core/devtools/index.d.ts.map +1 -0
  116. package/dist/src/core/devtools/index.js +33 -0
  117. package/dist/src/core/devtools/index.js.map +1 -0
  118. package/dist/src/core/devtools/inspector.d.ts +31 -0
  119. package/dist/src/core/devtools/inspector.d.ts.map +1 -0
  120. package/dist/src/core/devtools/inspector.js +249 -0
  121. package/dist/src/core/devtools/inspector.js.map +1 -0
  122. package/dist/src/core/elements/Block.d.ts +9 -7
  123. package/dist/src/core/elements/Block.d.ts.map +1 -1
  124. package/dist/src/core/elements/Block.js +33 -9
  125. package/dist/src/core/elements/Block.js.map +1 -1
  126. package/dist/src/core/elements/BlockOutlet.d.ts +19 -8
  127. package/dist/src/core/elements/BlockOutlet.d.ts.map +1 -1
  128. package/dist/src/core/elements/BlockOutlet.js +57 -13
  129. package/dist/src/core/elements/BlockOutlet.js.map +1 -1
  130. package/dist/src/core/elements/Component.d.ts +83 -11
  131. package/dist/src/core/elements/Component.d.ts.map +1 -1
  132. package/dist/src/core/elements/Component.js +334 -13
  133. package/dist/src/core/elements/Component.js.map +1 -1
  134. package/dist/src/core/elements/ForeachSlotCache.d.ts +95 -0
  135. package/dist/src/core/elements/ForeachSlotCache.d.ts.map +1 -0
  136. package/dist/src/core/elements/ForeachSlotCache.js +149 -0
  137. package/dist/src/core/elements/ForeachSlotCache.js.map +1 -0
  138. package/dist/src/core/elements/Fragment.d.ts +19 -13
  139. package/dist/src/core/elements/Fragment.d.ts.map +1 -1
  140. package/dist/src/core/elements/Fragment.js +43 -20
  141. package/dist/src/core/elements/Fragment.js.map +1 -1
  142. package/dist/src/core/elements/Html.d.ts +89 -12
  143. package/dist/src/core/elements/Html.d.ts.map +1 -1
  144. package/dist/src/core/elements/Html.js +707 -106
  145. package/dist/src/core/elements/Html.js.map +1 -1
  146. package/dist/src/core/elements/Output.d.ts +34 -10
  147. package/dist/src/core/elements/Output.d.ts.map +1 -1
  148. package/dist/src/core/elements/Output.js +141 -26
  149. package/dist/src/core/elements/Output.js.map +1 -1
  150. package/dist/src/core/elements/Reactive.d.ts +85 -14
  151. package/dist/src/core/elements/Reactive.d.ts.map +1 -1
  152. package/dist/src/core/elements/Reactive.js +315 -41
  153. package/dist/src/core/elements/Reactive.js.map +1 -1
  154. package/dist/src/core/elements/TextElement.d.ts +11 -7
  155. package/dist/src/core/elements/TextElement.d.ts.map +1 -1
  156. package/dist/src/core/elements/TextElement.js +18 -11
  157. package/dist/src/core/elements/TextElement.js.map +1 -1
  158. package/dist/src/core/elements/Wrapper.d.ts +24 -13
  159. package/dist/src/core/elements/Wrapper.d.ts.map +1 -1
  160. package/dist/src/core/elements/Wrapper.js +65 -8
  161. package/dist/src/core/elements/Wrapper.js.map +1 -1
  162. package/dist/src/core/elements/Yield.d.ts +10 -9
  163. package/dist/src/core/elements/Yield.d.ts.map +1 -1
  164. package/dist/src/core/elements/Yield.js +31 -19
  165. package/dist/src/core/elements/Yield.js.map +1 -1
  166. package/dist/src/core/elements/helpers.d.ts +7 -7
  167. package/dist/src/core/elements/helpers.d.ts.map +1 -1
  168. package/dist/src/core/elements/helpers.js +4 -4
  169. package/dist/src/core/elements/index.d.ts +12 -11
  170. package/dist/src/core/elements/index.d.ts.map +1 -1
  171. package/dist/src/core/elements/index.js +12 -11
  172. package/dist/src/core/elements/index.js.map +1 -1
  173. package/dist/src/core/elements/reconcileChildren.d.ts +7 -0
  174. package/dist/src/core/elements/reconcileChildren.d.ts.map +1 -0
  175. package/dist/src/core/elements/reconcileChildren.js +74 -0
  176. package/dist/src/core/elements/reconcileChildren.js.map +1 -0
  177. package/dist/src/core/hellpers/utils.d.ts +2 -2
  178. package/dist/src/core/hellpers/utils.js +4 -4
  179. package/dist/src/core/helpers/ApiClient.d.ts +58 -0
  180. package/dist/src/core/helpers/ApiClient.d.ts.map +1 -0
  181. package/dist/src/core/helpers/ApiClient.js +131 -0
  182. package/dist/src/core/helpers/ApiClient.js.map +1 -0
  183. package/dist/src/core/helpers/app.d.ts +3 -0
  184. package/dist/src/core/helpers/app.d.ts.map +1 -0
  185. package/dist/src/core/helpers/app.js +63 -0
  186. package/dist/src/core/helpers/app.js.map +1 -0
  187. package/dist/src/core/helpers/transition.d.ts +34 -0
  188. package/dist/src/core/helpers/transition.d.ts.map +1 -0
  189. package/dist/src/core/helpers/transition.js +118 -0
  190. package/dist/src/core/helpers/transition.js.map +1 -0
  191. package/dist/src/core/helpers/utils.d.ts +26 -0
  192. package/dist/src/core/helpers/utils.d.ts.map +1 -0
  193. package/dist/src/core/helpers/utils.js +96 -0
  194. package/dist/src/core/helpers/utils.js.map +1 -0
  195. package/dist/src/core/helpers/view.d.ts +39 -0
  196. package/dist/src/core/helpers/view.d.ts.map +1 -0
  197. package/dist/src/core/helpers/view.js +286 -0
  198. package/dist/src/core/helpers/view.js.map +1 -0
  199. package/dist/src/core/routers/Router.d.ts +108 -7
  200. package/dist/src/core/routers/Router.d.ts.map +1 -1
  201. package/dist/src/core/routers/Router.js +444 -68
  202. package/dist/src/core/routers/Router.js.map +1 -1
  203. package/dist/src/core/routers/index.d.ts +2 -2
  204. package/dist/src/core/routers/index.d.ts.map +1 -1
  205. package/dist/src/core/routers/index.js +1 -1
  206. package/dist/src/core/routers/index.js.map +1 -1
  207. package/dist/src/core/services/AssetManager.d.ts +112 -0
  208. package/dist/src/core/services/AssetManager.d.ts.map +1 -0
  209. package/dist/src/core/services/AssetManager.js +361 -0
  210. package/dist/src/core/services/AssetManager.js.map +1 -0
  211. package/dist/src/core/services/BlockManager.d.ts +73 -2
  212. package/dist/src/core/services/BlockManager.d.ts.map +1 -1
  213. package/dist/src/core/services/BlockManager.js +270 -48
  214. package/dist/src/core/services/BlockManager.js.map +1 -1
  215. package/dist/src/core/services/DomService.js +3 -3
  216. package/dist/src/core/services/DomService.js.map +1 -1
  217. package/dist/src/core/services/HeadService.d.ts +24 -0
  218. package/dist/src/core/services/HeadService.d.ts.map +1 -0
  219. package/dist/src/core/services/HeadService.js +120 -0
  220. package/dist/src/core/services/HeadService.js.map +1 -0
  221. package/dist/src/core/services/HelperService.d.ts +14 -3
  222. package/dist/src/core/services/HelperService.d.ts.map +1 -1
  223. package/dist/src/core/services/HelperService.js +32 -2
  224. package/dist/src/core/services/HelperService.js.map +1 -1
  225. package/dist/src/core/services/HttpService.d.ts +8 -0
  226. package/dist/src/core/services/HttpService.d.ts.map +1 -1
  227. package/dist/src/core/services/HttpService.js +121 -47
  228. package/dist/src/core/services/HttpService.js.map +1 -1
  229. package/dist/src/core/services/MarkerRegistry.d.ts +51 -8
  230. package/dist/src/core/services/MarkerRegistry.d.ts.map +1 -1
  231. package/dist/src/core/services/MarkerRegistry.js +135 -14
  232. package/dist/src/core/services/MarkerRegistry.js.map +1 -1
  233. package/dist/src/core/services/PageCache.d.ts +101 -0
  234. package/dist/src/core/services/PageCache.d.ts.map +1 -0
  235. package/dist/src/core/services/PageCache.js +184 -0
  236. package/dist/src/core/services/PageCache.js.map +1 -0
  237. package/dist/src/core/services/SectionManager.d.ts +100 -0
  238. package/dist/src/core/services/SectionManager.d.ts.map +1 -0
  239. package/dist/src/core/services/SectionManager.js +408 -0
  240. package/dist/src/core/services/SectionManager.js.map +1 -0
  241. package/dist/src/core/services/StorageService.js +1 -1
  242. package/dist/src/core/services/TranslationService.d.ts +21 -0
  243. package/dist/src/core/services/TranslationService.d.ts.map +1 -0
  244. package/dist/src/core/services/TranslationService.js +95 -0
  245. package/dist/src/core/services/TranslationService.js.map +1 -0
  246. package/dist/src/core/services/index.d.ts +11 -8
  247. package/dist/src/core/services/index.d.ts.map +1 -1
  248. package/dist/src/core/services/index.js +11 -8
  249. package/dist/src/core/services/index.js.map +1 -1
  250. package/dist/src/core/types/utils.d.ts +8 -8
  251. package/dist/src/core/types/utils.d.ts.map +1 -1
  252. package/dist/src/core/view/LoopContext.d.ts +21 -1
  253. package/dist/src/core/view/LoopContext.d.ts.map +1 -1
  254. package/dist/src/core/view/LoopContext.js +36 -0
  255. package/dist/src/core/view/LoopContext.js.map +1 -1
  256. package/dist/src/core/view/ResourceScope.d.ts +38 -0
  257. package/dist/src/core/view/ResourceScope.d.ts.map +1 -0
  258. package/dist/src/core/view/ResourceScope.js +167 -0
  259. package/dist/src/core/view/ResourceScope.js.map +1 -0
  260. package/dist/src/core/view/Section.d.ts +17 -32
  261. package/dist/src/core/view/Section.d.ts.map +1 -1
  262. package/dist/src/core/view/Section.js +14 -55
  263. package/dist/src/core/view/Section.js.map +1 -1
  264. package/dist/src/core/view/View.d.ts +43 -7
  265. package/dist/src/core/view/View.d.ts.map +1 -1
  266. package/dist/src/core/view/View.js +22 -4
  267. package/dist/src/core/view/View.js.map +1 -1
  268. package/dist/src/core/view/ViewController.d.ts +313 -40
  269. package/dist/src/core/view/ViewController.d.ts.map +1 -1
  270. package/dist/src/core/view/ViewController.js +1002 -147
  271. package/dist/src/core/view/ViewController.js.map +1 -1
  272. package/dist/src/core/view/ViewManager.d.ts +318 -27
  273. package/dist/src/core/view/ViewManager.d.ts.map +1 -1
  274. package/dist/src/core/view/ViewManager.js +1199 -258
  275. package/dist/src/core/view/ViewManager.js.map +1 -1
  276. package/dist/src/core/view/ViewState.d.ts +185 -6
  277. package/dist/src/core/view/ViewState.d.ts.map +1 -1
  278. package/dist/src/core/view/ViewState.js +851 -123
  279. package/dist/src/core/view/ViewState.js.map +1 -1
  280. package/dist/src/core/view/index.d.ts +5 -5
  281. package/dist/src/core/view/index.d.ts.map +1 -1
  282. package/dist/src/core/view/index.js +5 -5
  283. package/dist/src/core/view/index.js.map +1 -1
  284. package/dist/src/testing/index.d.ts +69 -0
  285. package/dist/src/testing/index.d.ts.map +1 -0
  286. package/dist/src/testing/index.js +140 -0
  287. package/dist/src/testing/index.js.map +1 -0
  288. package/index.ts +29 -5
  289. package/package.json +30 -21
  290. package/plugins.ts +2 -2
  291. package/src/core/app/Application.ts +58 -31
  292. package/src/core/bootstrap/app.ts +80 -6
  293. package/src/core/bootstrap/bundle.ts +101 -0
  294. package/src/core/bootstrap/providers/ApiServiceProvider.ts +4 -4
  295. package/src/core/bootstrap/providers/CoreServiceProvider.ts +14 -8
  296. package/src/core/bootstrap/providers/HelperServiceProvider.ts +8 -3
  297. package/src/core/bootstrap/providers/RouteServiceProvider.ts +15 -4
  298. package/src/core/bootstrap/providers/ServiceProvider.ts +8 -8
  299. package/src/core/bootstrap/providers/ViewServiceProvider.ts +3 -3
  300. package/src/core/bootstrap/providers/default-providers.ts +51 -16
  301. package/src/core/bootstrap/providers/index.ts +9 -9
  302. package/src/core/bootstrap/providers/provider-order.test.ts +16 -27
  303. package/src/core/bootstrap/providers/provider-order.ts +1 -1
  304. package/src/core/bootstrap/ssr.ts +136 -0
  305. package/src/core/cache/ViewCache.ts +1 -1
  306. package/src/core/cache/index.ts +1 -1
  307. package/src/core/contracts/ApplicationInterface.ts +13 -11
  308. package/src/core/contracts/BlockInterface.ts +18 -18
  309. package/src/core/contracts/BundleInterface.ts +40 -0
  310. package/src/core/contracts/ComponentInterface.ts +7 -6
  311. package/src/core/contracts/ElementInterface.ts +95 -27
  312. package/src/core/contracts/HeadServiceInterface.ts +41 -0
  313. package/src/core/contracts/HelperInterface.ts +9 -1
  314. package/src/core/contracts/HttpServiceInterface.ts +3 -0
  315. package/src/core/contracts/MarkerInterface.ts +22 -49
  316. package/src/core/contracts/ReactiveInterface.ts +9 -9
  317. package/src/core/contracts/RouterInterface.ts +5 -2
  318. package/src/core/contracts/SectionInterface.ts +49 -0
  319. package/src/core/contracts/ServiceProviderInterface.ts +3 -3
  320. package/src/core/contracts/ViewControllerInterface.ts +124 -16
  321. package/src/core/contracts/ViewInterface.ts +34 -5
  322. package/src/core/contracts/ViewManagerInterface.ts +40 -5
  323. package/src/core/contracts/ViewStateInterface.ts +39 -2
  324. package/src/core/contracts/common.ts +3 -3
  325. package/src/core/contracts/utils.ts +19 -19
  326. package/src/core/contracts/views.ts +2 -22
  327. package/src/core/devtools/hook.ts +142 -0
  328. package/src/core/devtools/index.ts +38 -0
  329. package/src/core/devtools/inspector.ts +270 -0
  330. package/src/core/elements/Block.ts +43 -21
  331. package/src/core/elements/BlockOutlet.ts +61 -21
  332. package/src/core/elements/Component.ts +352 -18
  333. package/src/core/elements/ElementManager.ts +9 -9
  334. package/src/core/elements/ForeachSlotCache.ts +170 -0
  335. package/src/core/elements/Fragment.ts +50 -32
  336. package/src/core/elements/Html.ts +696 -117
  337. package/src/core/elements/Output.ts +147 -30
  338. package/src/core/elements/Reactive.ts +329 -52
  339. package/src/core/elements/TextElement.ts +25 -16
  340. package/src/core/elements/Wrapper.ts +84 -18
  341. package/src/core/elements/Yield.ts +35 -24
  342. package/src/core/elements/helpers.ts +12 -12
  343. package/src/core/elements/index.ts +25 -24
  344. package/src/core/elements/reconcileChildren.ts +70 -0
  345. package/src/core/{hellpers → helpers}/ApiClient.ts +8 -1
  346. package/src/core/{hellpers → helpers}/app.ts +13 -2
  347. package/src/core/helpers/transition.ts +127 -0
  348. package/src/core/{hellpers → helpers}/utils.ts +4 -4
  349. package/src/core/helpers/view.ts +283 -0
  350. package/src/core/routers/Router.ts +458 -64
  351. package/src/core/routers/index.ts +2 -2
  352. package/src/core/services/AssetManager.ts +387 -0
  353. package/src/core/services/BlockManager.ts +272 -50
  354. package/src/core/services/DomService.ts +3 -3
  355. package/src/core/services/HeadService.ts +124 -0
  356. package/src/core/services/HelperService.ts +34 -4
  357. package/src/core/services/HttpService.ts +116 -55
  358. package/src/core/services/MarkerRegistry.ts +142 -16
  359. package/src/core/services/PageCache.ts +239 -0
  360. package/src/core/services/SectionManager.ts +387 -0
  361. package/src/core/services/StorageService.ts +1 -1
  362. package/src/core/services/TranslationService.ts +91 -0
  363. package/src/core/services/index.ts +11 -8
  364. package/src/core/types/utils.ts +8 -8
  365. package/src/core/view/LoopContext.ts +38 -1
  366. package/src/core/view/ResourceScope.ts +174 -0
  367. package/src/core/view/Section.ts +21 -68
  368. package/src/core/view/View.ts +57 -8
  369. package/src/core/view/ViewController.ts +1070 -186
  370. package/src/core/view/ViewManager.ts +1346 -272
  371. package/src/core/view/ViewState.ts +834 -116
  372. package/src/core/view/index.ts +5 -5
  373. package/src/testing/index.ts +184 -0
  374. package/dist/src/core/services/MarkerService.d.ts +0 -299
  375. package/dist/src/core/services/MarkerService.d.ts.map +0 -1
  376. package/dist/src/core/services/MarkerService.js +0 -723
  377. package/dist/src/core/services/MarkerService.js.map +0 -1
  378. package/src/core/services/MarkerService.ts +0 -923
@@ -17,22 +17,39 @@
17
17
  * - Section system → Block/BlockOutlet
18
18
  */
19
19
 
20
- import type { ApplicationInterface } from "../contracts/ApplicationInterface";
21
- import { ActiveRouteInterface, RouterNavigationType } from "../contracts/RouterInterface";
22
- import { FragmentInterface, HtmlInterface } from "../contracts/utils";
23
- import type { ViewControllerConfig, ViewControllerInterface } from "../contracts/ViewControllerInterface";
24
- import type { ViewInterface } from "../contracts/ViewInterface";
25
- import type { ViewManagerInterface, ActiveViewInfo } from "../contracts/ViewManagerInterface";
26
- import { BlockManager, BlockManagerService } from "../services/BlockManager";
27
- import { Html } from "../elements/Html";
28
- import { hasData } from "../hellpers/utils";
29
- import logger from "../services/LoggerService";
30
- import { StoreService } from "../services/StoreService";
31
- import { View } from "./View";
32
- import { InitMode, InitModes } from "../contracts/common";
33
- import { OOTEnum } from "../types/utils";
34
- import { WrapperInterface } from "../contracts/ElementInterface";
35
- import { app } from "../hellpers/app";
20
+ import type { ApplicationInterface } from "../contracts/ApplicationInterface.js";
21
+ import { ActiveRouteInterface, RouterNavigationType } from "../contracts/RouterInterface.js";
22
+ import { FragmentInterface, HtmlInterface } from "../contracts/utils.js";
23
+ import type { ViewControllerConfig, ViewControllerInterface } from "../contracts/ViewControllerInterface.js";
24
+ import type { ViewInterface } from "../contracts/ViewInterface.js";
25
+ import type { ViewManagerInterface, ActiveViewInfo } from "../contracts/ViewManagerInterface.js";
26
+ import { BlockManager, BlockManagerService } from "../services/BlockManager.js";
27
+ import { SectionManager, SectionManagerService } from "../services/SectionManager.js";
28
+ import devtools from "../devtools/hook.js";
29
+ import { BlockOutlet } from "../elements/BlockOutlet.js";
30
+ import { PageCacheService, PageCacheEntry, detachWrapperDOM } from "../services/PageCache.js";
31
+ import { Html } from "../elements/Html.js";
32
+ import { hasData } from "../helpers/utils.js";
33
+ import { activateView, claimHydratedView, commitView, flushView } from "../helpers/view.js";
34
+ import markerRegistry from "../services/MarkerRegistry.js";
35
+ import logger from "../services/LoggerService.js";
36
+ import { StoreService } from "../services/StoreService.js";
37
+ import { View } from "./View.js";
38
+ import { InitMode, InitModes } from "../contracts/common.js";
39
+ import { OOTEnum } from "../types/utils.js";
40
+ import { WrapperInterface } from "../contracts/ElementInterface.js";
41
+ import { app } from "../helpers/app.js";
42
+
43
+ /**
44
+ * SSR boot info — server nhúng sau khi render xong (RUNTIME_CONTRACT §6).
45
+ * `view` = registry path của page entry, `viewId` = id server đã dùng để prefix
46
+ * markers/classes của page. viewId của layout chain được client discover từ DOM.
47
+ */
48
+ export type SSRBootInfo = {
49
+ view: string;
50
+ viewId: string;
51
+ container?: string;
52
+ };
36
53
 
37
54
  type RenderPageViewSuccess = {
38
55
  type: 'success';
@@ -41,6 +58,8 @@ type RenderPageViewSuccess = {
41
58
  result: ViewInterface | unknown;
42
59
  superView: ViewInterface | null;
43
60
  finalView: ViewInterface;
61
+ /** Origin → outermost: [page, inner layout, ..., root layout]. */
62
+ chain: ViewInterface[];
44
63
  };
45
64
 
46
65
  type RenderPageViewError = {
@@ -50,12 +69,23 @@ type RenderPageViewError = {
50
69
  result: null;
51
70
  superView: null;
52
71
  finalView: ViewInterface;
72
+ chain: ViewInterface[];
73
+ };
74
+
75
+ type RenderPageViewCancelled = {
76
+ type: 'cancelled';
77
+ message: string;
78
+ view: ViewInterface;
79
+ result: null;
80
+ superView: null;
81
+ finalView: ViewInterface;
82
+ chain: ViewInterface[];
53
83
  };
54
84
 
55
- type RenderPageViewResult = RenderPageViewSuccess | RenderPageViewError;
85
+ type RenderPageViewResult = RenderPageViewSuccess | RenderPageViewError | RenderPageViewCancelled;
56
86
 
57
- function isRenderableObject(result: unknown): result is { oneType: string } {
58
- return typeof result === 'object' && result !== null && 'oneType' in result;
87
+ function isRenderableObject(result: unknown): result is { saoType: string } {
88
+ return typeof result === 'object' && result !== null && 'saoType' in result;
59
89
  }
60
90
 
61
91
  export class ViewManager implements ViewManagerInterface {
@@ -63,15 +93,16 @@ export class ViewManager implements ViewManagerInterface {
63
93
  private App: ApplicationInterface | null = null;
64
94
 
65
95
  private systemData: Record<string, any> = {}; // For internal use, not exposed to views
96
+ private contextRevision: string | null = null;
97
+ private contextViews: string | null = null;
66
98
 
67
99
  /** ROOT DOM container where views mount */
68
100
  private container: HTMLElement | null = null;
69
- private rootElement: Html | null = null; // Html wrapper for the root container
101
+ private rootElement: HtmlInterface | null = null; // Html wrapper for the root container
70
102
  /** View module registry: name → factory or async loader */
71
103
  private viewRegistry: Record<string, ((...args: any[]) => any) | (() => Promise<any>)> = {};
72
104
 
73
105
  /** Currently mounted views (keyed by path) */
74
- private activeViews: Map<string, ActiveViewInfo> = new Map();
75
106
 
76
107
  /** The outermost active view (layout or page) */
77
108
  private currentView: ActiveViewInfo | null = null;
@@ -80,14 +111,28 @@ export class ViewManager implements ViewManagerInterface {
80
111
  private currentLayoutPath: string | null = null;
81
112
 
82
113
  private currentLayoutView: ViewInterface | null = null; // Store the current layout view instance for reuse
114
+ /** Mounted layouts ordered outermost → innermost. */
115
+ private currentLayoutChain: ViewInterface[] = [];
83
116
  private currentPageView: ViewInterface | null = null; // Store the current page view instance for reference in blocks and sections
84
117
  private currentViewType: 'view' | 'layout' | null = null; // Track whether the current view is a page or layout for correct lifecycle handling
85
118
 
119
+ /**
120
+ * SSR boot info — view entry (page) + viewId server đã render. Set ở init()
121
+ * từ config.ssr (đọc từ DOM lúc boot). Router consume 1 lần cho route đầu
122
+ * tiên → hydrateView; các route sau là CSR (SPA takeover).
123
+ */
124
+ private ssrBoot: SSRBootInfo | null = null;
125
+ /** Standalone SPA: Laravel data requests may live on a different host. */
126
+ private dataEndpoint: string | null = null;
127
+ private fetchOptions: Record<string, any> = {};
128
+
129
+ /** Exact Page/Layout instance relationships exported by Blade for hydration. */
130
+ private ssrViewData: Record<string, any> = {};
131
+
86
132
 
87
133
  /** Current layout view info — reused if same layout */
88
134
  private currentLayout: ActiveViewInfo | null = null;
89
135
 
90
- private cachedLayouts: Map<string, ViewInterface> = new Map(); // Cache for previously mounted layouts
91
136
 
92
137
  /** All views in the current mount chain (outermost → innermost) */
93
138
  private viewStack: ViewInterface[] = [];
@@ -98,18 +143,82 @@ export class ViewManager implements ViewManagerInterface {
98
143
  /** Render counter for debugging */
99
144
  private renderCount = 0;
100
145
 
146
+ /** Invalidates fire-and-forget render work when a newer navigation begins. */
147
+ /**
148
+ * Số lượt render `@await` đang bay.
149
+ *
150
+ * Nhánh "có prerender" cố ý fire-and-forget: nó trả skeleton về NGAY rồi
151
+ * mới fetch, nên người gọi không có promise nào để đợi và không có cách
152
+ * nào biết nội dung thật đã vào chưa. Đếm ở đây để {@link isSettled} trả
153
+ * lời được câu đó — công cụ đo, chụp ảnh trang, hay test parity SSR↔CSR
154
+ * đều cần một tín hiệu THẬT thay vì đoán bằng "DOM đứng yên": trang đang
155
+ * chờ dữ liệu thì DOM cũng đứng yên y như đã xong.
156
+ */
157
+ private pendingAsyncRenders = 0;
158
+
159
+ /** Không còn render `@await` nào đang bay — nội dung thật đã vào DOM. */
160
+ get isSettled(): boolean {
161
+ return this.pendingAsyncRenders === 0;
162
+ }
163
+
164
+ private navigationGeneration = 0;
165
+
101
166
  public store: StoreService = StoreService.instance("ViewManager");
102
167
 
103
168
  public blockManager: BlockManagerService = BlockManager;
169
+ public sectionManager: SectionManagerService = SectionManager;
104
170
 
105
171
  constructor(app?: ApplicationInterface) {
106
172
  if (app) this.App = app;
173
+
174
+ // Layout instance sống ở CẢ HAI nơi: PageCache (DOM detached) + store
175
+ // (để extendView trả lại đúng instance). Entry bị evict/expire → instance
176
+ // đã destroy → PHẢI gỡ khỏi store, nếu không extendView trả instance chết.
177
+ this.pageCache.onEvict = (entry) => {
178
+ for (const v of entry.views) {
179
+ const path = v.__ctrl__?.path;
180
+ if (path && this.store.has(path) && this.store.get(path) === v) {
181
+ this.store.remove?.(path);
182
+ }
183
+ }
184
+ };
107
185
  }
186
+ /**
187
+ * Kiểm tra một view có đang mount (active) không.
188
+ * Dùng để guard duplicate mount hoặc kiểm tra trạng thái từ bên ngoài.
189
+ */
108
190
  isViewMounted(path: string): boolean {
109
- throw new Error("Method not implemented.");
191
+ if (this.currentPageView?.__ctrl__.path === path) return true;
192
+ return this.currentLayoutChain.some(v => v.__ctrl__.path === path);
110
193
  }
194
+
195
+ /** Invalidate async render/fetch work owned by the current navigation. */
196
+ cancelNavigation(): void {
197
+ this.navigationGeneration++;
198
+ }
199
+
200
+ /**
201
+ * Destroy ViewManager hoàn toàn — dọn sạch mọi view, DOM, state.
202
+ * Gọi khi teardown app (hot reload, test cleanup, unmount root).
203
+ */
111
204
  destroy(): void {
112
- throw new Error("Method not implemented.");
205
+ this.navigationGeneration++;
206
+ // 1. Stop và destroy tất cả views đang active
207
+ this.unmountAll();
208
+ // 2. Dọn PageCache
209
+ this.pageCache.clear();
210
+ // 3. Reset registry và internal state
211
+ this.viewRegistry = {};
212
+ this.rootElement = null;
213
+ this.container = null;
214
+ this._isInitialized = false;
215
+ this.renderCount = 0;
216
+ this.store.clear();
217
+ // MarkerRegistry là singleton TOÀN CỤC ở tầng module — không thuộc view
218
+ // nào nên không teardown nào khác chạm tới. Teardown app (hot reload,
219
+ // unmount root, dọn giữa các test) là chỗ duy nhất dọn được nó.
220
+ markerRegistry.clear();
221
+ logger.info('[ViewManager] destroyed.');
113
222
  }
114
223
 
115
224
 
@@ -160,7 +269,28 @@ export class ViewManager implements ViewManagerInterface {
160
269
  /**
161
270
  * Initialize the ViewManager.
162
271
  */
163
- init(config?: { container?: HTMLElement | string; registry?: Record<string, any> }): void {
272
+ init(config?: { container?: HTMLElement | string; registry?: Record<string, any>; ssr?: SSRBootInfo | null; systemData?: Record<string, any>; ssrData?: Record<string, any>; revision?: string; contextViews?: string; dataEndpoint?: string; fetchOptions?: Record<string, any> }): void {
273
+ if (typeof config?.dataEndpoint === 'string') this.dataEndpoint = config.dataEndpoint;
274
+ if (config?.fetchOptions) this.fetchOptions = { ...config.fetchOptions };
275
+ // SSR boot info (server-rendered): chứa view entry + viewId để route đầu
276
+ // tiên gọi hydrateView thay vì mountView. Xem RUNTIME_CONTRACT §6 (boot).
277
+ if (config?.ssr && config.ssr.view && config.ssr.viewId) {
278
+ this.ssrBoot = config.ssr;
279
+ }
280
+ // systemData (server __layout__/__base__/__page__/...): spread vào mọi
281
+ // view factory ở view() để compiled view resolve superView/namespace.
282
+ if (config?.systemData && typeof config.systemData === 'object') {
283
+ this.systemData = { ...this.systemData, ...config.systemData };
284
+ }
285
+ if (typeof config?.revision === 'string') {
286
+ this.contextRevision = config.revision;
287
+ }
288
+ if (typeof config?.contextViews === 'string') {
289
+ this.contextViews = config.contextViews;
290
+ }
291
+ if (config?.ssrData && typeof config.ssrData === 'object') {
292
+ this.ssrViewData = config.ssrData;
293
+ }
164
294
  if (config?.container) {
165
295
  if (typeof config.container === 'string') {
166
296
  const found = document.querySelector(config.container);
@@ -181,58 +311,187 @@ export class ViewManager implements ViewManagerInterface {
181
311
  ctx: this,
182
312
  tagName: this.container.tagName.toLowerCase(),
183
313
  element: this.container,
184
- initMode: 'hydrate',
314
+ initMode: InitModes.HYDRATE,
315
+ childrenFactory: () => [],
185
316
  });
186
317
  if (config?.registry) {
187
318
  this.setViewRegistry(config.registry);
188
319
  }
189
320
  this._isInitialized = true;
321
+ devtools.attach(this);
190
322
  }
191
323
 
324
+ /**
325
+ * Phương án CUỐI khi không error boundary nào xử lý (xem ViewController.onError):
326
+ * ghi đè cả container. Dựng bằng DOM API + textContent — message/details có thể
327
+ * mang nội dung từ server/URL, nội suy vào innerHTML là đường tiêm HTML.
328
+ */
192
329
  showError(message: string, details?: any) {
193
330
  logger.error(message, details);
194
- if (this.container) {
195
- this.container.innerHTML = `<div style="padding: 20px; background: #fee; color: #900; border: 1px solid #900;">
196
- <h2>Error</h2>
197
- <p>${message}</p>
198
- ${details ? `<pre style="white-space: pre-wrap; background: #fdd; padding: 10px; border: 1px solid #900;">${JSON.stringify(details, null, 2)}</pre>` : ''}
199
- </div>`;
331
+ if (!this.container) return;
332
+
333
+ const box = document.createElement('div');
334
+ box.style.cssText = 'padding:20px;background:#fee;color:#900;border:1px solid #900';
335
+
336
+ const title = document.createElement('h2');
337
+ title.textContent = 'Error';
338
+ box.appendChild(title);
339
+
340
+ const text = document.createElement('p');
341
+ text.textContent = message;
342
+ box.appendChild(text);
343
+
344
+ if (details) {
345
+ const pre = document.createElement('pre');
346
+ pre.style.cssText = 'white-space:pre-wrap;background:#fdd;padding:10px;border:1px solid #900';
347
+ let serialized: string;
348
+ try {
349
+ serialized = JSON.stringify(details, null, 2) ?? String(details);
350
+ } catch {
351
+ serialized = String(details); // circular / getter throw
352
+ }
353
+ pre.textContent = serialized;
354
+ box.appendChild(pre);
200
355
  }
356
+
357
+ this.container.replaceChildren(box);
201
358
  }
202
359
 
203
360
  // ─── View Loading ───────────────────────────────────────────
361
+ /**
362
+ * Khoá registry thật sự dùng cho `name`.
363
+ *
364
+ * Server có ThemeAwareViewFinder nên một view theme không đè vẫn render
365
+ * được từ base. Client không có đường đó: `__layout__` là tiền tố của CẢ
366
+ * context, nên `@extends(__layout__ + "workspace")` sinh khoá
367
+ * `themes.{slug}.layouts.workspace` dù registry chỉ có bản của base.
368
+ * Server gửi kèm cặp `__view_fallback_from__/to__` trong systemData; ở đây
369
+ * chỉ đổi tiền tố khi khoá gốc không có.
370
+ */
371
+ private resolveRegistryKey(name: string): string {
372
+ if (Object.prototype.hasOwnProperty.call(this.viewRegistry, name)) return name;
373
+
374
+ const from = (this.systemData as any)?.__view_fallback_from__;
375
+ const to = (this.systemData as any)?.__view_fallback_to__;
376
+ if (typeof from !== 'string' || typeof to !== 'string' || !from || !to) return name;
377
+
378
+ const rebased = name === from
379
+ ? to
380
+ : (name.startsWith(from + '.') ? to + name.slice(from.length) : name);
381
+
382
+ return Object.prototype.hasOwnProperty.call(this.viewRegistry, rebased) ? rebased : name;
383
+ }
384
+
204
385
  hasView(name: string): boolean {
205
- return Object.prototype.hasOwnProperty.call(this.viewRegistry, name);
386
+ return Object.prototype.hasOwnProperty.call(this.viewRegistry, this.resolveRegistryKey(name));
206
387
  }
207
388
 
208
389
  exists(name: string): boolean {
209
390
  return this.hasView(name);
210
391
  }
211
392
 
212
- view(name: string, data: Record<string, any>, cache: boolean): any {
393
+ /**
394
+ * generateViewId — tạo unique ID cho mỗi view instance.
395
+ *
396
+ * Compiled output gọi:
397
+ * const __VIEW_ID__ = __data__.__SSR_VIEW_ID__ || App.View.generateViewId();
398
+ *
399
+ * Dùng trong constructor của compiled View class để gán viewId
400
+ * (tránh hai instance cùng view path dùng chung ID gây clobber registry).
401
+ */
402
+ generateViewId(): string {
403
+ return `c${(this.viewSeq++).toString(36)}`;
404
+ }
405
+
406
+ /**
407
+ * Bộ đếm cho view do CLIENT tạo (CSR / điều hướng SPA).
408
+ *
409
+ * Tiền tố 'c' — server dùng 'v' (ViewContextServiceProvider) — nên hai nguồn
410
+ * id không thể trùng nhau theo cấu trúc, không phải nhờ may mắn ngẫu nhiên.
411
+ * View hydrate từ SSR KHÔNG đi qua đây: nó nhận thẳng __SSR_VIEW_ID__ của
412
+ * server, nên hai phía luôn khớp.
413
+ */
414
+ private viewSeq = 0;
415
+
416
+ /** Factory đã unwrap của các view lazy — tránh await + unwrap lại mỗi lần navigate. */
417
+ private resolvedFactories: Map<string, (...args: any[]) => any> = new Map();
418
+
419
+ /**
420
+ * Tạo View instance. Hỗ trợ registry lazy (`() => import('./x.js')`) — dùng
421
+ * cho view cấp ROUTE (mountView/hydrateView). `@include`/`@extends` chạy
422
+ * trong render tree đồng bộ nên dùng `resolveViewSync()`.
423
+ */
424
+ async view(name: string, data: Record<string, any>, cache: boolean): Promise<any> {
213
425
  try {
214
- if (cache && this.store.has(name)) {
215
- const cachedView = this.store.get<View>(name);
216
- if (hasData(data)) {
217
- cachedView?.__ctrl__.updateData(data);
426
+ // Khoá store phải là khoá registry ĐÃ RESOLVE, không phải tên được
427
+ // yêu cầu. Khi theme đang bật, `__layout__` là tiền tố của cả context
428
+ // nên tên yêu cầu là `themes.{slug}.layouts.docs` còn view thật lấy
429
+ // từ base: `ctrl.path` = `web.layouts.docs`. Hai chỗ dọn store
430
+ // (`pageCache.onEvict` và `destroyLayoutView`) đều xoá theo
431
+ // `ctrl.path`, nên lưu theo tên yêu cầu là instance ĐÃ DESTROY nằm
432
+ // lại trong store vĩnh viễn — lần điều hướng sau `extendView` trả về
433
+ // nó và `render()` trả rỗng. Không bật theme thì hai khoá trùng nhau
434
+ // nên bug này ẩn hoàn toàn.
435
+ const key = this.resolveRegistryKey(name);
436
+ const cached = this.viewFromStore(key, data, cache);
437
+ if (cached) return cached;
438
+ const factory = this.resolvedFactories.get(key) ?? this.viewRegistry[key];
439
+ if (!factory || typeof factory !== 'function') {
440
+ logger.error(`View "${name}" not found in registry.`);
441
+ return null;
442
+ }
443
+
444
+ const systemData = this.buildSystemData();
445
+ let view = factory(data ?? {}, systemData);
446
+
447
+ // Registry lazy → Promise<module>. Await, chuẩn hoá, cache factory đã
448
+ // unwrap để lần navigate sau không phải await/unwrap lại.
449
+ if (view && typeof view.then === 'function') {
450
+ const lazyFactory = this.unwrapLazyFactory(await view);
451
+ if (!lazyFactory) {
452
+ logger.error(`Lazy view "${name}" did not resolve to a factory or View.`);
453
+ return null;
218
454
  }
219
- return cachedView;
455
+ // Cache theo `key` cho khớp chỗ đọc ở trên; theo `name` thì
456
+ // lần sau luôn trượt cache khi theme đang bật.
457
+ this.resolvedFactories.set(key, lazyFactory);
458
+ view = lazyFactory(data ?? {}, systemData);
220
459
  }
221
- const factory = this.viewRegistry[name];
460
+ return this.finalizeView(key, view, cache);
461
+ }
462
+ catch (err) {
463
+ // Gồm cả chunk 404 / mạng lỗi khi import() — không để throw ra Router.
464
+ logger.error(`Error loading view ${name}:`, err);
465
+ return null;
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Bản đồng bộ cho `@include`/`@extends` — render tree không await được.
471
+ * View lazy CHƯA preload → null + hướng dẫn, thay vì trả Promise làm vỡ
472
+ * ngầm ở `view.__ctrl__` phía sau.
473
+ */
474
+ resolveViewSync(name: string, data: Record<string, any>, cache: boolean): any {
475
+ try {
476
+ // Xem ghi chú ở view(): khoá store bám khoá registry đã resolve.
477
+ const key = this.resolveRegistryKey(name);
478
+ const cached = this.viewFromStore(key, data, cache);
479
+ if (cached) return cached;
480
+ const factory = this.resolvedFactories.get(key) ?? this.viewRegistry[key];
222
481
  if (!factory || typeof factory !== 'function') {
223
482
  logger.error(`View "${name}" not found in registry.`);
224
483
  return null;
225
484
  }
226
- const view = factory(data ? { data } : {}, { App: this.App, View: this, ...this.systemData });
227
- if (!view) {
228
- logger.error(`Factory for view "${name}" did not return a valid view instance.`);
485
+
486
+ const view = factory(data ?? {}, this.buildSystemData());
487
+ if (view && typeof view.then === 'function') {
488
+ logger.error(
489
+ `View "${name}" là lazy nhưng được dùng qua @include/@extends — render tree đồng bộ ` +
490
+ `không await được. Gọi App.View.preloadView("${name}") trước, hoặc để view này eager trong registry.`
491
+ );
229
492
  return null;
230
493
  }
231
- if (cache) {
232
- this.store.set(name, view);
233
- }
234
- return view;
235
-
494
+ return this.finalizeView(key, view, cache);
236
495
  }
237
496
  catch (err) {
238
497
  logger.error(`Error loading view ${name}:`, err);
@@ -240,6 +499,54 @@ export class ViewManager implements ViewManagerInterface {
240
499
  }
241
500
  }
242
501
 
502
+ /** Nạp trước một view lazy để `@include`/`@extends` dùng được đồng bộ sau đó. */
503
+ async preloadView(name: string): Promise<boolean> {
504
+ if (this.resolvedFactories.has(name)) return true;
505
+ const view = await this.view(name, {}, false);
506
+ if (!view) return false;
507
+ view.__ctrl__?.destroy?.(); // instance dò đường — chỉ cần factory đã cache
508
+ return true;
509
+ }
510
+
511
+ private viewFromStore(name: string, data: Record<string, any>, cache: boolean): any {
512
+ if (!cache || !this.store.has(name)) return null;
513
+ const cachedView = this.store.get<View>(name);
514
+ if (hasData(data)) cachedView?.__ctrl__.updateData(data);
515
+ return cachedView;
516
+ }
517
+
518
+ /**
519
+ * Truyền data PHẲNG cho factory: compiled factory nhận __data__ là chính
520
+ * object data (đọc __data__.__SSR_VIEW_ID__, setup({data:__data__})).
521
+ * Trước đây bọc { data } → __data__.__SSR_VIEW_ID__ = undefined và ctrl.data
522
+ * bị lồng (vỡ route-param). Xem docs/HYDRATION.md §9.2.
523
+ */
524
+ private buildSystemData(): Record<string, any> {
525
+ return { App: this.App, View: this, ...this.systemData };
526
+ }
527
+
528
+ private finalizeView(name: string, view: any, cache: boolean): any {
529
+ if (!view) {
530
+ logger.error(`Factory for view "${name}" did not return a valid view instance.`);
531
+ return null;
532
+ }
533
+ if (cache) this.store.set(name, view);
534
+ return view;
535
+ }
536
+
537
+ /**
538
+ * Chuẩn hoá kết quả resolve của registry lazy về factory `(data, sys) => View`.
539
+ * Chấp nhận: module namespace (`{ default: factory }`), factory trần, hoặc
540
+ * View instance — người viết registry không phải tự `.then(m => m.default)`.
541
+ */
542
+ private unwrapLazyFactory(resolved: any): ((...args: any[]) => any) | null {
543
+ if (!resolved) return null;
544
+ if (typeof resolved === 'function') return resolved;
545
+ if (typeof resolved.default === 'function') return resolved.default;
546
+ if (resolved.__ctrl__) return () => resolved; // đã là View instance
547
+ return null;
548
+ }
549
+
243
550
  private createRenderPageViewError(view: ViewInterface, renderLevel: number, message?: string): RenderPageViewError {
244
551
  return {
245
552
  type: 'error',
@@ -250,14 +557,32 @@ export class ViewManager implements ViewManagerInterface {
250
557
  result: null,
251
558
  superView: null,
252
559
  finalView: view,
560
+ chain: [view],
253
561
  };
254
562
  }
255
563
 
564
+ private createRenderPageViewCancelled(view: ViewInterface): RenderPageViewCancelled {
565
+ return {
566
+ type: 'cancelled',
567
+ message: '',
568
+ view,
569
+ result: null,
570
+ superView: null,
571
+ finalView: view,
572
+ chain: [view],
573
+ };
574
+ }
575
+
576
+ private isNavigationCurrent(generation: number): boolean {
577
+ return generation === this.navigationGeneration;
578
+ }
579
+
256
580
  private createRenderPageViewSuccess(
257
581
  view: ViewInterface,
258
582
  result: ViewInterface | unknown,
259
583
  superView: ViewInterface | null,
260
- finalView: ViewInterface
584
+ finalView: ViewInterface,
585
+ chain: ViewInterface[] = [view],
261
586
  ): RenderPageViewSuccess {
262
587
  return {
263
588
  type: 'success',
@@ -266,11 +591,12 @@ export class ViewManager implements ViewManagerInterface {
266
591
  result,
267
592
  superView,
268
593
  finalView,
594
+ chain,
269
595
  };
270
596
  }
271
597
 
272
598
  private getRenderResultType(result: unknown): OOTEnum {
273
- return isRenderableObject(result) ? result.oneType as OOTEnum : OOTEnum.UNKNOWN;
599
+ return isRenderableObject(result) ? result.saoType as OOTEnum : OOTEnum.UNKNOWN;
274
600
  }
275
601
 
276
602
 
@@ -281,14 +607,23 @@ export class ViewManager implements ViewManagerInterface {
281
607
  mountRoot: HtmlInterface | null = null,
282
608
  initMode: InitMode = InitModes.CREATE,
283
609
  cache: boolean = false,
284
- renderLevel: number = 0
610
+ renderLevel: number = 0,
611
+ navigationGeneration: number = this.navigationGeneration,
285
612
  ): Promise<RenderPageViewResult> {
613
+ if (!this.isNavigationCurrent(navigationGeneration)) {
614
+ return this.createRenderPageViewCancelled(view);
615
+ }
286
616
  const ctrl = view.__ctrl__;
287
617
 
288
618
  if (hasData(data)) {
289
619
  ctrl.updateData(data);
290
620
  }
291
621
 
622
+ // Truyền initMode vào controller TRƯỚC khi render —
623
+ // để tất cả elements con (Html, Wrapper, Reactive, Output...)
624
+ // biết mình đang ở chế độ hydrate hay create.
625
+ ctrl.initMode = initMode;
626
+
292
627
  const result = method === 'render'
293
628
  ? ctrl.render()
294
629
  : (ctrl.prerender ? ctrl.prerender() : null);
@@ -305,17 +640,58 @@ export class ViewManager implements ViewManagerInterface {
305
640
  }
306
641
 
307
642
  if (resultType === OOTEnum.VIEW) {
643
+ const superView = result as ViewInterface;
644
+
645
+ // CSR navigation may resolve @extends to the exact Layout instance
646
+ // that is already active. Its DOM, outlets and lifecycle are retained,
647
+ // so rendering it again only recreates declarations and can repeat
648
+ // user side effects. Reuse the already-resolved outer chain instead.
649
+ // Hydration must still render every level to claim Blade's SSR DOM.
650
+ if (initMode === InitModes.CREATE) {
651
+ const activeLayoutIndex = this.currentLayoutChain.indexOf(superView);
652
+ if (activeLayoutIndex >= 0) {
653
+ const reusedChain = this.currentLayoutChain
654
+ .slice(0, activeLayoutIndex + 1)
655
+ .reverse();
656
+ return this.createRenderPageViewSuccess(
657
+ view,
658
+ superView,
659
+ superView,
660
+ reusedChain[reusedChain.length - 1] ?? superView,
661
+ [view, ...reusedChain],
662
+ );
663
+ }
664
+ }
665
+
666
+ // ── Hydrate: gán viewId của layout TRƯỚC khi render nó ──────────
667
+ // Page gọi extendView(layoutPath, {}) → layout tự sinh viewId ngẫu
668
+ // nhiên, KHÔNG khớp viewId server đã render. Phải gán đúng id ở đây
669
+ // (trước render → trước khi Wrapper layout capture ctx.viewId), bằng
670
+ // cách đọc lại từ DOM view marker <!--s:v:{id}-s--> server để lại.
671
+ if (initMode === InitModes.HYDRATE) {
672
+ const discovered = this.discoverChainViewId(
673
+ mountRoot,
674
+ new Set([ctrl.viewId]),
675
+ superView.__ctrl__.path,
676
+ ctrl.viewId,
677
+ );
678
+ if (discovered) {
679
+ superView.__ctrl__.viewId = discovered;
680
+ }
681
+ }
682
+
308
683
  const superResult = await this.renderPageView(
309
- result as ViewInterface, {}, mountRoot, initMode, cache, renderLevel + 1
684
+ superView, {}, mountRoot, initMode, cache, renderLevel + 1, navigationGeneration
310
685
  );
311
- if (superResult.type === 'error') {
686
+ if (superResult.type !== 'success') {
312
687
  return { ...superResult, view };
313
688
  }
314
689
  return this.createRenderPageViewSuccess(
315
690
  view,
316
- result,
691
+ superView,
317
692
  superResult.view,
318
- superResult.finalView ?? (result as ViewInterface)
693
+ superResult.finalView ?? superView,
694
+ [view, ...superResult.chain],
319
695
  );
320
696
  }
321
697
 
@@ -323,15 +699,67 @@ export class ViewManager implements ViewManagerInterface {
323
699
  `View "${ctrl.path}" returned invalid content (type: ${resultType}) from ${method}().`);
324
700
  }
325
701
 
702
+ /**
703
+ * Discover viewId của một layout/superView từ SSR DOM (hydration).
704
+ *
705
+ * Page gọi extendView() KHÔNG truyền viewId của layout (server tự sinh id),
706
+ * nên client lấy lại id từ quan hệ instance mà Blade export. Nếu output cũ
707
+ * chưa có quan hệ này, client mới discover từ metadata/marker trong DOM.
708
+ *
709
+ * Nested chain ưu tiên quan hệ parent chính xác trong APP_CONFIGS.view.ssrData,
710
+ * sau đó mới tới `data-view-name`/`data-view-id`; marker scan là fallback.
711
+ */
712
+ private discoverChainViewId(
713
+ mountRoot: HtmlInterface | null,
714
+ excludeIds: Set<string>,
715
+ viewPath?: string,
716
+ parentViewId?: string,
717
+ ): string | null {
718
+ if (viewPath) {
719
+ const instances = this.ssrViewData[viewPath]?.instances;
720
+ if (instances && typeof instances === 'object') {
721
+ for (const [instanceId, instance] of Object.entries(instances)) {
722
+ const id = (instance as any)?.viewId ?? instanceId;
723
+ if (id && !excludeIds.has(id)
724
+ && (!parentViewId || (instance as any)?.parent?.id === parentViewId)) {
725
+ return id;
726
+ }
727
+ }
728
+ }
729
+ const records = document.querySelectorAll('script[data-ref="view-data"][data-view-id]');
730
+ for (const record of records) {
731
+ const id = record.getAttribute('data-view-id');
732
+ if (record.getAttribute('data-view-name') === viewPath
733
+ && id && !excludeIds.has(id)) return id;
734
+ }
735
+ }
736
+ const root = mountRoot?.getElement?.() ?? this.rootElement?.getElement?.();
737
+ if (!root) return null;
738
+
739
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
740
+ let node: Comment | null;
741
+ while ((node = walker.nextNode() as Comment | null)) {
742
+ const parsed = markerRegistry.parseComment(node.nodeValue ?? '');
743
+ if (parsed && parsed.tag === 'view' && !parsed.isClose && !excludeIds.has(parsed.id)) {
744
+ return parsed.id;
745
+ }
746
+ }
747
+ return null;
748
+ }
749
+
326
750
  async renderPageView(
327
- view: ViewInterface,
328
- data: Record<string, any>,
329
- mountRoot: HtmlInterface | null = null,
330
- initMode: InitMode = InitModes.CREATE,
331
- cache: boolean = false,
332
- renderLevel: number = 0
751
+ view: ViewInterface,
752
+ data: Record<string, any>,
753
+ mountRoot: HtmlInterface | null = null,
754
+ initMode: InitMode = InitModes.CREATE,
755
+ cache: boolean = false,
756
+ renderLevel: number = 0,
757
+ navigationGeneration: number = this.navigationGeneration,
333
758
  ): Promise<RenderPageViewResult> {
334
759
  try {
760
+ if (!this.isNavigationCurrent(navigationGeneration)) {
761
+ return this.createRenderPageViewCancelled(view);
762
+ }
335
763
  const ctrl = view.__ctrl__;
336
764
  if (hasData(data)) {
337
765
  ctrl.updateData(data);
@@ -341,43 +769,124 @@ export class ViewManager implements ViewManagerInterface {
341
769
 
342
770
  // ── Case 1: Không có async data → render ngay ──
343
771
  if (!hasAsyncData) {
344
- return this.callViewRenderFactory(view, 'render', data, mountRoot, initMode, cache, renderLevel);
772
+ return this.callViewRenderFactory(view, 'render', data, mountRoot, initMode, cache, renderLevel, navigationGeneration);
773
+ }
774
+
775
+ // ── Hydrate: SSR đã fetch + nhúng data rồi (qua __data__/ssrData) —
776
+ // KHÔNG fetch lại, KHÔNG hiện skeleton. Nhưng prerender() không chỉ
777
+ // là skeleton: với view @extends + @block, compiler gom phần block/
778
+ // section TĨNH (không phụ thuộc data await) VÀO PRERENDER, render()
779
+ // chỉ khai báo lại đúng (các) block phụ thuộc data (xem
780
+ // examples/sao/await.sao đã compile: prerender() có
781
+ // block-content(placeholder)+block-footer+section('sidebar'), còn
782
+ // render() chỉ có block-content — bỏ qua prerender() sẽ làm mất
783
+ // hẳn block-footer/sidebar khi hydrate).
784
+ // → Vẫn phải gọi prerender() để đăng ký các block/section tĩnh đó,
785
+ // rồi gọi NGAY render() (data đã có sẵn, không await fetch) — nó
786
+ // ghi đè contentRenderFactory của block phụ thuộc data (placeholder
787
+ // → nội dung thật) tại chỗ. Không có gì được mount ở bước nào cả
788
+ // (hydrate mode chỉ claim), nên không có flash trung gian.
789
+ if (initMode === InitModes.HYDRATE) {
790
+ if (config.hasPrerender) {
791
+ const prerenderResult = await this.callViewRenderFactory(view, 'prerender', data, mountRoot, initMode, cache, renderLevel, navigationGeneration);
792
+ if (prerenderResult.type === 'cancelled') return prerenderResult;
793
+ if (prerenderResult.type === 'error') {
794
+ logger.error(`Error prerendering view "${ctrl.path}" during hydrate:`, prerenderResult.message);
795
+ // Tiếp tục render() — thà thiếu block tĩnh còn hơn vỡ cả trang.
796
+ }
797
+ }
798
+ return this.callViewRenderFactory(view, 'render', data, mountRoot, initMode, cache, renderLevel, navigationGeneration);
345
799
  }
346
800
 
347
801
  // ── Resolve fetch URL từ ViewController config hoặc fallback Router ──
348
802
  const App = app() as ApplicationInterface;
349
803
  const Http = App.Http;
350
804
  const fetchConfig = config.fetch;
351
- const fetchUrl = (config.hasAwaitData && fetchConfig?.url)? fetchConfig?.url : App.Router.getFullUrl();
805
+ const fetchUrl = (config.hasAwaitData && fetchConfig?.url) ? fetchConfig.url : resolveViewDataUrl(App.Router.getFullUrl(), this.dataEndpoint);
352
806
 
353
807
  // ── Case 2: Có async + có prerender → prerender skeleton trước, fetch sau ──
354
808
  if (config.hasPrerender) {
355
- const prerenderResult = await this.callViewRenderFactory(view, 'prerender', data, mountRoot, initMode, cache, renderLevel);
809
+ const renderGeneration = navigationGeneration;
810
+ const prerenderResult = await this.callViewRenderFactory(view, 'prerender', data, mountRoot, initMode, cache, renderLevel, navigationGeneration);
811
+ if (prerenderResult.type === 'cancelled') return prerenderResult;
356
812
  if (prerenderResult.type === 'error') {
357
813
  logger.error(`Error prerendering view "${ctrl.path}":`, prerenderResult.message);
358
814
  // Fallback: render trực tiếp không qua prerender
359
- return this.callViewRenderFactory(view, 'render', data, mountRoot, initMode, cache, renderLevel);
815
+ return this.callViewRenderFactory(view, 'render', data, mountRoot, initMode, cache, renderLevel, navigationGeneration);
360
816
  }
361
817
 
362
- // Fire-and-forget: fetch data rồi re-render thay thế skeleton
363
- // TODO: Sau khi render() xong, cần swap DOM từ preloadElement → mainElement
364
- // ViewController.wrapper() tự động lưu prerender vào preloadElement, render vào mainElement
365
- // Cần bổ sung logic: unmount preloadElement khỏi DOM → mount mainElement vào cùng vị trí
366
- Http.get(fetchUrl).then(async (response: any) => {
367
- const asyncData = response?.data ?? {};
818
+ // Fire-and-forget: fetch data → re-render → swap skeleton → main
819
+ this.pendingAsyncRenders++;
820
+ Http.get(fetchUrl, this.fetchOptions).then(async (response: any) => {
821
+ // Route mới hoặc manager teardown đã bắt đầu: tuyệt đối không
822
+ // render/mount kết quả cũ trở lại root DOM.
823
+ if (renderGeneration !== this.navigationGeneration
824
+ || ctrl.lifecycleState === 'destroyed') return;
825
+
826
+ const asyncData = this.extractAsyncData(response);
368
827
  if (hasData(asyncData)) {
369
828
  ctrl.updateData(asyncData);
370
829
  }
371
- const finalResult = await this.callViewRenderFactory(view, 'render', asyncData, mountRoot, initMode, cache, renderLevel);
830
+
831
+ // Render main content (kết quả sẽ nằm trong ctrl.mainElement)
832
+ const finalResult = await this.callViewRenderFactory(view, 'render', asyncData, mountRoot, initMode, cache, renderLevel, navigationGeneration);
833
+ if (renderGeneration !== this.navigationGeneration
834
+ || ctrl.lifecycleState === 'destroyed') return;
372
835
  if (finalResult.type === 'error') {
373
836
  logger.error(`Error rendering view "${ctrl.path}" after async data fetch:`, finalResult.message);
837
+ return;
838
+ }
839
+
840
+ if (finalResult.superView) {
841
+ // ── @extends page: render() re-registered real block content
842
+ // via this.block(...), but never called this.wrapper() — so
843
+ // ctrl.mainElement/preloadElement (what the standalone swap
844
+ // below acts on) were never set. The already-mounted layout's
845
+ // outlet still shows the placeholder from the prerender-time
846
+ // this.block(...) call; push the new content into it the same
847
+ // way the initial mount does (mountViewBlocks clears the old
848
+ // content first, so the placeholder is removed as part of this).
849
+ // Block tìm outlet THEO TÊN nên viewId của page là đủ;
850
+ // section thì không — xem mountSectionsAcrossChain().
851
+ this.blockManager.mountViewBlocks(ctrl.viewId);
852
+ this.mountSectionsAcrossChain(ctrl.viewId, this.currentLayoutChain);
853
+ commitView(ctrl);
854
+ this.blockManager.startAll();
855
+ this.sectionManager.startAll();
856
+ logger.info(`[ViewManager] prerender → main swap done for "${ctrl.path}" (block content)`);
857
+ return;
858
+ }
859
+
860
+ // ── Swap: preloadElement (skeleton) → mainElement (real) ──
861
+ // 1. Destroy skeleton khỏi DOM
862
+ if (ctrl.preloadElement && typeof ctrl.preloadElement.destroy === 'function') {
863
+ ctrl.preloadElement.stop?.();
864
+ ctrl.preloadElement.destroy();
865
+ }
866
+ // 2. Mount mainElement vào cùng vị trí (mountRoot)
867
+ if (ctrl.mainElement && mountRoot) {
868
+ ctrl.mainElement.setParentElement(mountRoot);
869
+ ctrl.mainElement.mountTo(mountRoot);
374
870
  }
375
- // TODO: swap preloadElement → mainElement trong DOM
376
- // ctrl.preloadElement?.destroy();
377
- // ctrl.mainElement?.setParentElement(mountRoot);
378
- // ctrl.mainElement?.render();
871
+ // 3. Commit data + start reactivity
872
+ commitView(ctrl);
873
+ ctrl.mainElement?.start?.();
874
+ logger.info(`[ViewManager] prerender → main swap done for "${ctrl.path}"`);
379
875
  }).catch((err: any) => {
380
- logger.error(`Error fetching async data for view "${ctrl.path}":`, err);
876
+ if (!this.isNavigationCurrent(renderGeneration)) return;
877
+ // Fetch hỏng khi skeleton đang hiển thị: không có boundary thì
878
+ // trước giờ chỉ log → skeleton treo vĩnh viễn. Đưa vào boundary
879
+ // để view tự hiện trạng thái lỗi.
880
+ const handled = ctrl.handleError(err, { phase: 'async', path: ctrl.path }).handled;
881
+ if (!handled) logger.error(`Error fetching async data for view "${ctrl.path}":`, err);
882
+ }).finally(() => {
883
+ this.pendingAsyncRenders--;
884
+ // Nội dung thật vừa thay chỗ skeleton. Ai đã trang trí DOM
885
+ // trước đó (tô màu code, dựng mục lục…) phải được báo để
886
+ // làm lại — bản trang trí cũ nằm trên node vừa bị thay.
887
+ if (this.pendingAsyncRenders === 0) {
888
+ (app() as ApplicationInterface).Event?.emit('view:settled');
889
+ }
381
890
  });
382
891
 
383
892
  // Return prerender result ngay — mountView sẽ mount skeleton
@@ -387,17 +896,25 @@ export class ViewManager implements ViewManagerInterface {
387
896
  // ── Case 3: Có async + không prerender → await fetch rồi render ──
388
897
  let asyncData: Record<string, any> = {};
389
898
  try {
390
- const response = await Http.get(fetchUrl);
391
- asyncData = response?.data ?? {};
899
+ const response = await Http.get(fetchUrl, this.fetchOptions);
900
+ asyncData = this.extractAsyncData(response);
392
901
  } catch (err) {
393
- logger.error(`Error fetching async data for view "${ctrl.path}":`, err);
902
+ if (!this.isNavigationCurrent(navigationGeneration)) {
903
+ return this.createRenderPageViewCancelled(view);
904
+ }
905
+ const handled = ctrl.handleError(err, { phase: 'async', path: ctrl.path }).handled;
906
+ if (!handled) logger.error(`Error fetching async data for view "${ctrl.path}":`, err);
907
+ }
908
+
909
+ if (!this.isNavigationCurrent(navigationGeneration)) {
910
+ return this.createRenderPageViewCancelled(view);
394
911
  }
395
912
 
396
913
  if (!hasData(asyncData)) {
397
914
  logger.warn(`View "${ctrl.path}" has async data config but fetch returned no data.`);
398
915
  }
399
916
 
400
- return this.callViewRenderFactory(view, 'render', asyncData, mountRoot, initMode, cache, renderLevel);
917
+ return this.callViewRenderFactory(view, 'render', asyncData, mountRoot, initMode, cache, renderLevel, navigationGeneration);
401
918
  }
402
919
  catch (err) {
403
920
  logger.error(`Error rendering view ${view.__ctrl__.path}:`, err);
@@ -416,266 +933,790 @@ export class ViewManager implements ViewManagerInterface {
416
933
 
417
934
  // ─── Mount Orchestration ────────────────────────────────────
418
935
 
936
+ /**
937
+ * Mount view khi navigate — luồng chuẩn (ROUTE_RENDER_FLOW.md):
938
+ * sweep TTL → duplicate guard → pause+cache trang cũ (standalone LẪN
939
+ * page thuộc layout) → thử restore từ PageCache (theo view name + URI,
940
+ * trong TTL, mọi navigation type) → mount mới
941
+ * (render → mount DOM → commitData → start).
942
+ *
943
+ * Lifecycle khi RỜI trang (deactivatePage):
944
+ * - page cacheable → pause (pausing/paused) + detach DOM → PageCache
945
+ * - page cache:false → destroy (stopping/stopped → unmounting/unmounted → destroyed)
946
+ * - layout KHÔNG đổi → không hook nào fire trên layout (giữ nguyên DOM + subscription)
947
+ * - layout đổi/về standalone → destroy layout chain
948
+ */
949
+ private localeInvalidated = false;
950
+
951
+ invalidateLocale(): void {
952
+ this.cancelNavigation();
953
+ this.localeInvalidated = true;
954
+ this.pageCache.clear();
955
+ }
956
+
419
957
  async mountView(name: string, data?: Record<string, any>, route?: ActiveRouteInterface, navigationType: RouterNavigationType = 'push'): Promise<any> {
420
- // ── Phase 1: Load view ──
421
- const view = this.view(name, data ?? {}, true);
422
- if (!view) {
423
- this.showError(`Failed to load view "${name}".`);
424
- return null;
958
+ if (this.localeInvalidated) {
959
+ this.unmountAll();
960
+ this.store.clear();
961
+ this.ssrBoot = null;
962
+ this.localeInvalidated = false;
963
+ }
964
+ // Request URI = path + query (KHÔNG hash) — Router cung cấp qua $uri
965
+ const targetUrl = (route as any)?.$uri ?? route?.$urlPath ?? name;
966
+
967
+ // ── Phase 0: TTL sweep + duplicate guard ──
968
+ this.pageCache.sweep();
969
+ if (this.currentPageView
970
+ && this.currentPageView.__ctrl__.urlPath === targetUrl) {
971
+ return null; // đã đứng đúng trang này
425
972
  }
426
973
 
974
+ // Mọi async work của navigation trước trở thành stale từ thời điểm này.
975
+ const navigationGeneration = ++this.navigationGeneration;
976
+
977
+ // Head tags (title/meta) từ trang trước không được rò rỉ sang trang mới —
978
+ // mountViewSections() của layout/page mới sẽ set lại đúng phần chúng khai báo.
979
+ this.sectionManager.resetPageHead();
980
+
427
981
  const oldPageView = this.currentPageView;
428
982
  const oldLayoutView = this.currentLayoutView;
429
- const oldLayoutPath = this.currentLayoutPath;
430
-
431
- view.__ctrl__.urlPath = route?.$urlPath ?? null;
983
+ const oldLayoutChain = [...this.currentLayoutChain];
984
+ // BlockManager hiện đăng ký block theo outlet dùng chung. Với page
985
+ // thuộc Layout, phải detach ownership cũ trước khi render page mới;
986
+ // nếu không block mới sẽ ghi đè registry và cache sai DOM.
987
+ // Standalone không có ràng buộc này nên được giữ active tới commit.
988
+ const oldPageRequiresEarlyDeactivate = oldPageView !== null && oldLayoutChain.length > 0;
989
+ let oldPageDeactivated = false;
990
+ if (oldPageRequiresEarlyDeactivate) {
991
+ this.deactivatePage(oldPageView!, oldLayoutView);
992
+ this.currentPageView = null;
993
+ oldPageDeactivated = true;
994
+ }
432
995
 
433
- if(oldLayoutView){
434
- oldLayoutView.__ctrl__.stop();
996
+ // ── Phase 1: thử restore từ PageCache (key = view name + request URI) ──
997
+ // Trong TTL: mọi navigation type đều restore (bfcache). Hết TTL → mount tươi.
998
+ const cachedEntry = this.pageCache.take(this.cacheKey(name, targetUrl));
999
+ if (cachedEntry) {
1000
+ if (oldPageView && !oldPageDeactivated) {
1001
+ this.deactivatePage(oldPageView, oldLayoutView);
1002
+ oldPageDeactivated = true;
1003
+ }
1004
+ this.currentPageView = null;
1005
+ const restored = this.restoreFromCache(cachedEntry, oldLayoutView, navigationType);
1006
+ if (restored) return restored;
435
1007
  }
436
- oldPageView?.__ctrl__.stop();
437
1008
 
438
- // ── Phase 2: Render chain ──
439
- const renderResult = await this.renderPageView(view, data ?? {}, this.rootElement, InitModes.CREATE, true);
1009
+ // ── Phase 2: Load + render chain trong khi page cũ vẫn active. ──
1010
+ // Chỉ khi render thành công mới pause/destroy page cũ; fetch/render lỗi
1011
+ // không được làm màn hình hiện tại biến mất.
1012
+ const view = await this.view(name, data ?? {}, false);
1013
+ if (!view) {
1014
+ const message = `Failed to load view "${name}".`;
1015
+ if (oldPageView && oldPageDeactivated) {
1016
+ this.restoreDeactivatedPage(oldPageView!, oldLayoutView);
1017
+ }
1018
+ if (oldPageView) logger.error(message);
1019
+ else this.showError(message);
1020
+ return null;
1021
+ }
1022
+ view.__ctrl__.urlPath = targetUrl;
1023
+
1024
+ const renderResult = await this.renderPageView(
1025
+ view, data ?? {}, this.rootElement, InitModes.CREATE, false, 0, navigationGeneration
1026
+ );
1027
+ if (renderResult.type === 'cancelled') {
1028
+ view.__ctrl__.destroy();
1029
+ if (oldPageView && oldPageDeactivated) {
1030
+ this.restoreDeactivatedPage(oldPageView!, oldLayoutView);
1031
+ }
1032
+ return null;
1033
+ }
440
1034
  if (renderResult.type === 'error') {
441
- this.showError(renderResult.message);
1035
+ view.__ctrl__.destroy();
1036
+ if (oldPageView && oldPageDeactivated) {
1037
+ this.restoreDeactivatedPage(oldPageView!, oldLayoutView);
1038
+ }
1039
+ if (oldPageView) logger.error(renderResult.message);
1040
+ else this.showError(renderResult.message);
1041
+ return null;
1042
+ }
1043
+
1044
+ // ── Phase 3: commit transition ──
1045
+ // Page/Layout mới đã resolve xong; bây giờ mới rời chain cũ và mount chain mới.
1046
+ if (oldPageView && !oldPageDeactivated) {
1047
+ this.deactivatePage(oldPageView, oldLayoutView);
1048
+ }
1049
+ this.currentPageView = null;
1050
+ // Phase mount nằm NGOÀI try/catch của renderPageView: lỗi phát sinh khi
1051
+ // gắn tree vào DOM (vd @include con throw) trước đây thoát hẳn ra ngoài
1052
+ // → unhandled rejection + trang mount dở. Error boundary đã xử lý phần
1053
+ // lớn (Component/Reactive); đây là lưới cuối khi không boundary nào nhận.
1054
+ try {
1055
+ this.activateRenderedChain(renderResult, InitModes.CREATE, oldLayoutChain);
1056
+ } catch (err) {
1057
+ logger.error(`Error mounting view "${name}":`, err);
1058
+ this.showError(`Error mounting view "${name}".`, err instanceof Error ? err.message : err);
442
1059
  return null;
443
1060
  }
444
1061
 
445
- const pageView: ViewInterface = renderResult.view;
446
- const finalView: ViewInterface = renderResult.finalView;
447
- const hasSuperView = renderResult.superView !== null;
448
- const newLayoutPath = hasSuperView ? finalView.__ctrl__.path : null;
449
-
450
- // bắt đầu logic
451
-
452
- // ── Phase 3: Classify mount scenario ──
453
- const isSameLayout = hasSuperView
454
- && oldLayoutView !== null
455
- && finalView === oldLayoutView;
456
-
457
- // ── Duplicate navigation guard ──
458
- if (oldPageView === pageView
459
- && view.__ctrl__.urlPath === oldPageView?.__ctrl__?.urlPath) {
460
- return renderResult;
461
- }
462
-
463
- // ══════════════════════════════════════════════════════════
464
- // Case 1: Có superView + cùng layout instance (reuse layout)
465
- // ══════════════════════════════════════════════════════════
466
- if (hasSuperView && isSameLayout) {
467
- // Cắt origin chain cũ → gắn page mới vào layout
468
- oldLayoutView!.__ctrl__.ejectOriginChain();
469
- pageView.__ctrl__.setChainFromOrigin();
470
-
471
-
472
- // Mount block content mới vào layout outlets
473
- this.blockManager.mountAll();
474
-
475
- }
476
- // ══════════════════════════════════════════════════════════
477
- // Case 2: Có superView + layout khác (hoặc first mount có layout)
478
- // ══════════════════════════════════════════════════════════
479
- else if (hasSuperView) {
480
- // Cleanup toàn bộ old views
481
- oldLayoutView?.__ctrl__.ejectOriginChain();
482
- this.stopPageView(oldPageView);
483
- this.stopLayoutView(oldLayoutView);
484
- this.blockManager.clearAllOutlets();
485
- this.unmountLayoutDOM(oldLayoutView);
486
-
487
- // Gắn origin chain cho layout mới
488
- pageView.__ctrl__.setChainFromOrigin();
489
-
490
- // Build DOM từ layout wrapper vào container
491
- this.buildViewDOM(finalView);
492
-
493
- // Mount block content vào layout outlets
494
- this.blockManager.mountAll();
495
-
496
- // Commit & Start toàn bộ chain
497
- this.commitViewChain(pageView, finalView, true);
498
- this.startViewChain(pageView, finalView, true);
499
- }
500
- // ══════════════════════════════════════════════════════════
501
- // Case 3: Không có superView (standalone view)
502
- // ══════════════════════════════════════════════════════════
503
- else {
504
- // Cleanup toàn bộ old views
505
- oldLayoutView?.__ctrl__.ejectOriginChain();
506
- this.stopPageView(oldPageView);
507
- this.stopLayoutView(oldLayoutView);
508
- this.blockManager.clearAllOutlets();
509
- this.unmountLayoutDOM(oldLayoutView);
510
-
511
- // Build DOM trực tiếp vào container
512
- this.buildViewDOM(pageView);
513
-
514
- // Commit & Start
515
- this.commitViewChain(pageView, pageView, false);
516
- this.startViewChain(pageView, pageView, false);
517
- }
518
-
519
- // ── Update state ──
1062
+ return renderResult;
1063
+ }
1064
+
1065
+ /** Commit the successfully mounted/hydrated chain as the only active route state. */
1066
+ private commitActiveChain(
1067
+ pageView: ViewInterface,
1068
+ layoutChain: ViewInterface[],
1069
+ ): void {
520
1070
  this.currentPageView = pageView;
521
- this.currentLayoutView = hasSuperView ? finalView : null;
522
- this.currentLayoutPath = newLayoutPath;
523
- this.currentViewType = hasSuperView ? 'layout' : 'view';
524
- this.viewStack = hasSuperView ? [finalView, pageView] : [pageView];
1071
+ this.currentLayoutChain = [...layoutChain];
1072
+ this.currentLayoutView = layoutChain[0] ?? null;
1073
+ this.currentLayoutPath = this.currentLayoutView?.__ctrl__.path ?? null;
1074
+ this.currentViewType = layoutChain.length > 0 ? 'layout' : 'view';
1075
+ this.viewStack = [...layoutChain, pageView];
525
1076
  this.renderCount++;
1077
+ }
526
1078
 
527
- return renderResult;
1079
+ /**
1080
+ * Common post-render transaction. Rendering decides the Page/Layout chain;
1081
+ * this step applies the DOM strategy, activates it, then publishes one
1082
+ * coherent active-chain state.
1083
+ */
1084
+ private activateRenderedChain(
1085
+ renderResult: RenderPageViewSuccess,
1086
+ initMode: InitMode,
1087
+ oldLayoutChain: ViewInterface[] = [],
1088
+ ): void {
1089
+ const pageView = renderResult.view;
1090
+ const layoutChain = renderResult.chain.slice(1).reverse();
1091
+
1092
+ if (initMode === InitModes.HYDRATE) {
1093
+ this.activateHydratedChain(pageView, layoutChain);
1094
+ } else {
1095
+ this.activateCreatedChain(pageView, layoutChain, oldLayoutChain);
1096
+ }
1097
+
1098
+ this.commitActiveChain(pageView, layoutChain);
528
1099
  }
529
1100
 
530
- // ─── DOM Building ───────────────────────────────────────────
1101
+ /** CSR strategy: insert new DOM, while preserving/reusing a compatible Layout. */
1102
+ private activateCreatedChain(
1103
+ pageView: ViewInterface,
1104
+ layoutChain: ViewInterface[],
1105
+ oldLayoutChain: ViewInterface[],
1106
+ ): void {
1107
+ const pageCtrl = pageView.__ctrl__;
1108
+
1109
+ if (layoutChain.length === 0) {
1110
+ if (oldLayoutChain.length > 0) {
1111
+ this.deactivateLayoutChain(oldLayoutChain);
1112
+ this.currentLayoutView = null;
1113
+ this.currentLayoutChain = [];
1114
+ this.currentLayoutPath = null;
1115
+ }
1116
+ pageCtrl.mount(this.rootElement!);
1117
+ commitView(pageCtrl);
1118
+ activateView(pageCtrl);
1119
+ this.sectionManager.mountViewSections(pageCtrl.viewId);
1120
+ this.sectionManager.startAll();
1121
+ return;
1122
+ }
1123
+
1124
+ let common = 0;
1125
+ while (common < oldLayoutChain.length
1126
+ && common < layoutChain.length
1127
+ && oldLayoutChain[common] === layoutChain[common]) {
1128
+ common++;
1129
+ }
1130
+
1131
+ if (common === 0 && oldLayoutChain.length > 0) {
1132
+ this.deactivateLayoutChain(oldLayoutChain);
1133
+ } else {
1134
+ // Root layout vẫn dùng chung; chỉ dọn phần chain không còn reuse.
1135
+ for (let i = oldLayoutChain.length - 1; i >= common; i--) {
1136
+ this.destroyLayoutView(oldLayoutChain[i]);
1137
+ }
1138
+ }
1139
+
1140
+ const resumed = common === 0 && this.resumeLayoutChainFromCache(layoutChain);
1141
+ const newLayouts: ViewInterface[] = [];
1142
+
1143
+ if (!resumed) {
1144
+ let start = common;
1145
+ if (common === 0) {
1146
+ layoutChain[0].__ctrl__.mount(this.rootElement!);
1147
+ newLayouts.push(layoutChain[0]);
1148
+ start = 1;
1149
+ }
1150
+ for (let i = start; i < layoutChain.length; i++) {
1151
+ const layout = layoutChain[i];
1152
+ this.blockManager.mountViewBlocks(layout.__ctrl__.viewId);
1153
+ this.sectionManager.mountViewSections(layout.__ctrl__.viewId);
1154
+ layout.__ctrl__.mount();
1155
+ newLayouts.push(layout);
1156
+ }
1157
+ }
1158
+
1159
+ this.blockManager.mountViewBlocks(pageCtrl.viewId);
1160
+ this.mountSectionsAcrossChain(pageCtrl.viewId, layoutChain);
1161
+ pageCtrl.mount();
1162
+ for (const layout of newLayouts) commitView(layout.__ctrl__);
1163
+ commitView(pageCtrl);
1164
+
1165
+ for (const layout of newLayouts) activateView(layout.__ctrl__);
1166
+ this.blockManager.startAll();
1167
+ this.sectionManager.startAll();
1168
+ activateView(pageCtrl);
1169
+ }
531
1170
 
532
1171
  /**
533
- * Build DOM từ finalView's Wrapper vào container.
534
- * Wrapper.render() sẽ execute childrenFactory → tạo DOM tree.
1172
+ * Đưa `@section` của page tới `@yield` của layout.
1173
+ *
1174
+ * `mountViewSections` lọc yield theo `yieldEl.ctx.viewId`, mà YIELD THUỘC
1175
+ * LAYOUT khai báo nó chứ không thuộc page — nên gọi riêng với viewId của
1176
+ * page thì không yield nào khớp. Chỉ quét các layout MỚI cũng không đủ:
1177
+ * điều hướng giữa hai trang dùng chung một layout thì `common` phủ hết
1178
+ * chuỗi, vòng lặp mount layout không chạy lần nào, và section của trang mới
1179
+ * không bao giờ tới nơi.
1180
+ *
1181
+ * Quét cả chuỗi là an toàn: mountViewSections chỉ áp lại `activeSections`
1182
+ * hiện hành vào từng yield, gọi thừa không đổi kết quả.
535
1183
  */
536
- private buildViewDOM(finalView: ViewInterface): void {
537
- const wrapper = finalView.__ctrl__.mainElement;
538
- if (!wrapper) {
539
- logger.error(`[ViewManager] finalView "${finalView.__ctrl__.path}" has no mainElement (Wrapper).`);
1184
+ private mountSectionsAcrossChain(pageViewId: string, layoutChain: ViewInterface[]): void {
1185
+ this.sectionManager.mountViewSections(pageViewId);
1186
+ for (const layout of layoutChain) {
1187
+ const layoutId = layout?.__ctrl__?.viewId;
1188
+ if (layoutId && layoutId !== pageViewId) {
1189
+ this.sectionManager.mountViewSections(layoutId);
1190
+ }
1191
+ }
1192
+ }
1193
+
1194
+ /** Hydration strategy: claim Blade DOM without insert/clear mutations. */
1195
+ private activateHydratedChain(
1196
+ pageView: ViewInterface,
1197
+ layoutChain: ViewInterface[],
1198
+ ): void {
1199
+ const pageCtrl = pageView.__ctrl__;
1200
+
1201
+ if (layoutChain.length === 0) {
1202
+ commitView(pageCtrl, true);
1203
+ claimHydratedView(pageCtrl, this.rootElement!);
1204
+ pageCtrl.mount();
1205
+ pageCtrl.initMode = InitModes.CREATE;
1206
+ activateView(pageCtrl);
1207
+ this.sectionManager.hydrateViewSections(pageCtrl.viewId);
1208
+ this.sectionManager.startAll();
540
1209
  return;
541
1210
  }
542
- wrapper.setParentElement(this.rootElement);
543
- wrapper.render();
1211
+
1212
+ for (const layout of layoutChain) commitView(layout.__ctrl__, true);
1213
+ commitView(pageCtrl, true);
1214
+ const rootLayoutCtrl = layoutChain[0].__ctrl__;
1215
+ claimHydratedView(rootLayoutCtrl, this.rootElement!);
1216
+ rootLayoutCtrl.mount();
1217
+ for (let i = 1; i < layoutChain.length; i++) {
1218
+ const ctrl = layoutChain[i].__ctrl__;
1219
+ this.blockManager.hydrateViewBlocks(ctrl.viewId);
1220
+ this.sectionManager.hydrateViewSections(ctrl.viewId);
1221
+ ctrl.mount();
1222
+ }
1223
+ this.blockManager.hydrateViewBlocks(pageCtrl.viewId);
1224
+ this.sectionManager.hydrateViewSections(pageCtrl.viewId);
1225
+ pageCtrl.mount();
1226
+ for (const layout of layoutChain) layout.__ctrl__.initMode = InitModes.CREATE;
1227
+ pageCtrl.initMode = InitModes.CREATE;
1228
+
1229
+ for (const layout of layoutChain) activateView(layout.__ctrl__);
1230
+ this.blockManager.startAll();
1231
+ this.sectionManager.startAll();
1232
+ activateView(pageCtrl);
544
1233
  }
545
1234
 
546
- // ─── Lifecycle: Stop ────────────────────────────────────────
1235
+ /**
1236
+ * Destroy layout cũ (đổi layout hoặc về standalone).
1237
+ * Page cũ đã rời ở Phase 1 (deactivatePage) — không destroy tại đây.
1238
+ */
1239
+ private destroyLayoutView(layoutView: ViewInterface): void {
1240
+ const layoutPath = layoutView.__ctrl__.path;
1241
+ layoutView.__ctrl__.destroy();
1242
+ // Layout cached theo path trong store (extendView dùng cache=true) —
1243
+ // instance đã destroy không được phép trả về từ cache nữa
1244
+ if (layoutPath && this.store.has(layoutPath)) {
1245
+ this.store.remove?.(layoutPath);
1246
+ }
1247
+ }
547
1248
 
548
- private stopPageView(pageView: ViewInterface | null): void {
549
- if (!pageView) return;
550
- // Stop block content (tracked by BlockManager)
551
- this.stopBlockContent();
552
- // Fire onDeactivated hook
553
- if (typeof pageView.onDeactivated === 'function') {
554
- try { pageView.onDeactivated(); }
555
- catch (e) { logger.error(`[ViewManager] onDeactivated error:`, e); }
1249
+ private destroyLayoutChain(layoutChain: ViewInterface[]): void {
1250
+ for (let i = layoutChain.length - 1; i >= 0; i--) {
1251
+ this.destroyLayoutView(layoutChain[i]);
556
1252
  }
557
1253
  }
558
1254
 
559
- private stopLayoutView(layoutView: ViewInterface | null): void {
560
- if (!layoutView) return;
561
- const wrapper = layoutView.__ctrl__.mainElement;
562
- if (wrapper && typeof wrapper.stop === 'function') {
563
- wrapper.stop();
1255
+ // ─── PageCache integration ──────────────────────────────────
1256
+
1257
+ /** bfcache-style cache cho trang đã ghé (ROUTE_RENDER_FLOW.md §8) */
1258
+ public pageCache: PageCacheService = new PageCacheService();
1259
+
1260
+ /**
1261
+ * Cache key = `${viewName}::${requestUri}` — URI gồm path + query,
1262
+ * KHÔNG gồm hash (strip defensive tại đây).
1263
+ */
1264
+ private cacheKey(viewName: string, uri: string): string {
1265
+ return `${viewName}::${(uri ?? '').split('#')[0]}`;
1266
+ }
1267
+
1268
+ /** Cache key cho layout — namespace riêng, không đụng key page (name::uri) */
1269
+ private layoutCacheKey(layoutPath: string): string {
1270
+ return `__layout__::${layoutPath}`;
1271
+ }
1272
+
1273
+ private layoutChainIdentity(layoutChain: ViewInterface[]): string {
1274
+ return layoutChain.map(view => view.__ctrl__.path).join('>');
1275
+ }
1276
+
1277
+ /**
1278
+ * Rời một layout (đổi layout / về standalone): pause + detach toàn vùng DOM
1279
+ * → PageCache (key `__layout__::{path}`). KHÁC destroy ở 2 điểm:
1280
+ * 1. Instance GIỮ NGUYÊN trong store — extendView của page sau trả lại
1281
+ * đúng instance này (đang paused) → resumeLayoutFromCache nhận ra.
1282
+ * 2. Outlets gỡ khỏi BlockManager registry (không destroy) — tránh
1283
+ * mountAll đụng outlet trùng tên của layout đang nằm trong cache.
1284
+ * Layout khai cache:false (hoặc ttl 0) → destroy như cũ.
1285
+ */
1286
+ private deactivateLayout(layoutView: ViewInterface): void {
1287
+ this.deactivateLayoutChain([layoutView]);
1288
+ }
1289
+
1290
+ private deactivateLayoutChain(layoutChain: ViewInterface[]): void {
1291
+ const rootCtrl = layoutChain[0]?.__ctrl__;
1292
+ if (!rootCtrl) return;
1293
+ const cacheConfig = rootCtrl.getConfig('cache');
1294
+ const ttl = typeof cacheConfig === 'object' && cacheConfig?.ttl != null ? cacheConfig.ttl : undefined;
1295
+ const wrapper = rootCtrl.mainElement;
1296
+
1297
+ if (!wrapper || cacheConfig === false || ttl === 0) {
1298
+ this.destroyLayoutChain(layoutChain);
1299
+ return;
1300
+ }
1301
+
1302
+ for (let i = layoutChain.length - 1; i >= 0; i--) {
1303
+ layoutChain[i].__ctrl__.pause();
564
1304
  }
565
- if (typeof layoutView.onDeactivated === 'function') {
566
- try { layoutView.onDeactivated(); }
567
- catch (e) { logger.error(`[ViewManager] layout onDeactivated error:`, e); }
1305
+ for (const layout of layoutChain) {
1306
+ this.blockManager.detachOutletsOfView(layout.__ctrl__.viewId);
568
1307
  }
1308
+ const fragment = detachWrapperDOM(wrapper);
1309
+ this.pageCache.set(this.layoutCacheKey(this.layoutChainIdentity(layoutChain)), {
1310
+ views: [...layoutChain],
1311
+ fragment,
1312
+ layoutPath: null,
1313
+ scroll: { x: 0, y: 0 },
1314
+ ttl,
1315
+ });
569
1316
  }
570
1317
 
571
- private stopBlockContent(): void {
572
- // BlockManager tracks mounted children per outlet
573
- for (const [name, children] of (this.blockManager as any).mountedChildren) {
574
- if (Array.isArray(children)) {
575
- for (const child of children) {
576
- if (child && typeof child.stop === 'function') {
577
- child.stop();
578
- }
579
- }
1318
+ /**
1319
+ * Layout lấy từ store đang PAUSED (đã vào cache trước đó) → reattach
1320
+ * fragment + resume thay vì mount lại. Trả false nếu layout không paused
1321
+ * (layout mới tạo → caller mount bình thường).
1322
+ */
1323
+ private resumeLayoutChainFromCache(layoutChain: ViewInterface[]): boolean {
1324
+ if (layoutChain.length === 0
1325
+ || layoutChain.some(layout => layout.__ctrl__.lifecycleState !== 'paused')) return false;
1326
+
1327
+ const entry = this.pageCache.take(this.layoutCacheKey(this.layoutChainIdentity(layoutChain)));
1328
+ if (entry) {
1329
+ this.rootElement!.getElement().appendChild(entry.fragment);
1330
+ for (const layout of layoutChain) layout.__ctrl__.resume();
1331
+ } else {
1332
+ return false;
1333
+ }
1334
+ for (const layout of layoutChain) this.reregisterLayoutOutlets(layout.__ctrl__);
1335
+ return true;
1336
+ }
1337
+
1338
+ /** Đăng ký lại outlets của layout vừa resume vào BlockManager registry */
1339
+ private reregisterLayoutOutlets(layoutCtrl: ViewControllerInterface): void {
1340
+ const elements: Map<string, any> | undefined = (layoutCtrl as any).elements;
1341
+ if (!elements) return;
1342
+ for (const [id, el] of elements) {
1343
+ if (el instanceof BlockOutlet && !(el as any).__destroyed__) {
1344
+ this.blockManager.addOutlet(id, el);
580
1345
  }
581
1346
  }
582
1347
  }
583
1348
 
584
- // ─── Lifecycle: Start ───────────────────────────────────────
1349
+ /**
1350
+ * Navigate rời một page: pause + detach DOM → PageCache.
1351
+ * - Standalone: detach toàn vùng wrapper (markers + content).
1352
+ * - Page thuộc layout: detach block content THEO OUTLET — layout giữ nguyên,
1353
+ * không hook nào fire trên layout.
1354
+ * View khai cache:false (hoặc ttl 0) → destroy luôn.
1355
+ */
1356
+ private deactivatePage(
1357
+ pageView: ViewInterface,
1358
+ layoutView: ViewInterface | null,
1359
+ layoutChain: ViewInterface[] = this.currentLayoutChain,
1360
+ ): void {
1361
+ const ctrl = pageView.__ctrl__;
1362
+ const urlPath = ctrl.urlPath;
1363
+ const cacheConfig = ctrl.getConfig('cache');
1364
+ const ttl = typeof cacheConfig === 'object' && cacheConfig?.ttl != null ? cacheConfig.ttl : undefined;
1365
+
1366
+ if (!urlPath || cacheConfig === false || ttl === 0) {
1367
+ ctrl.destroy();
1368
+ return;
1369
+ }
1370
+
1371
+ const scroll = {
1372
+ x: typeof window !== 'undefined' ? window.scrollX ?? 0 : 0,
1373
+ y: typeof window !== 'undefined' ? window.scrollY ?? 0 : 0,
1374
+ };
585
1375
 
586
- private startViewChain(pageView: ViewInterface, finalView: ViewInterface, hasSuperView: boolean): void {
587
- if (hasSuperView) {
588
- // 1. Start layout element tree
589
- this.startLayoutView(finalView);
590
- // 2. Start page's block content
591
- this.startBlockContent();
592
- // 3. Fire page onMounted
593
- if (typeof pageView.onMounted === 'function') {
594
- try { pageView.onMounted(); }
595
- catch (e) { logger.error(`[ViewManager] page onMounted error:`, e); }
1376
+ if (!layoutView) {
1377
+ const wrapper = ctrl.mainElement;
1378
+ if (!wrapper) {
1379
+ ctrl.destroy();
1380
+ return;
596
1381
  }
1382
+ // pause TRƯỚC detach: flush pending → DOM snapshot nhất quán rồi mới rời
1383
+ ctrl.pause();
1384
+ const fragment = detachWrapperDOM(wrapper);
1385
+ this.pageCache.set(this.cacheKey(ctrl.path, urlPath), {
1386
+ views: [pageView], fragment, layoutPath: null, scroll, ttl,
1387
+ });
597
1388
  } else {
598
- // Standalone view — start wrapper tree directly
599
- const wrapper = pageView.__ctrl__.mainElement;
600
- if (wrapper && typeof wrapper.start === 'function') {
601
- wrapper.start();
602
- }
603
- if (typeof pageView.onMounted === 'function') {
604
- try { pageView.onMounted(); }
605
- catch (e) { logger.error(`[ViewManager] onMounted error:`, e); }
606
- }
1389
+ ctrl.pause();
1390
+ const outletContents = this.blockManager.detachPageContent(ctrl.viewId);
1391
+ this.pageCache.set(this.cacheKey(ctrl.path, urlPath), {
1392
+ views: [pageView],
1393
+ outletContents,
1394
+ layoutPath: this.layoutChainIdentity(layoutChain.length > 0 ? layoutChain : [layoutView]),
1395
+ scroll,
1396
+ ttl,
1397
+ });
607
1398
  }
608
1399
  }
609
1400
 
610
- private startLayoutView(layoutView: ViewInterface): void {
611
- const wrapper = layoutView.__ctrl__.mainElement;
612
- if (wrapper && typeof wrapper.start === 'function') {
613
- wrapper.start();
614
- }
615
- if (typeof layoutView.onMounted === 'function') {
616
- try { layoutView.onMounted(); }
617
- catch (e) { logger.error(`[ViewManager] layout onMounted error:`, e); }
618
- }
1401
+ /** Roll back an early Layout-page detach when prepare/render did not commit. */
1402
+ private restoreDeactivatedPage(
1403
+ pageView: ViewInterface,
1404
+ layoutView: ViewInterface | null,
1405
+ ): boolean {
1406
+ const ctrl = pageView.__ctrl__;
1407
+ if (!ctrl.urlPath) return false;
1408
+ const entry = this.pageCache.take(this.cacheKey(ctrl.path, ctrl.urlPath));
1409
+ if (!entry) return false;
1410
+ return this.restoreFromCache(entry, layoutView, 'pop') !== null;
619
1411
  }
620
1412
 
621
- private startBlockContent(): void {
622
- for (const [name, children] of (this.blockManager as any).mountedChildren) {
623
- if (Array.isArray(children)) {
624
- for (const child of children) {
625
- if (child && typeof child.start === 'function') {
626
- child.start();
1413
+ /**
1414
+ * Cache hit: gắn lại DOM + resume — không render, không gọi API.
1415
+ * - Entry standalone: layout cũ (nếu còn) pause+cache → gắn fragment vào container.
1416
+ * - Entry layout-page: layout đang mount trùng layoutPath → restore thẳng
1417
+ * vào outlets. Không trùng → thử resurrect layout từ layout cache
1418
+ * (bfcache đầy đủ: restore CẢ layout LẪN page trong một lần). Layout
1419
+ * cũng không có trong cache → putBack entry (còn TTL thì lần điều hướng
1420
+ * sau restore được), trả null để caller mount tươi.
1421
+ * Scroll: pop → khôi phục vị trí cũ; push → về đầu trang.
1422
+ */
1423
+ private restoreFromCache(
1424
+ entry: PageCacheEntry,
1425
+ oldLayoutView: ViewInterface | null,
1426
+ navigationType: RouterNavigationType
1427
+ ): any {
1428
+ const pageView = entry.views[entry.views.length - 1];
1429
+
1430
+ if (entry.layoutPath) {
1431
+ // ── Page thuộc layout ────────────────────────────────────────
1432
+ // 1. Layout đang mount trùng path? → dùng luôn
1433
+ let layoutChain = this.layoutChainIdentity(this.currentLayoutChain) === entry.layoutPath
1434
+ ? [...this.currentLayoutChain]
1435
+ : [];
1436
+ let layoutView: ViewInterface | null = layoutChain[0] ?? null;
1437
+
1438
+ // 2. Không trùng → thử resurrect layout từ layout cache
1439
+ if (!layoutView && entry.outletContents) {
1440
+ const layoutEntry = this.pageCache.take(this.layoutCacheKey(entry.layoutPath));
1441
+ if (layoutEntry) {
1442
+ if (this.currentLayoutChain.length > 0) {
1443
+ this.deactivateLayoutChain(this.currentLayoutChain);
627
1444
  }
1445
+ layoutChain = [...layoutEntry.views];
1446
+ const lv = layoutChain[0];
1447
+ this.rootElement!.getElement().appendChild(layoutEntry.fragment);
1448
+ for (const layout of layoutChain) layout.__ctrl__.resume();
1449
+ for (const layout of layoutChain) this.reregisterLayoutOutlets(layout.__ctrl__);
1450
+ layoutView = lv;
628
1451
  }
629
1452
  }
630
- }
631
- }
632
1453
 
633
- // ─── Lifecycle: Commit Data ─────────────────────────────────
1454
+ // 3. Không có layout để restore vào → trả entry lại cache, mount tươi
1455
+ if (!layoutView || !entry.outletContents) {
1456
+ this.pageCache.putBack(entry);
1457
+ return null;
1458
+ }
634
1459
 
635
- private commitViewChain(pageView: ViewInterface, finalView: ViewInterface, hasSuperView: boolean): void {
636
- if (hasSuperView) {
637
- finalView.__ctrl__.commitData();
638
- pageView.__ctrl__.commitData();
1460
+ this.blockManager.restorePageContent(pageView.__ctrl__.viewId, entry.outletContents);
1461
+ for (const v of entry.views) {
1462
+ v.__ctrl__.resume();
1463
+ }
1464
+ for (const v of entry.views) {
1465
+ this.sectionManager.mountViewSections(v.__ctrl__.viewId);
1466
+ }
1467
+ this.currentPageView = pageView;
1468
+ this.currentLayoutView = layoutView;
1469
+ this.currentLayoutChain = layoutChain;
1470
+ this.currentLayoutPath = layoutView.__ctrl__.path;
1471
+ this.currentViewType = 'layout';
1472
+ this.viewStack = [...layoutChain, pageView];
639
1473
  } else {
640
- pageView.__ctrl__.commitData();
1474
+ // ── Standalone ───────────────────────────────────────────────
1475
+ if (this.currentLayoutChain.length > 0) {
1476
+ this.deactivateLayoutChain(this.currentLayoutChain);
1477
+ this.currentLayoutView = null;
1478
+ this.currentLayoutChain = [];
1479
+ this.currentLayoutPath = null;
1480
+ }
1481
+ const container = this.rootElement!.getElement();
1482
+ container.appendChild(entry.fragment);
1483
+ for (const v of entry.views) {
1484
+ v.__ctrl__.resume();
1485
+ }
1486
+ for (const v of entry.views) {
1487
+ this.sectionManager.mountViewSections(v.__ctrl__.viewId);
1488
+ }
1489
+ this.currentPageView = pageView;
1490
+ this.currentLayoutView = null;
1491
+ this.currentLayoutChain = [];
1492
+ this.currentLayoutPath = null;
1493
+ this.currentViewType = 'view';
1494
+ this.viewStack = [...entry.views];
641
1495
  }
642
- }
1496
+ this.renderCount++;
643
1497
 
644
- // ─── Unmount ────────────────────────────────────────────────
1498
+ try {
1499
+ if (typeof window !== 'undefined' && typeof window.scrollTo === 'function') {
1500
+ if (navigationType === 'pop') {
1501
+ window.scrollTo(entry.scroll.x, entry.scroll.y);
1502
+ } else {
1503
+ window.scrollTo(0, 0);
1504
+ }
1505
+ }
1506
+ } catch { /* jsdom không hỗ trợ — bỏ qua */ }
645
1507
 
646
- private unmountLayoutDOM(layoutView: ViewInterface | null): void {
647
- if (!layoutView) return;
648
- const wrapper = layoutView.__ctrl__.mainElement;
649
- if (wrapper && typeof wrapper.destroy === 'function') {
650
- wrapper.destroy();
651
- }
1508
+ return {
1509
+ type: 'restored',
1510
+ view: pageView,
1511
+ finalView: this.currentLayoutView ?? pageView,
1512
+ superView: this.currentLayoutView,
1513
+ };
652
1514
  }
653
1515
 
1516
+ // ─── Unmount ────────────────────────────────────────────────
1517
+
654
1518
  unmountAll(): void {
655
- this.stopPageView(this.currentPageView);
656
- this.stopLayoutView(this.currentLayoutView);
657
- this.blockManager.clearAllOutlets();
658
- this.blockManager.destroy();
1519
+ // Stop block content (unsubscribe) trước, rồi destroy chain trong → ngoài.
1520
+ // ctrl.destroy() tự fire đủ hook: stopping/stopped → unmounting/unmounted → destroyed.
1521
+ this.blockManager.stopAll();
1522
+ this.sectionManager.stopAll();
659
1523
 
660
1524
  if (this.currentPageView) {
661
1525
  this.currentPageView.__ctrl__.destroy();
662
1526
  }
663
- if (this.currentLayoutView) {
664
- this.currentLayoutView.__ctrl__.destroy();
1527
+ for (let i = this.currentLayoutChain.length - 1; i >= 0; i--) {
1528
+ this.currentLayoutChain[i].__ctrl__.destroy();
665
1529
  }
666
1530
 
1531
+ this.blockManager.clearAllOutlets();
1532
+ this.blockManager.destroy();
1533
+ this.sectionManager.destroy();
1534
+
667
1535
  this.currentPageView = null;
668
1536
  this.currentLayoutView = null;
1537
+ this.currentLayoutChain = [];
669
1538
  this.currentLayoutPath = null;
670
1539
  this.currentViewType = null;
671
- this.activeViews.clear();
672
1540
  this.viewStack = [];
673
1541
  }
674
1542
 
675
- unmountView(path: string): void {
676
- const info = this.activeViews.get(path);
677
- if (!info) return;
678
- this.activeViews.delete(path);
1543
+ /**
1544
+ * Apply an atomic context update received from a JSON response before the
1545
+ * Router retries navigation with the newly materialized route table.
1546
+ */
1547
+ requiresReloadForViewContext(state: Record<string, any>): boolean {
1548
+ const revision = typeof state?.revision === 'string' ? state.revision : null;
1549
+ if (!revision || revision === this.contextRevision) return false;
1550
+
1551
+ const nextViews = typeof state?.views === 'string' && state.views !== ''
1552
+ ? state.views
1553
+ : this.contextViews;
1554
+
1555
+ // The current registry was built for one namespace. Changing only its
1556
+ // string prefix cannot materialize the new theme factories/CSS and can
1557
+ // mix new Blade names with old JavaScript. Until a staged registry swap
1558
+ // exists, a document load is the only coherent transition.
1559
+ return nextViews !== this.contextViews;
1560
+ }
1561
+
1562
+ applyViewContext(state: Record<string, any>): boolean {
1563
+ if (this.requiresReloadForViewContext(state)) {
1564
+ // Router owns the document transition. A direct caller must not
1565
+ // mutate revision/systemData while the matching registry is absent.
1566
+ return false;
1567
+ }
1568
+
1569
+ const revision = typeof state?.revision === 'string' ? state.revision : null;
1570
+ if (!revision || revision === this.contextRevision) return false;
1571
+
1572
+ this.cancelNavigation();
1573
+ this.contextRevision = revision;
1574
+ this.contextViews = typeof state.views === 'string' ? state.views : this.contextViews;
1575
+ if (state.systemData && typeof state.systemData === 'object') {
1576
+ this.systemData = { ...this.systemData, ...state.systemData };
1577
+ }
1578
+
1579
+ // Cached pages/layout factories belong to the previous namespace.
1580
+ this.pageCache.clear();
1581
+ this.store.clear();
1582
+ this.ssrBoot = null;
1583
+
1584
+ if (typeof window !== 'undefined') {
1585
+ const appConfigs = (window as any).APP_CONFIGS ??= {};
1586
+ appConfigs.view ??= {};
1587
+ appConfigs.view.revision = revision;
1588
+ appConfigs.view.contextViews = this.contextViews;
1589
+ appConfigs.view.systemData = { ...this.systemData };
1590
+ }
1591
+
1592
+ return true;
1593
+ }
1594
+
1595
+ getContextRevision(): string | null {
1596
+ return this.contextRevision;
1597
+ }
1598
+
1599
+ private extractAsyncData(response: any): Record<string, any> {
1600
+ const payload = response?.data ?? {};
1601
+ if (payload && typeof payload === 'object'
1602
+ && Object.prototype.hasOwnProperty.call(payload, 'data')
1603
+ && (Object.prototype.hasOwnProperty.call(payload, 'viewContext')
1604
+ || Object.prototype.hasOwnProperty.call(payload, 'view'))) {
1605
+ return payload.data ?? {};
1606
+ }
1607
+ return payload;
1608
+ }
1609
+
1610
+ /**
1611
+ * hydrateView — Hydrate một view đã được server-side render.
1612
+ *
1613
+ * SSR flow:
1614
+ * 1. Laravel render Blade template → HTML gửi về client
1615
+ * 2. Blade dùng $__VIEW_ID__ để prefix class của mỗi element (e.g. "v12345-af0882bc")
1616
+ * 3. Laravel truyền $__VIEW_ID__ về client qua page data (__SSR_VIEW_ID__)
1617
+ * 4. Client gọi hydrateView() — view được tạo với cùng viewId
1618
+ * 5. Html elements tìm server-rendered DOM nodes bằng class {viewId}-{elementId}
1619
+ * 6. Reactive regions claim server markers via markerRegistry.claim()
1620
+ * 7. Event handlers và state subscriptions được gắn vào DOM đã có
1621
+ *
1622
+ * Lưu ý: Không gây layout shift vì cấu trúc DOM được reuse (Html claim),
1623
+ * chỉ text nodes và markers được re-tạo (nội dung giống nhau nên không thấy được).
1624
+ *
1625
+ * @param name - View registry key (e.g. 'web.home')
1626
+ * @param data - View data, PHẢI chứa __SSR_VIEW_ID__ từ server
1627
+ * @param route - Active route (optional)
1628
+ *
1629
+ * Tham chiếu: COMPILER_CONTRACT.md §hydration, Html.ts §constructor
1630
+ */
1631
+ /**
1632
+ * hydrateView — Hydrate một view đã được server-side render (SSR).
1633
+ *
1634
+ * Flow:
1635
+ * 1. Server (Laravel) render Blade → HTML gửi về client
1636
+ * 2. Blade dùng $__VIEW_ID__ prefix class mỗi element ("v12345-af0882bc")
1637
+ * 3. Client gọi hydrateView() với __SSR_VIEW_ID__ = cùng viewId
1638
+ * 4. Html elements claim server DOM nodes bằng class {viewId}-{elementId}
1639
+ * 5. Output claim comment markers + text nodes giữa markers
1640
+ * 6. Event handlers và state subscriptions gắn vào DOM có sẵn
1641
+ *
1642
+ * Khác mountView:
1643
+ * - Không xoá DOM cũ, không tạo HTML element mới (claim cái đã có)
1644
+ * - Markers (Reactive, Output) vẫn được tạo mới nếu server không emit
1645
+ * - Sau hydrate, SPA navigation dùng mountView bình thường (CSR)
1646
+ */
1647
+ async hydrateView(
1648
+ name: string,
1649
+ data: Record<string, any> & { __SSR_VIEW_ID__: string },
1650
+ route?: ActiveRouteInterface,
1651
+ ): Promise<any> {
1652
+ // Không có viewId từ server → không hydrate được, fallback CSR
1653
+ if (!data.__SSR_VIEW_ID__) {
1654
+ console.warn('[ViewManager] hydrateView: __SSR_VIEW_ID__ không có. Fallback → mountView.');
1655
+ return this.mountView(name, data, route);
1656
+ }
1657
+
1658
+ const navigationGeneration = ++this.navigationGeneration;
1659
+
1660
+ // HTML server của lượt này là nguồn duy nhất để claim marker — bỏ index
1661
+ // của lượt hydrate trước (bfcache / test / re-hydrate) trước khi quét.
1662
+ markerRegistry.invalidateIndex();
1663
+
1664
+ const targetUrl = (route as any)?.$uri ?? route?.$urlPath ?? name;
1665
+
1666
+ // Router only supplies route params and the SSR id. Seed the page from
1667
+ // the matching server instance before constructing data-derived states.
1668
+ const { __SSR_VIEW_ID__: ssrViewId, ...routeData } = data;
1669
+ const key = this.resolveRegistryKey(name);
1670
+ const serverData = this.ssrViewData[key]?.instances?.[ssrViewId]?.data ?? {};
1671
+ const viewData = { ...serverData, ...routeData };
1672
+ // Compiled factories capture this id in their render closures; assigning
1673
+ // ctrl.viewId afterwards alone cannot update those closures.
1674
+ const constructorData = { ...viewData, __SSR_VIEW_ID__: ssrViewId };
1675
+ const view = await this.view(name, constructorData, false);
1676
+ // Keep the internal id out of application data after factory setup.
1677
+ delete (constructorData as Record<string, any>).__SSR_VIEW_ID__;
1678
+ if (!view) {
1679
+ this.showError(`hydrateView: View "${name}" không tìm thấy.`);
1680
+ return null;
1681
+ }
1682
+ view.__ctrl__.urlPath = targetUrl;
1683
+
1684
+ // Ghi đè viewId bằng SSR viewId — đảm bảo Html elements claim đúng
1685
+ // server DOM nodes có class prefix = viewId (e.g. "vssr-abc12-page-root").
1686
+ // (Belt-and-suspenders: đúng cả khi factory không đọc __SSR_VIEW_ID__.)
1687
+ view.__ctrl__.viewId = ssrViewId;
1688
+
1689
+ // Render ở HYDRATE mode — elements claim DOM thay vì tạo mới
1690
+ const renderResult = await this.renderPageView(
1691
+ view, viewData, this.rootElement, InitModes.HYDRATE, false, 0, navigationGeneration
1692
+ );
1693
+ if (renderResult.type === 'cancelled') {
1694
+ view.__ctrl__.destroy();
1695
+ return null;
1696
+ }
1697
+ if (renderResult.type === 'error') {
1698
+ view.__ctrl__.destroy();
1699
+ this.showError(renderResult.message);
1700
+ return null;
1701
+ }
1702
+
1703
+ // Lưới cuối như mountView — hydrate claim lỗi không được làm chết cả app.
1704
+ try {
1705
+ this.activateRenderedChain(renderResult, InitModes.HYDRATE);
1706
+ } catch (err) {
1707
+ logger.error(`Error hydrating view "${name}":`, err);
1708
+ this.showError(`Error hydrating view "${name}".`, err instanceof Error ? err.message : err);
1709
+ return null;
1710
+ } finally {
1711
+ // Hết lượt hydrate thì KHÔNG ai đọc index nữa (claim chỉ có ở đường
1712
+ // HYDRATE, mà mỗi lần nạp trang chỉ hydrate một lượt) — giữ lại chỉ
1713
+ // để Map trỏ vào comment của trang đầu suốt phiên: đo trên /docs
1714
+ // thấy 94/106 entry thành node rời DOM ngay sau lần SPA nav đầu.
1715
+ // Claim đến muộn (@await hydrate sau) vẫn đúng: miss sẽ tự dựng lại.
1716
+ markerRegistry.invalidateIndex();
1717
+ }
1718
+
1719
+ return renderResult;
679
1720
  }
680
1721
 
681
1722
  // ─── Getters ────────────────────────────────────────────────
@@ -688,6 +1729,32 @@ export class ViewManager implements ViewManagerInterface {
688
1729
  return this.currentPageView;
689
1730
  }
690
1731
 
1732
+ /** Layout chain đang mount, ngoài → trong (devtools/debug). */
1733
+ getLayoutChain(): ViewInterface[] {
1734
+ return [...this.currentLayoutChain];
1735
+ }
1736
+
1737
+ // ─── SSR boot ───────────────────────────────────────────────
1738
+
1739
+ /** Còn SSR boot chưa consume? (route đầu tiên nên hydrate thay vì mount) */
1740
+ hasSSRBoot(): boolean {
1741
+ return this.ssrBoot !== null;
1742
+ }
1743
+
1744
+ /**
1745
+ * Lấy viewId SSR cho một view name nếu nó là entry server đã render, rồi
1746
+ * CONSUME (xoá) — đảm bảo chỉ route ĐẦU TIÊN hydrate; navigate sau là CSR.
1747
+ * Trả null nếu không có SSR boot hoặc view không khớp entry.
1748
+ */
1749
+ consumeSSRViewId(viewName: string): string | null {
1750
+ if (this.ssrBoot && this.ssrBoot.view === viewName) {
1751
+ const id = this.ssrBoot.viewId;
1752
+ this.ssrBoot = null;
1753
+ return id;
1754
+ }
1755
+ return null;
1756
+ }
1757
+
691
1758
  getViewStack(): ViewInterface[] {
692
1759
  return this.viewStack;
693
1760
  }
@@ -698,3 +1765,10 @@ export class ViewManager implements ViewManagerInterface {
698
1765
 
699
1766
 
700
1767
  }
1768
+
1769
+ /** Remap only implicit page-data requests; explicit @await URLs keep their meaning. */
1770
+ export function resolveViewDataUrl(routeUrl: string, dataEndpoint?: string | null): string {
1771
+ if (!dataEndpoint) return routeUrl;
1772
+ const route = new URL(routeUrl, typeof window !== 'undefined' ? window.location.href : 'http://localhost');
1773
+ return dataEndpoint.replace(/\/+$/, '') + route.pathname + route.search;
1774
+ }