@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
@@ -1,3 +1,4 @@
1
+ import devtools from "../devtools/hook.js";
1
2
  /**
2
3
  * StateManager — manages reactive state for a ViewController.
3
4
  *
@@ -19,14 +20,17 @@
19
20
  export class StateManager {
20
21
  constructor(stateInstance, controller) {
21
22
  this.states = {};
22
- this.listeners = new Map();
23
- this.multiKeyListeners = [];
23
+ this.subscriptions = new Map();
24
24
  this.pendingChanges = new Set();
25
+ this.versions = new Map();
25
26
  this.stateIndex = 0;
26
27
  this.flushRAF = null;
27
28
  this.hasPendingFlush = false;
28
29
  this.isFlushing = false;
29
30
  this._isDestroyed = false;
31
+ // Browsers opt in explicitly; production never scans unrelated state objects.
32
+ this.mutationDiagnostics = !!globalThis.process?.env?.NODE_ENV
33
+ && globalThis.process.env.NODE_ENV !== 'production';
30
34
  /** Flag — cho phép update state qua update$xxx chỉ trước lock */
31
35
  this._canUpdateStateByKey = true;
32
36
  /** Setter functions exposed for direct property assignment on ViewState */
@@ -35,6 +39,33 @@ export class StateManager {
35
39
  this.controller = null;
36
40
  /** Properties that should NOT become state keys */
37
41
  this.ownProperties = ['__', 'on', 'off', 'unsubscribe'];
42
+ this.computedNodes = new Map();
43
+ this.computedDependents = new Map();
44
+ // ─── Pause / Resume (dirty tracking) ────────────────────────
45
+ // Thiết kế: ROUTE_RENDER_FLOW.md §7, §8.2-8.3.
46
+ // Khi paused: state VẪN nhận giá trị mới, nhưng không notify listener —
47
+ // key đổi được ghi vào dirtyKeys. resume() flush đúng các key dirty.
48
+ this._isPaused = false;
49
+ this.dirtyKeys = new Set();
50
+ // ─── Phát hiện mutate tại chỗ KHÔNG kèm set ──────────────────
51
+ // `warnSameReference` chỉ bắt được `list.push(x); setList(list)` — có đi qua
52
+ // setter. Trường hợp còn lại KHÔNG đi qua đâu cả:
53
+ // list.push(x); // hết. Không cập nhật, không cảnh báo.
54
+ // Chỗ duy nhất còn quan sát được là lúc flush: so snapshot NÔNG của lần
55
+ // flush trước với giá trị hiện tại. Reference y nguyên mà nội dung đã khác
56
+ // ⇒ ai đó mutate ngoài luồng.
57
+ /** Bản sao nông của lần flush gần nhất, theo key. */
58
+ this.mutationSnapshots = new Map();
59
+ /**
60
+ * Một kênh cho MỘT key. `nodes` giữ đúng tập node đang reachable để cleanup
61
+ * không phụ thuộc vào hình dạng cây tại thời điểm destroy.
62
+ */
63
+ this.trackedChannels = new Map();
64
+ /**
65
+ * Key vừa được hook mutate xử lý trong chu kỳ hiện tại — `detectExternalMutation`
66
+ * dựa vào đây để chụp lại mà KHÔNG cảnh báo. Xoá sau mỗi lần quét.
67
+ */
68
+ this.hookHandledKeys = new Set();
38
69
  this.stateInstance = stateInstance;
39
70
  this.controller = controller ?? null;
40
71
  }
@@ -65,8 +96,10 @@ export class StateManager {
65
96
  if (!this._canUpdateStateByKey)
66
97
  return;
67
98
  for (const key in stateMap) {
68
- if (stateMap.hasOwnProperty(key) && this.states[key]) {
99
+ if (stateMap.hasOwnProperty(key) && this.states[key] && !this.computedNodes.has(key)) {
69
100
  this.states[key].value = stateMap[key];
101
+ this.trackArray(key, stateMap[key]);
102
+ this.invalidateComputed(key, false);
70
103
  }
71
104
  }
72
105
  }
@@ -80,17 +113,40 @@ export class StateManager {
80
113
  * `viewState.count` reads/writes the state reactively.
81
114
  */
82
115
  useState(value, key) {
116
+ let stateKey;
117
+ if (key === undefined || key === null) {
118
+ do
119
+ stateKey = String(this.stateIndex++);
120
+ while (this.states[stateKey]);
121
+ }
122
+ else {
123
+ stateKey = String(key);
124
+ }
83
125
  // If key already exists, return existing state
84
- if (key !== undefined && key !== null && this.states[key]) {
85
- return [this.states[key].value, this.states[key].setValue, key];
126
+ if (this.states[stateKey]) {
127
+ return [this.states[stateKey].value, this.states[stateKey].setValue, stateKey];
86
128
  }
87
- const stateKey = String(key ?? this.stateIndex++);
88
129
  const setValue = (newValue) => {
89
130
  const oldValue = this.states[stateKey].value;
90
131
  this.states[stateKey].value = newValue;
91
- this.commitStateChange(stateKey, oldValue);
132
+ this.trackArray(stateKey, newValue);
133
+ // fromSetter: đây là đường DEV tự set (`state.x = v` / `set$x(v)`).
134
+ // Đường props plumbing (updateStateByKey) re-pass cùng ref là bình
135
+ // thường nên không cảnh báo — xem warnSameReference.
136
+ this.commitStateChange(stateKey, oldValue, true);
92
137
  };
93
138
  this.states[stateKey] = { value, setValue, key: stateKey };
139
+ // Gieo bản chụp NGAY khi khai báo, không đợi flush đầu tiên. `mutatedInPlace`
140
+ // coi "chưa có bản chụp" là ĐÃ ĐỔI (an toàn: thà render thừa còn hơn nuốt
141
+ // cập nhật) — thiếu baseline thì lần set-cùng-ref đầu tiên luôn bị tính là
142
+ // thay đổi, kể cả khi thật ra không đổi gì.
143
+ if (this.mutationDiagnostics && value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
144
+ this.mutationSnapshots.set(stateKey, {
145
+ ref: value,
146
+ copy: StateManager.shallowCopy(value),
147
+ });
148
+ }
149
+ this.trackArray(stateKey, value);
94
150
  this.setters[stateKey] = setValue;
95
151
  // Define reactive property on ViewState if not a reserved name
96
152
  if (!this.ownProperties.includes(stateKey)) {
@@ -109,17 +165,133 @@ export class StateManager {
109
165
  return [value, setValue, stateKey];
110
166
  }
111
167
  // ─── State Access ───────────────────────────────────────────
112
- /** Register shorthand — just returns the setter */
168
+ /**
169
+ * Register shorthand — pre-declare a state slot, returns setter.
170
+ *
171
+ * Compiler pattern:
172
+ * const set$count = __STATE__.__.register('count');
173
+ * // initial value set later in commitConstructorData:
174
+ * update$count(0); → updateStateByKey('count', 0)
175
+ *
176
+ * Value is optional (defaults to undefined until commitConstructorData runs).
177
+ */
113
178
  register(key, value) {
114
179
  return this.useState(value, key)[1];
115
180
  }
181
+ /** Invalidate the dependency graph synchronously; evaluation remains lazy. */
182
+ invalidateComputed(key, notify = true) {
183
+ const seen = new Set([key]);
184
+ const pending = [key];
185
+ for (let i = 0; i < pending.length; i++) {
186
+ for (const dependent of this.computedDependents.get(pending[i]) ?? []) {
187
+ if (seen.has(dependent))
188
+ continue;
189
+ seen.add(dependent);
190
+ this.computedNodes.get(dependent)?.dirty();
191
+ this.versions.set(dependent, (this.versions.get(dependent) ?? 0) + 1);
192
+ if (notify)
193
+ (this._isPaused ? this.dirtyKeys : this.pendingChanges).add(dependent);
194
+ pending.push(dependent);
195
+ }
196
+ }
197
+ }
198
+ /**
199
+ * State dẫn xuất có memo hoá (kiểu Vue `computed`).
200
+ *
201
+ * Chỉ tính lại khi 1 trong `deps` đổi, và **lazy**: đánh dấu bẩn lúc dep
202
+ * đổi, tính thật lúc ĐỌC. Deps đổi 5 lần trong 1 batch → tính 1 lần; đổi
203
+ * mà không ai đọc → không tính.
204
+ *
205
+ * Slot nằm chung `states` với state thường nên `getStateByKey(key)`,
206
+ * `viewState[key]` và `subscribe([key])` đều dùng được — Output/Reactive
207
+ * chỉ cần `stateKeys: [key]`, không cần biết đó là computed.
208
+ *
209
+ * @example
210
+ * states.__.computed('fullName', () => `${first} ${last}`, ['first', 'last']);
211
+ * this.output('o', p, true, ['fullName'], () => states.__.getStateByKey('fullName'));
212
+ */
213
+ computed(key, fn, deps = []) {
214
+ if (this._isDestroyed)
215
+ throw new Error('[ViewState] Cannot register computed after destroy.');
216
+ // Validate before replacing any edges, including indirect cycles.
217
+ const reaches = (name, seen = new Set()) => {
218
+ if (name === key)
219
+ return true;
220
+ if (seen.has(name))
221
+ return false;
222
+ seen.add(name);
223
+ return (this.computedNodes.get(name)?.deps ?? []).some(dep => reaches(dep, seen));
224
+ };
225
+ if (deps.some(dep => reaches(dep)))
226
+ throw new Error(`[ViewState] Computed dependency cycle: ${key}`);
227
+ const existing = this.states[key];
228
+ if (existing && !existing.__computed__)
229
+ throw new Error(`[ViewState] State already exists: ${key}`);
230
+ for (const dep of this.computedNodes.get(key)?.deps ?? []) {
231
+ const dependents = this.computedDependents.get(dep);
232
+ dependents?.delete(key);
233
+ if (dependents?.size === 0)
234
+ this.computedDependents.delete(dep);
235
+ }
236
+ let cache;
237
+ let dirty = true;
238
+ let evaluating = false;
239
+ const slot = {
240
+ key,
241
+ __computed__: true,
242
+ setValue: () => console.warn(`[ViewState] computed("${key}") is read-only.`),
243
+ };
244
+ Object.defineProperty(slot, 'value', {
245
+ get: () => {
246
+ if (dirty) {
247
+ if (evaluating)
248
+ throw new Error(`[ViewState] Recursive computed read: ${key}`);
249
+ evaluating = true;
250
+ try {
251
+ cache = fn();
252
+ dirty = false;
253
+ }
254
+ finally {
255
+ evaluating = false;
256
+ }
257
+ }
258
+ return cache;
259
+ },
260
+ set: slot.setValue,
261
+ enumerable: true,
262
+ });
263
+ this.states[key] = slot;
264
+ this.setters[key] = slot.setValue;
265
+ this.computedNodes.set(key, { deps: [...new Set(deps)], dirty: () => { dirty = true; } });
266
+ for (const dep of deps) {
267
+ if (!this.computedDependents.has(dep))
268
+ this.computedDependents.set(dep, new Set());
269
+ this.computedDependents.get(dep).add(key);
270
+ }
271
+ if (!existing && !this.ownProperties.includes(key)) {
272
+ Object.defineProperty(this.stateInstance, key, {
273
+ get: () => this.states[key]?.value,
274
+ set: slot.setValue,
275
+ enumerable: true,
276
+ });
277
+ }
278
+ if (existing)
279
+ this.enqueueChange(key);
280
+ return () => this.getStateByKey(key);
281
+ }
116
282
  /** Update state by key */
117
283
  updateStateByKey(key, value) {
118
- if (!this.states[key])
284
+ const stateKey = String(key);
285
+ if (!this.states[stateKey])
119
286
  return undefined;
120
- const oldValue = this.states[key].value;
121
- this.states[key].value = value;
122
- this.commitStateChange(key, oldValue);
287
+ if (this.computedNodes.has(stateKey)) {
288
+ this.setters[stateKey](value);
289
+ return this.states[stateKey].value;
290
+ }
291
+ const oldValue = this.states[stateKey].value;
292
+ this.states[stateKey].value = value;
293
+ this.trackArray(stateKey, value);
294
+ this.commitStateChange(stateKey, oldValue);
123
295
  return value;
124
296
  }
125
297
  /**
@@ -175,103 +347,212 @@ export class StateManager {
175
347
  }
176
348
  // ─── Subscribe / Unsubscribe ────────────────────────────────
177
349
  subscribe(key, callback) {
178
- // Array of keys
179
- if (Array.isArray(key)) {
180
- if (key.length === 0)
181
- return () => { };
182
- if (key.length === 1 && callback)
183
- return this.subscribe(key[0], callback);
184
- if (typeof callback !== 'function')
185
- return () => { };
186
- const keys = new Set();
187
- for (const k of key) {
188
- if (this.states[k])
189
- keys.add(k);
190
- }
191
- if (keys.size === 0)
192
- return () => { };
193
- const listener = { keys, callback, called: false };
194
- this.multiKeyListeners.push(listener);
195
- return () => {
196
- const idx = this.multiKeyListeners.indexOf(listener);
197
- if (idx !== -1)
198
- this.multiKeyListeners.splice(idx, 1);
199
- };
200
- }
201
- // Object map of keys → callbacks
202
- if (typeof key === 'object' && key !== null) {
203
- const unsubs = {};
204
- for (const k in key) {
205
- unsubs[k] = this.subscribe(k, key[k]);
206
- }
207
- return () => { for (const k in unsubs)
208
- unsubs[k](); };
350
+ if (this._isDestroyed)
351
+ return () => { };
352
+ if (typeof key === 'object' && key !== null && !Array.isArray(key)) {
353
+ const unsubs = Object.entries(key).map(([k, cb]) => this.subscribe(k, cb));
354
+ return () => { for (const off of unsubs)
355
+ off(); };
209
356
  }
210
- // Single key
211
357
  if (typeof callback !== 'function')
212
358
  return () => { };
213
- if (!this.listeners.has(key))
214
- this.listeners.set(key, []);
215
- this.listeners.get(key).push(callback);
216
- const index = this.listeners.get(key).length - 1;
217
- return () => {
218
- const listeners = this.listeners.get(key);
219
- if (listeners) {
220
- listeners.splice(index, 1);
221
- if (listeners.length === 0)
222
- this.listeners.delete(key);
223
- }
224
- };
359
+ const keys = new Set((Array.isArray(key) ? key : [key]).map(String));
360
+ if (keys.size === 0)
361
+ return () => { };
362
+ const record = { keys, callback, multi: Array.isArray(key) && key.length > 1, active: true };
363
+ for (const k of keys) {
364
+ let records = this.subscriptions.get(k);
365
+ if (!records)
366
+ this.subscriptions.set(k, records = new Set());
367
+ records.add(record);
368
+ }
369
+ return () => this.removeSubscription(record);
225
370
  }
226
- unsubscribe(key, callback) {
227
- if (Array.isArray(key)) {
228
- if (key.length === 0)
229
- return;
230
- if (key.length === 1) {
231
- this.unsubscribe(key[0], callback);
232
- return;
233
- }
234
- const keySet = new Set(key);
235
- if (!callback) {
236
- for (let i = this.multiKeyListeners.length - 1; i >= 0; i--) {
237
- if (this.setsEqual(this.multiKeyListeners[i].keys, keySet)) {
238
- this.multiKeyListeners.splice(i, 1);
239
- }
240
- }
241
- return;
242
- }
243
- const idx = this.multiKeyListeners.findIndex(l => l.callback === callback && this.setsEqual(l.keys, keySet));
244
- if (idx !== -1)
245
- this.multiKeyListeners.splice(idx, 1);
371
+ removeSubscription(record) {
372
+ if (!record.active)
246
373
  return;
374
+ record.active = false;
375
+ for (const k of record.keys) {
376
+ const records = this.subscriptions.get(k);
377
+ records?.delete(record);
378
+ if (records?.size === 0)
379
+ this.subscriptions.delete(k);
247
380
  }
248
- if (typeof key === 'object' && key !== null) {
249
- for (const k in key)
250
- this.unsubscribe(k, key[k]);
381
+ }
382
+ unsubscribe(key, callback) {
383
+ if (typeof key === 'object' && key !== null && !Array.isArray(key)) {
384
+ for (const [k, cb] of Object.entries(key))
385
+ this.unsubscribe(k, cb);
251
386
  return;
252
387
  }
253
- if (callback) {
254
- const listeners = this.listeners.get(key);
255
- if (listeners) {
256
- const idx = listeners.indexOf(callback);
257
- if (idx !== -1) {
258
- listeners.splice(idx, 1);
259
- if (listeners.length === 0)
260
- this.listeners.delete(key);
261
- }
388
+ const keys = new Set((Array.isArray(key) ? key : [key]).map(String));
389
+ const multi = Array.isArray(key) && key.length > 1;
390
+ const first = keys.values().next().value;
391
+ if (first === undefined)
392
+ return;
393
+ for (const record of Array.from(this.subscriptions.get(first) ?? [])) {
394
+ if (record.multi === multi && this.setsEqual(record.keys, keys) && (!callback || record.callback === callback)) {
395
+ this.removeSubscription(record);
396
+ if (callback)
397
+ break; // remove one registration, as before
262
398
  }
263
399
  }
264
- else {
265
- this.listeners.delete(key);
400
+ }
401
+ /** Expensive checks for mutations not intercepted by accessors/mutators. */
402
+ setMutationDiagnostics(enabled) {
403
+ this.mutationDiagnostics = enabled;
404
+ this.mutationSnapshots.clear();
405
+ this.hookHandledKeys.clear();
406
+ if (enabled)
407
+ this.detectExternalMutation();
408
+ }
409
+ /** Synchronous revision, including changes not flushed to DOM yet. */
410
+ getStateVersion(key) {
411
+ return this.versions.get(key) ?? 0;
412
+ }
413
+ get isPaused() {
414
+ return this._isPaused;
415
+ }
416
+ /** Chuyển sang dirty-mode. Flush nốt pending changes trước để DOM là snapshot nhất quán. */
417
+ pause() {
418
+ if (this._isPaused || this._isDestroyed)
419
+ return;
420
+ this.flushNow();
421
+ this._isPaused = true;
422
+ }
423
+ /**
424
+ * Thoát dirty-mode. Notify listeners cho đúng các key đã đổi trong lúc paused.
425
+ * Trả về danh sách dirty keys (rỗng = không có gì thay đổi, không render).
426
+ */
427
+ resume() {
428
+ if (!this._isPaused || this._isDestroyed)
429
+ return [];
430
+ this._isPaused = false;
431
+ const dirty = Array.from(this.dirtyKeys);
432
+ this.dirtyKeys.clear();
433
+ if (dirty.length > 0) {
434
+ for (const key of dirty)
435
+ this.pendingChanges.add(key);
436
+ this.flushNow();
437
+ }
438
+ return dirty;
439
+ }
440
+ /** Flush đồng bộ pending changes (huỷ RAF đang chờ nếu có). */
441
+ flushNow() {
442
+ if (this.flushRAF !== null) {
443
+ cancelAnimationFrame(this.flushRAF);
444
+ this.flushRAF = null;
445
+ }
446
+ if (this.pendingChanges.size > 0) {
447
+ this.executeFlush();
266
448
  }
449
+ this.hasPendingFlush = false;
267
450
  }
268
451
  // ─── Batch Flush System ─────────────────────────────────────
269
- commitStateChange(key, _oldValue) {
452
+ commitStateChange(key, _oldValue, fromSetter = false) {
270
453
  if (this._isDestroyed)
271
454
  return;
272
455
  const newValue = this.states[key]?.value;
273
- if (_oldValue === newValue)
456
+ if (_oldValue === newValue) {
457
+ if (fromSetter && !this.mutationDiagnostics && newValue !== null
458
+ && typeof newValue === 'object' && !Object.isFrozen(newValue)) {
459
+ // An explicit same-reference setter declares a mutation. No copy
460
+ // is needed in production, including for new keys/array indexes.
461
+ this.enqueueChange(key);
462
+ return;
463
+ }
464
+ // Cùng reference. Trước đây dừng luôn ⇒ `list.splice(i,1); setList(list)`
465
+ // — cách viết TỰ NHIÊN NHẤT — im lặng không cập nhật gì.
466
+ //
467
+ // Khi dev GỌI SETTER là họ đã tuyên bố ý định "giá trị này vừa đổi".
468
+ // Lúc đó ta đối chiếu NỘI DUNG với bản chụp nông của lần flush trước
469
+ // (`mutationSnapshots` — đã có sẵn cho phần cảnh báo) thay vì chỉ so
470
+ // reference. Khác nội dung ⇒ coi như đổi thật.
471
+ //
472
+ // ĐÂY KHÔNG PHẢI deep reactivity/Proxy (xem GAPS §2.16b — quyết định
473
+ // KHÔNG làm vẫn giữ nguyên): không có dep tracking runtime, không đổi
474
+ // granularity. Vẫn đúng một `enqueueChange(key)` ở tầng key — kết quả
475
+ // y hệt như dev tự viết `state.x = [...state.x]`, chỉ khác là không
476
+ // bắt họ phải nhớ.
477
+ if (fromSetter && this.mutatedInPlace(key, newValue)) {
478
+ this.enqueueChange(key);
479
+ return;
480
+ }
481
+ // Đã có cập nhật ĐANG CHỜ cho key này (thường do hook mutate mảng
482
+ // vừa bắt `push`/`splice` và đã làm mới bản chụp) ⇒ set lại cùng ref
483
+ // chỉ là thừa, KHÔNG phải bug. Không cảnh báo, tránh dương tính giả
484
+ // cho cách viết rất phổ biến: `list.splice(i,1); setList(list)`.
485
+ if (fromSetter && (this.pendingChanges.has(key) || this.dirtyKeys.has(key)))
486
+ return;
487
+ if (fromSetter && this.mutationDiagnostics && !Object.isFrozen(newValue))
488
+ this.warnSameReference(key, newValue);
489
+ return;
490
+ }
491
+ this.enqueueChange(key);
492
+ }
493
+ /**
494
+ * Cùng reference nhưng NỘI DUNG (độ sâu 1) đã khác bản chụp gần nhất?
495
+ *
496
+ * Chưa có bản chụp (state chưa qua flush nào) → trả true: thà render thừa
497
+ * một lần còn hơn nuốt mất cập nhật.
498
+ * Cập nhật lại bản chụp NGAY để hai lần mutate liên tiếp trong cùng một tick
499
+ * đều được nhận, không phải đợi flush làm mới.
500
+ */
501
+ mutatedInPlace(key, value) {
502
+ if (value === null || typeof value !== 'object')
503
+ return false;
504
+ const snap = this.mutationSnapshots.get(key);
505
+ const changed = !snap || snap.ref !== value
506
+ || StateManager.shallowDiffers(snap.copy, value);
507
+ if (changed) {
508
+ this.mutationSnapshots.set(key, { ref: value, copy: StateManager.shallowCopy(value) });
509
+ }
510
+ return changed;
511
+ }
512
+ /**
513
+ * Đến được đây nghĩa là: dev gọi setter, cùng reference, VÀ nội dung ở độ
514
+ * sâu 1 KHÔNG khác gì (`mutatedInPlace` đã trả false). Còn đúng hai khả năng:
515
+ * - no-op thật (set lại y nguyên) — vô hại;
516
+ * - mutate LỒNG SÂU (`user.profile.name = 'x'`) — `shallowDiffers` chỉ so
517
+ * độ sâu 1 nên không thấy. Đây mới là ca cần cảnh báo.
518
+ * Mutate nông (`push`/`splice`/gán phần tử) rồi set KHÔNG còn tới đây nữa —
519
+ * nó đã được `mutatedInPlace` nhận và cập nhật bình thường.
520
+ *
521
+ * Hai lớp lọc để không có dương tính giả:
522
+ * - chỉ object/array (set lại cùng số/chuỗi là bình thường, vô hại)
523
+ * - chỉ đường `setValue` (dev tự set). Đường `updateStateByKey` —
524
+ * `update$x()` lúc init và `__UPDATE_DATA_TRAIT__` khi cha truyền
525
+ * props — hoàn toàn có thể re-pass đúng ref cũ một cách hợp lệ.
526
+ * Kèm warn-once theo view+key để không spam.
527
+ */
528
+ warnSameReference(key, value) {
529
+ if (value === null || typeof value !== 'object')
530
+ return;
531
+ const path = this.controller?.path ?? '';
532
+ const warnKey = `${path}::${String(key)}`;
533
+ if (StateManager.warnedKeys.has(warnKey))
534
+ return;
535
+ StateManager.warnedKeys.add(warnKey);
536
+ console.warn(`[ViewState] "${String(key)}"${path ? ` (view "${path}")` : ''} được set bằng CHÍNH ` +
537
+ `reference cũ và nội dung ở cấp 1 KHÔNG đổi → không có gì cập nhật. ` +
538
+ `Nếu vừa sửa dữ liệu LỒNG SÂU (vd state.${String(key)}.a.b = ...), hãy tạo ` +
539
+ `object/array MỚI ở cấp ngoài: state.${String(key)} = { ...cũ, a: { ...cũ.a, b } }.`);
540
+ }
541
+ /**
542
+ * Đưa key vào hàng đợi flush, KHÔNG so sánh giá trị.
543
+ * Tách khỏi commitStateChange để computed dùng được: so sánh sẽ phải ĐỌC
544
+ * `states[key].value` → kích hoạt tính lại ngay, mất tính lazy.
545
+ */
546
+ enqueueChange(key) {
547
+ if (this._isDestroyed)
548
+ return;
549
+ this.versions.set(String(key), (this.versions.get(String(key)) ?? 0) + 1);
550
+ this.invalidateComputed(key);
551
+ // Paused → ghi sổ, không notify (giá trị đã được set vào states)
552
+ if (this._isPaused) {
553
+ this.dirtyKeys.add(key);
274
554
  return;
555
+ }
275
556
  this.pendingChanges.add(key);
276
557
  if (!this.hasPendingFlush) {
277
558
  this.hasPendingFlush = true;
@@ -283,57 +564,459 @@ export class StateManager {
283
564
  return;
284
565
  try {
285
566
  this.isFlushing = true;
286
- this.flushChanges();
567
+ // Listener CÓ THỂ enqueue key mới ngay trong lúc flush (computed
568
+ // phụ thuộc computed). flushChanges() đã snapshot xong nên key mới
569
+ // sẽ nằm lại hàng đợi; lặp cho tới khi lắng, trong CÙNG frame —
570
+ // nếu không, cập nhật dẫn xuất kẹt tới lần state đổi kế tiếp.
571
+ let depth = 0;
572
+ while (this.pendingChanges.size > 0 && depth < StateManager.MAX_CASCADE) {
573
+ this.flushChanges();
574
+ depth++;
575
+ }
576
+ if (this.pendingChanges.size > 0) {
577
+ console.warn('[ViewState] Cascade update chưa lắng sau '
578
+ + `${StateManager.MAX_CASCADE} vòng — nghi computed phụ thuộc vòng:`, Array.from(this.pendingChanges));
579
+ this.pendingChanges.clear();
580
+ }
287
581
  }
288
582
  finally {
289
583
  this.isFlushing = false;
290
584
  this.hasPendingFlush = false;
291
585
  this.flushRAF = null;
292
586
  }
587
+ // FIX(F6, docs/FIX_PLAN_2026-08-14.md): state flush chạy trên RAF #1;
588
+ // listener gọi Reactive.update() → ctx.scheduleUpdate() lại đặt RAF #2
589
+ // (ViewController.scheduleUpdate) — trong một frame, Output patch
590
+ // textContent ĐỒNG BỘ ngay trong listener nên đã đổi, còn
591
+ // @foreach/@if (chờ RAF #2) thì CHƯA — DOM tạm thời không nhất quán
592
+ // (đo được: `{{ count }}` đã là "1" nhưng @foreach vẫn list cũ, phải
593
+ // đợi thêm 1 frame nữa). Flush luôn ngay sau khi cascade lắng, CÙNG
594
+ // frame — không đợi RAF kế tiếp. RAF trong scheduleUpdate() vẫn giữ
595
+ // nguyên làm đường dự phòng cho update phát sinh NGOÀI chu kỳ flush
596
+ // state; gặp hàng đợi rỗng (đã flush ở đây) thì chỉ là no-op.
597
+ this.controller?.flushReactiveUpdatesNow?.();
293
598
  }
294
- flushChanges() {
295
- if (this.pendingChanges.size === 0)
599
+ /** Chỉ quan sát object THUẦN và mảng — tránh phá Date/Map/Set/instance class. */
600
+ static isObservable(v) {
601
+ if (v === null || typeof v !== 'object' || Object.isFrozen(v))
602
+ return false;
603
+ if (Array.isArray(v))
604
+ return true;
605
+ const proto = Object.getPrototypeOf(v);
606
+ return proto === Object.prototype || proto === null;
607
+ }
608
+ /**
609
+ * Gắn (hoặc gỡ) bộ bắt mutate cho giá trị mới của `key`.
610
+ * Gọi ở MỌI chỗ gán `states[key].value`.
611
+ */
612
+ trackArray(key, value) {
613
+ if (this.trackedChannels.get(key)?.root === value)
296
614
  return;
297
- const changed = Array.from(this.pendingChanges);
298
- this.pendingChanges.clear();
299
- // Reset multi-key listener flags
300
- for (const listener of this.multiKeyListeners) {
301
- listener.called = false;
302
- }
303
- // Notify single-key listeners
304
- for (const changedKey of changed) {
305
- const listeners = this.listeners.get(changedKey);
306
- if (listeners) {
307
- const currentValue = this.states[changedKey]?.value;
308
- for (const listener of listeners) {
615
+ this.untrackKey(key);
616
+ if (!StateManager.isObservable(value))
617
+ return;
618
+ const channel = { notify: null, root: value, nodes: new Set() };
619
+ channel.notify = () => {
620
+ if (this._isDestroyed)
621
+ return;
622
+ // ĐƯỜNG NÓNG — chạy mỗi lần mutate. Chỉ ĐÁNH DẤU (O(1)); tuyệt đối
623
+ // không chụp lại ở đây. Bản trước gọi `shallowCopy(value)` tại chỗ
624
+ // này ⇒ mỗi lần gán một thuộc tính lại copy CẢ mảng: đo được 1000
625
+ // lần gán trên list 10k mất ~28ms. `detectExternalMutation` ở đầu
626
+ // flush vốn đã chụp lại mọi key rồi — chỉ cần cho nó biết key này
627
+ // đã được xử lý để đừng cảnh báo "mutate mà KHÔNG set lại".
628
+ if (this.mutationDiagnostics)
629
+ this.hookHandledKeys.add(key);
630
+ this.enqueueChange(key);
631
+ };
632
+ this.trackedChannels.set(key, channel);
633
+ StateManager.observe(value, channel, new Set());
634
+ }
635
+ /**
636
+ * Ngừng theo dõi một key: GỠ HẲN channel khỏi tập hook của từng node.
637
+ *
638
+ * Chỉ `notify = null` là KHÔNG đủ. Dữ liệu ở đây được truyền bằng THAM CHIẾU
639
+ * TRỰC TIẾP (item của `@foreach` đi thẳng vào view con qua props, mảng có thể
640
+ * nằm trong store dùng chung), nên cùng một object sống qua nhiều lần
641
+ * mount/destroy. Channel chết mà nằm lại thì tập hook phình vô hạn — đo được:
642
+ * 50 lần mount/destroy trên cùng mảng để lại 50 channel. Đúng lớp lỗi mà
643
+ * `tests/view/registry-cleanup.test.ts` canh ("mọi registry phải có trần").
644
+ *
645
+ * Channel giữ tập node đang sống. Mỗi mutator/setter gỡ node tách khỏi cây
646
+ * ngay lập tức; nhờ vậy destroy có thể dọn cả cây chính xác mà không bỏ sót
647
+ * object đã từng được observe.
648
+ */
649
+ untrackKey(key) {
650
+ const prev = this.trackedChannels.get(key);
651
+ if (!prev)
652
+ return;
653
+ prev.notify = null; // chặn notify ngay
654
+ for (const node of prev.nodes) {
655
+ const hooks = node[StateManager.HOOKS];
656
+ hooks?.delete(prev);
657
+ }
658
+ prev.nodes.clear();
659
+ this.trackedChannels.delete(key);
660
+ }
661
+ /** Gỡ mọi channel của manager này (destroy view). */
662
+ untrackAllArrays() {
663
+ for (const key of Array.from(this.trackedChannels.keys()))
664
+ this.untrackKey(key);
665
+ this.trackedChannels.clear();
666
+ }
667
+ /** Gỡ `channel` khỏi tập hook của `node` và toàn bộ cây con. */
668
+ static unobserve(node, channel, seen, reachable) {
669
+ if (!StateManager.isObservable(node) || seen.has(node))
670
+ return;
671
+ seen.add(node);
672
+ if (!reachable?.has(node)) {
673
+ const hooks = node[StateManager.HOOKS];
674
+ hooks?.delete(channel);
675
+ channel.nodes.delete(node);
676
+ }
677
+ if (Array.isArray(node)) {
678
+ for (const item of node)
679
+ StateManager.unobserve(item, channel, seen, reachable);
680
+ return;
681
+ }
682
+ for (const prop of Object.keys(node))
683
+ StateManager.unobserve(node[prop], channel, seen, reachable);
684
+ }
685
+ /** Thu thập cây hiện còn reachable để không gỡ nhầm shared reference. */
686
+ static collectReachable(node, seen = new Set()) {
687
+ if (!StateManager.isObservable(node) || seen.has(node))
688
+ return seen;
689
+ seen.add(node);
690
+ if (Array.isArray(node)) {
691
+ for (const item of node)
692
+ StateManager.collectReachable(item, seen);
693
+ }
694
+ else {
695
+ for (const prop of Object.keys(node))
696
+ StateManager.collectReachable(node[prop], seen);
697
+ }
698
+ return seen;
699
+ }
700
+ /**
701
+ * Cài bộ bắt mutate lên `node` và toàn bộ cây con, rồi ghi `channel` vào
702
+ * tập hook của mỗi node. `seen` chặn vòng lặp tham chiếu.
703
+ */
704
+ static observe(node, channel, seen) {
705
+ if (!StateManager.isObservable(node) || seen.has(node))
706
+ return;
707
+ seen.add(node);
708
+ let hooks = node[StateManager.HOOKS];
709
+ const fresh = !hooks;
710
+ if (!hooks) {
711
+ hooks = new Set();
712
+ try {
713
+ Object.defineProperty(node, StateManager.HOOKS, {
714
+ value: hooks, enumerable: false, configurable: true, writable: false,
715
+ });
716
+ }
717
+ catch {
718
+ return; // node bị seal → bỏ qua, vẫn còn cảnh báo lúc flush
719
+ }
720
+ }
721
+ hooks.add(channel);
722
+ channel.nodes.add(node);
723
+ // Đường NÓNG — chạy mỗi lần gán/mutate. Duyệt thẳng, không `Array.from`:
724
+ // `notify` chỉ enqueue key, không bao giờ thêm/bớt hook nên không có
725
+ // rủi ro sửa Set đang duyệt.
726
+ const fire = () => {
727
+ const set = node[StateManager.HOOKS];
728
+ if (!set)
729
+ return;
730
+ for (const ch of set)
731
+ ch.notify?.();
732
+ };
733
+ /** Giá trị MỚI gán vào cũng phải được quan sát — chỉ khi nó đáng quan sát. */
734
+ const observeIncoming = (next) => {
735
+ if (!StateManager.isObservable(next))
736
+ return; // primitive: thoát sớm, không cấp phát
737
+ const set = node[StateManager.HOOKS];
738
+ if (!set)
739
+ return;
740
+ for (const ch of Array.from(set)) {
741
+ if (ch.notify)
742
+ StateManager.observe(next, ch, new Set());
743
+ }
744
+ };
745
+ /** Gỡ hook khỏi nhánh vừa rời cây, nhưng giữ node còn reachable qua nhánh khác. */
746
+ const unobserveOutgoing = (outgoing) => {
747
+ const observableOutgoing = outgoing.filter(StateManager.isObservable);
748
+ if (observableOutgoing.length === 0)
749
+ return;
750
+ const set = node[StateManager.HOOKS];
751
+ if (!set)
752
+ return;
753
+ for (const ch of Array.from(set)) {
754
+ if (!ch.notify)
755
+ continue;
756
+ const reachable = StateManager.collectReachable(ch.root);
757
+ const seen = new Set();
758
+ for (const oldValue of observableOutgoing) {
759
+ StateManager.unobserve(oldValue, ch, seen, reachable);
760
+ }
761
+ }
762
+ };
763
+ if (Array.isArray(node)) {
764
+ if (fresh) {
765
+ for (const name of StateManager.ARRAY_MUTATORS) {
766
+ const original = Array.prototype[name];
309
767
  try {
310
- listener(currentValue);
311
- }
312
- catch (e) {
313
- console.error('[ViewState] Listener error:', e);
768
+ Object.defineProperty(node, name, {
769
+ value: function (...args) {
770
+ const outgoing = [];
771
+ const length = this.length;
772
+ const normalizeIndex = (raw, fallback) => {
773
+ const number = raw === undefined ? fallback : Number(raw);
774
+ if (!Number.isFinite(number))
775
+ return number < 0 ? 0 : length;
776
+ const integer = Math.trunc(number);
777
+ return integer < 0
778
+ ? Math.max(length + integer, 0)
779
+ : Math.min(integer, length);
780
+ };
781
+ if (name === 'fill') {
782
+ outgoing.push(...this.slice(normalizeIndex(args[1], 0), normalizeIndex(args[2], length)));
783
+ }
784
+ else if (name === 'copyWithin') {
785
+ const target = normalizeIndex(args[0], 0);
786
+ const start = normalizeIndex(args[1], 0);
787
+ const end = normalizeIndex(args[2], length);
788
+ const count = Math.max(0, Math.min(end - start, length - target));
789
+ outgoing.push(...this.slice(target, target + count));
790
+ }
791
+ const result = original.apply(this, args);
792
+ if ((name === 'pop' || name === 'shift') && result !== undefined) {
793
+ outgoing.push(result);
794
+ }
795
+ else if (name === 'splice') {
796
+ outgoing.push(...result);
797
+ }
798
+ // Chỉ quan sát phần tử MỚI ĐƯA VÀO (tham số), KHÔNG
799
+ // quét lại cả mảng — quét lại là O(n) mỗi lần push,
800
+ // list 10k phần tử sẽ đứng hình. `push`/`unshift`/
801
+ // `splice`/`fill` truyền giá trị mới qua args; các
802
+ // method còn lại chỉ nhận số/hàm nên `isObservable`
803
+ // tự bỏ qua.
804
+ for (const arg of args)
805
+ observeIncoming(arg);
806
+ unobserveOutgoing(outgoing);
807
+ fire();
808
+ return result;
809
+ },
810
+ enumerable: false, configurable: true, writable: true,
811
+ });
314
812
  }
813
+ catch { /* method không ghi đè được → bỏ qua */ }
315
814
  }
316
815
  }
317
- // Notify multi-key listeners
318
- for (const mkl of this.multiKeyListeners) {
319
- if (!mkl.called && mkl.keys.has(changedKey)) {
320
- mkl.called = true;
321
- const values = {};
322
- for (const k of mkl.keys) {
323
- if (changed.includes(k)) {
324
- values[String(k)] = this.states[k]?.value;
816
+ for (const item of node)
817
+ StateManager.observe(item, channel, seen);
818
+ return;
819
+ }
820
+ for (const prop of Object.keys(node)) {
821
+ StateManager.observe(node[prop], channel, seen);
822
+ if (!fresh)
823
+ continue; // accessor đã cài từ lần trước
824
+ const desc = Object.getOwnPropertyDescriptor(node, prop);
825
+ if (!desc || !desc.configurable || desc.get || desc.set)
826
+ continue;
827
+ let current = desc.value;
828
+ try {
829
+ Object.defineProperty(node, prop, {
830
+ enumerable: desc.enumerable, // giữ nguyên → spread/keys không đổi
831
+ configurable: true,
832
+ get: () => current,
833
+ set: (next) => {
834
+ if (next === current)
835
+ return;
836
+ const previous = current;
837
+ current = next;
838
+ observeIncoming(next);
839
+ unobserveOutgoing([previous]);
840
+ fire();
841
+ },
842
+ });
843
+ }
844
+ catch { /* prop không cấu hình được → bỏ qua */ }
845
+ }
846
+ }
847
+ static shallowCopy(v) {
848
+ return Array.isArray(v) ? v.slice() : { ...v };
849
+ }
850
+ /**
851
+ * ponytail: chỉ so ĐỘ SÂU 1 — bắt push/splice/shift/sort/gán lại phần tử/
852
+ * thêm-bớt field. KHÔNG bắt `user.profile.name = 'x'`. So sâu cần deep clone
853
+ * mỗi flush; nếu mutate lồng thành vấn đề thật thì đó là lúc cân nhắc Proxy,
854
+ * không phải làm snapshot nặng thêm.
855
+ */
856
+ static shallowDiffers(prev, cur) {
857
+ if (Array.isArray(cur)) {
858
+ if (!Array.isArray(prev) || prev.length !== cur.length)
859
+ return true;
860
+ for (let i = 0; i < cur.length; i++)
861
+ if (prev[i] !== cur[i])
862
+ return true;
863
+ return false;
864
+ }
865
+ if (Array.isArray(prev))
866
+ return true;
867
+ const prevKeys = Object.keys(prev);
868
+ const curKeys = Object.keys(cur);
869
+ if (prevKeys.length !== curKeys.length)
870
+ return true;
871
+ for (const k of curKeys)
872
+ if (prev[k] !== cur[k])
873
+ return true;
874
+ return false;
875
+ }
876
+ /**
877
+ * Chạy đầu mỗi flush: mọi state kiểu object được đối chiếu rồi chụp lại.
878
+ * Nghĩa là mutate lặng lẽ sẽ lộ ở lần flush KẾ TIẾP do bất kỳ key nào —
879
+ * gần như luôn xảy ra ngay lần tương tác sau.
880
+ */
881
+ detectExternalMutation() {
882
+ if (!this.mutationDiagnostics)
883
+ return;
884
+ for (const key in this.states) {
885
+ const slot = this.states[key];
886
+ if (slot?.__computed__)
887
+ continue; // lazy — đọc `.value` sẽ ép tính lại
888
+ const value = slot?.value;
889
+ if (value === null || typeof value !== 'object' || Object.isFrozen(value)) {
890
+ this.mutationSnapshots.delete(key);
891
+ continue;
892
+ }
893
+ const snap = this.mutationSnapshots.get(key);
894
+ if (snap && snap.ref === value
895
+ && !this.hookHandledKeys.has(key) // hook đã lo → không phải "quên set"
896
+ && StateManager.shallowDiffers(snap.copy, value)) {
897
+ this.warnMutatedWithoutSet(key);
898
+ }
899
+ this.mutationSnapshots.set(key, { ref: value, copy: StateManager.shallowCopy(value) });
900
+ }
901
+ this.hookHandledKeys.clear();
902
+ }
903
+ /** Dùng CHUNG `warnedKeys` với warnSameReference — 1 key chỉ kêu 1 lần. */
904
+ warnMutatedWithoutSet(key) {
905
+ const path = this.controller?.path ?? '';
906
+ const warnKey = `${path}::${String(key)}`;
907
+ if (StateManager.warnedKeys.has(warnKey))
908
+ return;
909
+ StateManager.warnedKeys.add(warnKey);
910
+ console.warn(`[ViewState] "${String(key)}"${path ? ` (view "${path}")` : ''} bị thay đổi tại chỗ ` +
911
+ `mà KHÔNG set lại → UI không cập nhật. Thay vì mutate, hãy gán giá trị mới: ` +
912
+ `state.${String(key)} = [...] / { ... }.`);
913
+ }
914
+ flushChanges() {
915
+ if (this.pendingChanges.size === 0)
916
+ return;
917
+ this.detectExternalMutation();
918
+ const changed = Array.from(this.pendingChanges);
919
+ this.pendingChanges.clear();
920
+ if (devtools.isEnabled()) {
921
+ devtools.emit('state:changed', {
922
+ viewId: this.controller?.viewId,
923
+ path: this.controller?.path,
924
+ detail: { keys: changed.map(String) },
925
+ });
926
+ }
927
+ // Snapshot the whole batch before invoking callbacks. Subscriptions added
928
+ // during notification start next batch; removed records are never invoked.
929
+ const changedSet = new Set(changed.map(String));
930
+ const batches = changed.map(key => [String(key), Array.from(this.subscriptions.get(String(key)) ?? [])]);
931
+ const called = new Set();
932
+ for (const [key, records] of batches) {
933
+ // Retain single-key-before-multi-key order used by existing bindings.
934
+ for (const multi of [false, true]) {
935
+ for (const record of records) {
936
+ if (!record.active || record.multi !== multi || called.has(record))
937
+ continue;
938
+ called.add(record);
939
+ let value;
940
+ if (multi) {
941
+ value = {};
942
+ for (const k of record.keys) {
943
+ if (changedSet.has(k))
944
+ value[k] = this.states[k]?.value;
325
945
  }
326
946
  }
947
+ else
948
+ value = this.states[key]?.value;
327
949
  try {
328
- mkl.callback(values);
950
+ record.callback(value);
329
951
  }
330
952
  catch (e) {
331
- console.error('[ViewState] Multi-key listener error:', e);
953
+ this.retryAfterReactiveFlush(e, v => {
954
+ if (record.active)
955
+ record.callback(v);
956
+ }, value);
332
957
  }
333
958
  }
334
959
  }
335
960
  }
336
961
  }
962
+ /**
963
+ * Vùng reactive re-render ở RAF kế tiếp, còn listener của Output/Text/binding
964
+ * chạy NGAY trong đợt flush này. Nên khi guard đổi chiều
965
+ * (`@if(record !== null)` thành false), factory bên trong vùng —
966
+ * `{{ record['name'] }}` — vẫn chạy một lần với state mới và ném, dù DOM đó
967
+ * chỉ còn sống thêm một frame.
968
+ *
969
+ * Có vùng đang chờ dựng lại thì chạy lại listener SAU đợt đó: element hoặc
970
+ * đã bị destroy (listener no-op), hoặc chạy được với state nhất quán. Còn
971
+ * ném nữa mới là lỗi thật và đi tiếp tới boundary.
972
+ *
973
+ * rAF của flushReactiveUpdates đã đăng ký TRƯỚC (trong chính đợt flush này,
974
+ * lúc vùng gọi scheduleUpdate), nên rAF đăng ký ở đây chạy sau nó.
975
+ *
976
+ * Giới hạn: chỉ bắt được khi vùng bọc đã kịp vào hàng đợi trước listener
977
+ * này — đúng với @if/@foreach bọc ngoài vì chúng subscribe trước con.
978
+ */
979
+ retryAfterReactiveFlush(err, listener, value) {
980
+ const ctrl = this.controller;
981
+ if (!ctrl?.hasPendingReactiveUpdate?.()) {
982
+ this.reportListenerError(err);
983
+ return;
984
+ }
985
+ requestAnimationFrame(() => {
986
+ if (this._isDestroyed)
987
+ return;
988
+ try {
989
+ listener(value);
990
+ }
991
+ catch (e) {
992
+ this.reportListenerError(e);
993
+ }
994
+ });
995
+ }
996
+ /**
997
+ * Lỗi ném ra từ callback subscribe — đưa về error boundary thay vì nuốt.
998
+ *
999
+ * MỌI factory người dùng chạy khi state đổi đều đi qua đây: Output `{{ }}`,
1000
+ * TextElement, Html attr/class/style/prop binding, mirror-sync của computed.
1001
+ * Trước đây chỉ `console.error` → DOM giữ giá trị cũ và boundary KHÔNG hề
1002
+ * biết (im lặng sai, tệ hơn nổ). Đây là 1 chỗ bao trọn tất cả các đường đó.
1003
+ *
1004
+ * Không có "fallback content" ở tầng này (không biết vùng DOM nào hỏng) —
1005
+ * boundary chỉ được BÁO để log/đặt state lỗi; giá trị trả về bị bỏ qua.
1006
+ * Muốn thay nội dung vùng lỗi thì dùng boundary ở Component/Reactive
1007
+ * (phase 'render'/'update'), nơi có ranh giới marker rõ ràng.
1008
+ */
1009
+ reportListenerError(err) {
1010
+ const ctrl = this.controller;
1011
+ try {
1012
+ if (ctrl?.handleError?.(err, { phase: 'update', path: ctrl.path ?? '' })?.handled)
1013
+ return;
1014
+ }
1015
+ catch (e) {
1016
+ console.error('[ViewState] onError handler threw:', e);
1017
+ }
1018
+ console.error('[ViewState] Listener error:', err);
1019
+ }
337
1020
  // ─── Cleanup ────────────────────────────────────────────────
338
1021
  destroy() {
339
1022
  this._isDestroyed = true;
@@ -341,9 +1024,18 @@ export class StateManager {
341
1024
  cancelAnimationFrame(this.flushRAF);
342
1025
  this.flushRAF = null;
343
1026
  }
344
- this.listeners.clear();
345
- this.multiKeyListeners = [];
1027
+ this.computedNodes.clear();
1028
+ this.computedDependents.clear();
1029
+ this.versions.clear();
1030
+ for (const records of this.subscriptions.values()) {
1031
+ for (const record of records)
1032
+ record.active = false;
1033
+ }
1034
+ this.subscriptions.clear();
346
1035
  this.pendingChanges.clear();
1036
+ this.untrackAllArrays();
1037
+ this.hookHandledKeys.clear();
1038
+ this.mutationSnapshots.clear();
347
1039
  this.states = {};
348
1040
  this.setters = {};
349
1041
  this.controller = null;
@@ -365,6 +1057,42 @@ export class StateManager {
365
1057
  return data;
366
1058
  }
367
1059
  }
1060
+ /** Key đã cảnh báo rồi — mỗi key tối đa 1 dòng cho cả vòng đời app. */
1061
+ StateManager.warnedKeys = new Set();
1062
+ /** Trần số vòng flush nối tiếp trong 1 frame — chặn computed phụ thuộc vòng. */
1063
+ StateManager.MAX_CASCADE = 20;
1064
+ // ─── Tự bắt mutate TẠI CHỖ (mảng và object) ─────────────────
1065
+ //
1066
+ // `list.push(x)` / `user.name = 'x'` KHÔNG đổi reference và KHÔNG đi qua
1067
+ // setter nào ⇒ trước đây chỉ được PHÁT HIỆN muộn ở `detectExternalMutation`
1068
+ // (cảnh báo, không cập nhật). Cách vá — kỹ thuật **Vue 2**, KHÔNG phải Proxy:
1069
+ // - MẢNG: thay các method mutate ngay trên chính mảng đó bằng bản bọc;
1070
+ // - OBJECT: thay từng own-property bằng cặp getter/setter.
1071
+ // Gọi/gán xong thì `enqueueChange(key)`.
1072
+ //
1073
+ // Vì sao hợp kiến trúc này:
1074
+ // - reference KHÔNG đổi ⇒ `ForeachSlotCache` (so `slot.item === item`),
1075
+ // `Array.isArray`, `===` của người dùng đều nguyên vẹn;
1076
+ // - method vá của mảng là own-property KHÔNG enumerable; accessor của
1077
+ // object giữ `enumerable: true` ⇒ spread / `JSON.stringify` /
1078
+ // `Object.keys` / `for…in` đều không thấy gì khác;
1079
+ // - vẫn đúng MỘT `enqueueChange` ở TẦNG KEY GỐC ⇒ granularity không đổi,
1080
+ // không cần dep tracking runtime (giữ nguyên lập luận GAPS §2.16b),
1081
+ // `stateKeys`/contract SSR không đụng tới.
1082
+ //
1083
+ // Quan sát ĐỆ QUY: `user.profile.name = 'x'` (lồng sâu) trước đây vừa không
1084
+ // cập nhật vừa KHÔNG cảnh báo (`shallowDiffers` chỉ so độ sâu 1) — im lặng
1085
+ // hoàn toàn, ca tệ nhất. Nay bắt được.
1086
+ //
1087
+ // GIỚI HẠN (cần Proxy mới vượt, giống hệt Vue 2):
1088
+ // - THÊM KEY MỚI chưa từng có lúc quan sát: `user.extra = 1`;
1089
+ // - gán qua index/length của mảng: `list[0] = x`, `list.length = 0`.
1090
+ // Hai ca này vẫn do `detectExternalMutation` cảnh báo ở lần flush kế.
1091
+ StateManager.ARRAY_MUTATORS = [
1092
+ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin',
1093
+ ];
1094
+ /** Tập "kênh" gắn trên một node — nhiều view/key có thể dùng chung dữ liệu. */
1095
+ StateManager.HOOKS = Symbol.for('sao.mutationHooks');
368
1096
  /**
369
1097
  * ViewState — thin wrapper around StateManager.
370
1098
  *