@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.
- package/README.md +52 -195
- package/dist/index.d.ts +20 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/plugins.d.ts +2 -2
- package/dist/plugins.js +2 -2
- package/dist/src/core/app/Application.d.ts +25 -15
- package/dist/src/core/app/Application.d.ts.map +1 -1
- package/dist/src/core/app/Application.js +55 -18
- package/dist/src/core/app/Application.js.map +1 -1
- package/dist/src/core/bootstrap/app.d.ts +1 -1
- package/dist/src/core/bootstrap/app.d.ts.map +1 -1
- package/dist/src/core/bootstrap/app.js +77 -4
- package/dist/src/core/bootstrap/app.js.map +1 -1
- package/dist/src/core/bootstrap/bundle.d.ts +41 -0
- package/dist/src/core/bootstrap/bundle.d.ts.map +1 -0
- package/dist/src/core/bootstrap/bundle.js +98 -0
- package/dist/src/core/bootstrap/bundle.js.map +1 -0
- package/dist/src/core/bootstrap/providers/ApiServiceProvider.d.ts +1 -1
- package/dist/src/core/bootstrap/providers/ApiServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/ApiServiceProvider.js +3 -3
- package/dist/src/core/bootstrap/providers/ApiServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/CoreServiceProvider.d.ts +1 -1
- package/dist/src/core/bootstrap/providers/CoreServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/CoreServiceProvider.js +11 -8
- package/dist/src/core/bootstrap/providers/CoreServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/HelperServiceProvider.d.ts +3 -1
- package/dist/src/core/bootstrap/providers/HelperServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/HelperServiceProvider.js +8 -3
- package/dist/src/core/bootstrap/providers/HelperServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/RouteServiceProvider.d.ts +2 -1
- package/dist/src/core/bootstrap/providers/RouteServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/RouteServiceProvider.js +14 -4
- package/dist/src/core/bootstrap/providers/RouteServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/ServiceProvider.d.ts +4 -4
- package/dist/src/core/bootstrap/providers/ServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/ServiceProvider.js +5 -5
- package/dist/src/core/bootstrap/providers/ServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/ViewServiceProvider.d.ts +1 -1
- package/dist/src/core/bootstrap/providers/ViewServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/ViewServiceProvider.js +3 -3
- package/dist/src/core/bootstrap/providers/ViewServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/default-providers.d.ts +1 -1
- package/dist/src/core/bootstrap/providers/default-providers.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/default-providers.js +44 -11
- package/dist/src/core/bootstrap/providers/default-providers.js.map +1 -1
- package/dist/src/core/bootstrap/providers/index.d.ts +9 -9
- package/dist/src/core/bootstrap/providers/index.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/index.js +8 -8
- package/dist/src/core/bootstrap/providers/index.js.map +1 -1
- package/dist/src/core/bootstrap/providers/provider-order.d.ts +1 -1
- package/dist/src/core/bootstrap/providers/provider-order.d.ts.map +1 -1
- package/dist/src/core/bootstrap/ssr.d.ts +47 -0
- package/dist/src/core/bootstrap/ssr.d.ts.map +1 -0
- package/dist/src/core/bootstrap/ssr.js +129 -0
- package/dist/src/core/bootstrap/ssr.js.map +1 -0
- package/dist/src/core/cache/ViewCache.d.ts +1 -1
- package/dist/src/core/cache/ViewCache.d.ts.map +1 -1
- package/dist/src/core/cache/index.d.ts +1 -1
- package/dist/src/core/cache/index.d.ts.map +1 -1
- package/dist/src/core/cache/index.js +1 -1
- package/dist/src/core/cache/index.js.map +1 -1
- package/dist/src/core/contracts/ApplicationInterface.d.ts +15 -11
- package/dist/src/core/contracts/ApplicationInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/BlockInterface.d.ts +10 -10
- package/dist/src/core/contracts/BlockInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/BundleInterface.d.ts +38 -0
- package/dist/src/core/contracts/BundleInterface.d.ts.map +1 -0
- package/dist/src/core/contracts/BundleInterface.js +2 -0
- package/dist/src/core/contracts/BundleInterface.js.map +1 -0
- package/dist/src/core/contracts/ComponentInterface.d.ts +7 -6
- package/dist/src/core/contracts/ComponentInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ElementInterface.d.ts +91 -27
- package/dist/src/core/contracts/ElementInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/HeadServiceInterface.d.ts +37 -0
- package/dist/src/core/contracts/HeadServiceInterface.d.ts.map +1 -0
- package/dist/src/core/contracts/HeadServiceInterface.js +3 -0
- package/dist/src/core/contracts/HeadServiceInterface.js.map +1 -0
- package/dist/src/core/contracts/HelperInterface.d.ts +9 -1
- package/dist/src/core/contracts/HelperInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/HttpServiceInterface.d.ts +3 -0
- package/dist/src/core/contracts/HttpServiceInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/MarkerInterface.d.ts +29 -44
- package/dist/src/core/contracts/MarkerInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ReactiveInterface.d.ts +9 -9
- package/dist/src/core/contracts/ReactiveInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/RouterInterface.d.ts +5 -2
- package/dist/src/core/contracts/RouterInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/SectionInterface.d.ts +48 -0
- package/dist/src/core/contracts/SectionInterface.d.ts.map +1 -0
- package/dist/src/core/contracts/SectionInterface.js +2 -0
- package/dist/src/core/contracts/SectionInterface.js.map +1 -0
- package/dist/src/core/contracts/ServiceProviderInterface.d.ts +3 -3
- package/dist/src/core/contracts/ServiceProviderInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ViewControllerInterface.d.ts +116 -15
- package/dist/src/core/contracts/ViewControllerInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ViewInterface.d.ts +31 -5
- package/dist/src/core/contracts/ViewInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ViewManagerInterface.d.ts +47 -5
- package/dist/src/core/contracts/ViewManagerInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ViewStateInterface.d.ts +39 -2
- package/dist/src/core/contracts/ViewStateInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/common.d.ts +3 -3
- package/dist/src/core/contracts/common.js +3 -3
- package/dist/src/core/contracts/utils.d.ts +19 -19
- package/dist/src/core/contracts/utils.d.ts.map +1 -1
- package/dist/src/core/contracts/views.d.ts +1 -22
- package/dist/src/core/contracts/views.d.ts.map +1 -1
- package/dist/src/core/devtools/hook.d.ts +66 -0
- package/dist/src/core/devtools/hook.d.ts.map +1 -0
- package/dist/src/core/devtools/hook.js +114 -0
- package/dist/src/core/devtools/hook.js.map +1 -0
- package/dist/src/core/devtools/index.d.ts +34 -0
- package/dist/src/core/devtools/index.d.ts.map +1 -0
- package/dist/src/core/devtools/index.js +33 -0
- package/dist/src/core/devtools/index.js.map +1 -0
- package/dist/src/core/devtools/inspector.d.ts +31 -0
- package/dist/src/core/devtools/inspector.d.ts.map +1 -0
- package/dist/src/core/devtools/inspector.js +249 -0
- package/dist/src/core/devtools/inspector.js.map +1 -0
- package/dist/src/core/elements/Block.d.ts +9 -7
- package/dist/src/core/elements/Block.d.ts.map +1 -1
- package/dist/src/core/elements/Block.js +33 -9
- package/dist/src/core/elements/Block.js.map +1 -1
- package/dist/src/core/elements/BlockOutlet.d.ts +19 -8
- package/dist/src/core/elements/BlockOutlet.d.ts.map +1 -1
- package/dist/src/core/elements/BlockOutlet.js +57 -13
- package/dist/src/core/elements/BlockOutlet.js.map +1 -1
- package/dist/src/core/elements/Component.d.ts +83 -11
- package/dist/src/core/elements/Component.d.ts.map +1 -1
- package/dist/src/core/elements/Component.js +334 -13
- package/dist/src/core/elements/Component.js.map +1 -1
- package/dist/src/core/elements/ForeachSlotCache.d.ts +95 -0
- package/dist/src/core/elements/ForeachSlotCache.d.ts.map +1 -0
- package/dist/src/core/elements/ForeachSlotCache.js +149 -0
- package/dist/src/core/elements/ForeachSlotCache.js.map +1 -0
- package/dist/src/core/elements/Fragment.d.ts +19 -13
- package/dist/src/core/elements/Fragment.d.ts.map +1 -1
- package/dist/src/core/elements/Fragment.js +43 -20
- package/dist/src/core/elements/Fragment.js.map +1 -1
- package/dist/src/core/elements/Html.d.ts +89 -12
- package/dist/src/core/elements/Html.d.ts.map +1 -1
- package/dist/src/core/elements/Html.js +707 -106
- package/dist/src/core/elements/Html.js.map +1 -1
- package/dist/src/core/elements/Output.d.ts +34 -10
- package/dist/src/core/elements/Output.d.ts.map +1 -1
- package/dist/src/core/elements/Output.js +141 -26
- package/dist/src/core/elements/Output.js.map +1 -1
- package/dist/src/core/elements/Reactive.d.ts +85 -14
- package/dist/src/core/elements/Reactive.d.ts.map +1 -1
- package/dist/src/core/elements/Reactive.js +315 -41
- package/dist/src/core/elements/Reactive.js.map +1 -1
- package/dist/src/core/elements/TextElement.d.ts +11 -7
- package/dist/src/core/elements/TextElement.d.ts.map +1 -1
- package/dist/src/core/elements/TextElement.js +18 -11
- package/dist/src/core/elements/TextElement.js.map +1 -1
- package/dist/src/core/elements/Wrapper.d.ts +24 -13
- package/dist/src/core/elements/Wrapper.d.ts.map +1 -1
- package/dist/src/core/elements/Wrapper.js +65 -8
- package/dist/src/core/elements/Wrapper.js.map +1 -1
- package/dist/src/core/elements/Yield.d.ts +10 -9
- package/dist/src/core/elements/Yield.d.ts.map +1 -1
- package/dist/src/core/elements/Yield.js +31 -19
- package/dist/src/core/elements/Yield.js.map +1 -1
- package/dist/src/core/elements/helpers.d.ts +7 -7
- package/dist/src/core/elements/helpers.d.ts.map +1 -1
- package/dist/src/core/elements/helpers.js +4 -4
- package/dist/src/core/elements/index.d.ts +12 -11
- package/dist/src/core/elements/index.d.ts.map +1 -1
- package/dist/src/core/elements/index.js +12 -11
- package/dist/src/core/elements/index.js.map +1 -1
- package/dist/src/core/elements/reconcileChildren.d.ts +7 -0
- package/dist/src/core/elements/reconcileChildren.d.ts.map +1 -0
- package/dist/src/core/elements/reconcileChildren.js +74 -0
- package/dist/src/core/elements/reconcileChildren.js.map +1 -0
- package/dist/src/core/helpers/ApiClient.d.ts.map +1 -0
- package/dist/src/core/{hellpers → helpers}/ApiClient.js +10 -1
- package/dist/src/core/helpers/ApiClient.js.map +1 -0
- package/dist/src/core/helpers/app.d.ts +3 -0
- package/dist/src/core/helpers/app.d.ts.map +1 -0
- package/dist/src/core/{hellpers → helpers}/app.js +11 -1
- package/dist/src/core/helpers/app.js.map +1 -0
- package/dist/src/core/helpers/transition.d.ts +34 -0
- package/dist/src/core/helpers/transition.d.ts.map +1 -0
- package/dist/src/core/helpers/transition.js +118 -0
- package/dist/src/core/helpers/transition.js.map +1 -0
- package/dist/src/core/{hellpers → helpers}/utils.d.ts +2 -2
- package/dist/src/core/helpers/utils.d.ts.map +1 -0
- package/dist/src/core/{hellpers → helpers}/utils.js +4 -4
- package/dist/src/core/helpers/utils.js.map +1 -0
- package/dist/src/core/helpers/view.d.ts +39 -0
- package/dist/src/core/helpers/view.d.ts.map +1 -0
- package/dist/src/core/helpers/view.js +286 -0
- package/dist/src/core/helpers/view.js.map +1 -0
- package/dist/src/core/routers/Router.d.ts +108 -7
- package/dist/src/core/routers/Router.d.ts.map +1 -1
- package/dist/src/core/routers/Router.js +444 -68
- package/dist/src/core/routers/Router.js.map +1 -1
- package/dist/src/core/routers/index.d.ts +2 -2
- package/dist/src/core/routers/index.d.ts.map +1 -1
- package/dist/src/core/routers/index.js +1 -1
- package/dist/src/core/routers/index.js.map +1 -1
- package/dist/src/core/services/AssetManager.d.ts +112 -0
- package/dist/src/core/services/AssetManager.d.ts.map +1 -0
- package/dist/src/core/services/AssetManager.js +361 -0
- package/dist/src/core/services/AssetManager.js.map +1 -0
- package/dist/src/core/services/BlockManager.d.ts +73 -2
- package/dist/src/core/services/BlockManager.d.ts.map +1 -1
- package/dist/src/core/services/BlockManager.js +270 -48
- package/dist/src/core/services/BlockManager.js.map +1 -1
- package/dist/src/core/services/DomService.js +3 -3
- package/dist/src/core/services/DomService.js.map +1 -1
- package/dist/src/core/services/HeadService.d.ts +24 -0
- package/dist/src/core/services/HeadService.d.ts.map +1 -0
- package/dist/src/core/services/HeadService.js +120 -0
- package/dist/src/core/services/HeadService.js.map +1 -0
- package/dist/src/core/services/HelperService.d.ts +14 -3
- package/dist/src/core/services/HelperService.d.ts.map +1 -1
- package/dist/src/core/services/HelperService.js +32 -2
- package/dist/src/core/services/HelperService.js.map +1 -1
- package/dist/src/core/services/HttpService.d.ts +8 -0
- package/dist/src/core/services/HttpService.d.ts.map +1 -1
- package/dist/src/core/services/HttpService.js +121 -47
- package/dist/src/core/services/HttpService.js.map +1 -1
- package/dist/src/core/services/MarkerRegistry.d.ts +51 -8
- package/dist/src/core/services/MarkerRegistry.d.ts.map +1 -1
- package/dist/src/core/services/MarkerRegistry.js +135 -14
- package/dist/src/core/services/MarkerRegistry.js.map +1 -1
- package/dist/src/core/services/PageCache.d.ts +101 -0
- package/dist/src/core/services/PageCache.d.ts.map +1 -0
- package/dist/src/core/services/PageCache.js +184 -0
- package/dist/src/core/services/PageCache.js.map +1 -0
- package/dist/src/core/services/SectionManager.d.ts +100 -0
- package/dist/src/core/services/SectionManager.d.ts.map +1 -0
- package/dist/src/core/services/SectionManager.js +408 -0
- package/dist/src/core/services/SectionManager.js.map +1 -0
- package/dist/src/core/services/StorageService.js +1 -1
- package/dist/src/core/services/TranslationService.d.ts +21 -0
- package/dist/src/core/services/TranslationService.d.ts.map +1 -0
- package/dist/src/core/services/TranslationService.js +95 -0
- package/dist/src/core/services/TranslationService.js.map +1 -0
- package/dist/src/core/services/index.d.ts +11 -8
- package/dist/src/core/services/index.d.ts.map +1 -1
- package/dist/src/core/services/index.js +11 -8
- package/dist/src/core/services/index.js.map +1 -1
- package/dist/src/core/types/utils.d.ts +8 -8
- package/dist/src/core/types/utils.d.ts.map +1 -1
- package/dist/src/core/view/LoopContext.d.ts +21 -1
- package/dist/src/core/view/LoopContext.d.ts.map +1 -1
- package/dist/src/core/view/LoopContext.js +36 -0
- package/dist/src/core/view/LoopContext.js.map +1 -1
- package/dist/src/core/view/ResourceScope.d.ts +38 -0
- package/dist/src/core/view/ResourceScope.d.ts.map +1 -0
- package/dist/src/core/view/ResourceScope.js +167 -0
- package/dist/src/core/view/ResourceScope.js.map +1 -0
- package/dist/src/core/view/Section.d.ts +17 -32
- package/dist/src/core/view/Section.d.ts.map +1 -1
- package/dist/src/core/view/Section.js +14 -55
- package/dist/src/core/view/Section.js.map +1 -1
- package/dist/src/core/view/View.d.ts +43 -7
- package/dist/src/core/view/View.d.ts.map +1 -1
- package/dist/src/core/view/View.js +22 -4
- package/dist/src/core/view/View.js.map +1 -1
- package/dist/src/core/view/ViewController.d.ts +313 -40
- package/dist/src/core/view/ViewController.d.ts.map +1 -1
- package/dist/src/core/view/ViewController.js +1002 -147
- package/dist/src/core/view/ViewController.js.map +1 -1
- package/dist/src/core/view/ViewManager.d.ts +318 -27
- package/dist/src/core/view/ViewManager.d.ts.map +1 -1
- package/dist/src/core/view/ViewManager.js +1199 -258
- package/dist/src/core/view/ViewManager.js.map +1 -1
- package/dist/src/core/view/ViewState.d.ts +185 -6
- package/dist/src/core/view/ViewState.d.ts.map +1 -1
- package/dist/src/core/view/ViewState.js +851 -123
- package/dist/src/core/view/ViewState.js.map +1 -1
- package/dist/src/core/view/index.d.ts +5 -5
- package/dist/src/core/view/index.d.ts.map +1 -1
- package/dist/src/core/view/index.js +5 -5
- package/dist/src/core/view/index.js.map +1 -1
- package/dist/src/testing/index.d.ts +69 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +140 -0
- package/dist/src/testing/index.js.map +1 -0
- package/index.ts +29 -5
- package/package.json +30 -21
- package/plugins.ts +2 -2
- package/src/core/app/Application.ts +72 -31
- package/src/core/bootstrap/app.ts +84 -6
- package/src/core/bootstrap/bundle.ts +101 -0
- package/src/core/bootstrap/providers/ApiServiceProvider.ts +4 -4
- package/src/core/bootstrap/providers/CoreServiceProvider.ts +14 -8
- package/src/core/bootstrap/providers/HelperServiceProvider.ts +8 -3
- package/src/core/bootstrap/providers/RouteServiceProvider.ts +15 -4
- package/src/core/bootstrap/providers/ServiceProvider.ts +8 -8
- package/src/core/bootstrap/providers/ViewServiceProvider.ts +3 -3
- package/src/core/bootstrap/providers/default-providers.ts +51 -16
- package/src/core/bootstrap/providers/index.ts +9 -9
- package/src/core/bootstrap/providers/provider-order.test.ts +16 -27
- package/src/core/bootstrap/providers/provider-order.ts +1 -1
- package/src/core/bootstrap/ssr.ts +141 -0
- package/src/core/cache/ViewCache.ts +1 -1
- package/src/core/cache/index.ts +1 -1
- package/src/core/contracts/ApplicationInterface.ts +15 -11
- package/src/core/contracts/BlockInterface.ts +18 -18
- package/src/core/contracts/BundleInterface.ts +40 -0
- package/src/core/contracts/ComponentInterface.ts +7 -6
- package/src/core/contracts/ElementInterface.ts +95 -27
- package/src/core/contracts/HeadServiceInterface.ts +41 -0
- package/src/core/contracts/HelperInterface.ts +9 -1
- package/src/core/contracts/HttpServiceInterface.ts +3 -0
- package/src/core/contracts/MarkerInterface.ts +22 -49
- package/src/core/contracts/ReactiveInterface.ts +9 -9
- package/src/core/contracts/RouterInterface.ts +5 -2
- package/src/core/contracts/SectionInterface.ts +49 -0
- package/src/core/contracts/ServiceProviderInterface.ts +3 -3
- package/src/core/contracts/ViewControllerInterface.ts +124 -16
- package/src/core/contracts/ViewInterface.ts +34 -5
- package/src/core/contracts/ViewManagerInterface.ts +40 -5
- package/src/core/contracts/ViewStateInterface.ts +39 -2
- package/src/core/contracts/common.ts +3 -3
- package/src/core/contracts/utils.ts +19 -19
- package/src/core/contracts/views.ts +2 -22
- package/src/core/devtools/hook.ts +142 -0
- package/src/core/devtools/index.ts +38 -0
- package/src/core/devtools/inspector.ts +270 -0
- package/src/core/elements/Block.ts +43 -21
- package/src/core/elements/BlockOutlet.ts +61 -21
- package/src/core/elements/Component.ts +352 -18
- package/src/core/elements/ElementManager.ts +9 -9
- package/src/core/elements/ForeachSlotCache.ts +170 -0
- package/src/core/elements/Fragment.ts +50 -32
- package/src/core/elements/Html.ts +696 -117
- package/src/core/elements/Output.ts +147 -30
- package/src/core/elements/Reactive.ts +329 -52
- package/src/core/elements/TextElement.ts +25 -16
- package/src/core/elements/Wrapper.ts +84 -18
- package/src/core/elements/Yield.ts +35 -24
- package/src/core/elements/helpers.ts +12 -12
- package/src/core/elements/index.ts +25 -24
- package/src/core/elements/reconcileChildren.ts +70 -0
- package/src/core/{hellpers → helpers}/ApiClient.ts +8 -1
- package/src/core/{hellpers → helpers}/app.ts +13 -2
- package/src/core/helpers/transition.ts +127 -0
- package/src/core/{hellpers → helpers}/utils.ts +4 -4
- package/src/core/helpers/view.ts +283 -0
- package/src/core/routers/Router.ts +458 -64
- package/src/core/routers/index.ts +2 -2
- package/src/core/services/AssetManager.ts +387 -0
- package/src/core/services/BlockManager.ts +272 -50
- package/src/core/services/DomService.ts +3 -3
- package/src/core/services/HeadService.ts +124 -0
- package/src/core/services/HelperService.ts +34 -4
- package/src/core/services/HttpService.ts +116 -55
- package/src/core/services/MarkerRegistry.ts +142 -16
- package/src/core/services/PageCache.ts +239 -0
- package/src/core/services/SectionManager.ts +387 -0
- package/src/core/services/StorageService.ts +1 -1
- package/src/core/services/TranslationService.ts +91 -0
- package/src/core/services/index.ts +11 -8
- package/src/core/types/utils.ts +8 -8
- package/src/core/view/LoopContext.ts +38 -1
- package/src/core/view/ResourceScope.ts +174 -0
- package/src/core/view/Section.ts +21 -68
- package/src/core/view/View.ts +57 -8
- package/src/core/view/ViewController.ts +1070 -186
- package/src/core/view/ViewManager.ts +1346 -272
- package/src/core/view/ViewState.ts +834 -116
- package/src/core/view/index.ts +5 -5
- package/src/testing/index.ts +184 -0
- package/dist/src/core/hellpers/ApiClient.d.ts.map +0 -1
- package/dist/src/core/hellpers/ApiClient.js.map +0 -1
- package/dist/src/core/hellpers/app.d.ts +0 -3
- package/dist/src/core/hellpers/app.d.ts.map +0 -1
- package/dist/src/core/hellpers/app.js.map +0 -1
- package/dist/src/core/hellpers/utils.d.ts.map +0 -1
- package/dist/src/core/hellpers/utils.js.map +0 -1
- package/dist/src/core/services/MarkerService.d.ts +0 -299
- package/dist/src/core/services/MarkerService.d.ts.map +0 -1
- package/dist/src/core/services/MarkerService.js +0 -723
- package/dist/src/core/services/MarkerService.js.map +0 -1
- package/src/core/services/MarkerService.ts +0 -923
- /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.
|
|
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 (
|
|
85
|
-
return [this.states[
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
284
|
+
const stateKey = String(key);
|
|
285
|
+
if (!this.states[stateKey])
|
|
119
286
|
return undefined;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
227
|
-
if (
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
950
|
+
record.callback(value);
|
|
329
951
|
}
|
|
330
952
|
catch (e) {
|
|
331
|
-
|
|
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.
|
|
345
|
-
this.
|
|
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
|
*
|