@saolabs/client 1.0.0 → 1.1.0

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