@adukiorg/anza 0.2.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 (349) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +215 -0
  3. package/bin/anza.js +63 -0
  4. package/bin/create.js +150 -0
  5. package/importmap.json +72 -0
  6. package/package.json +100 -0
  7. package/src/core/animations/index.js +55 -0
  8. package/src/core/animations/play.js +111 -0
  9. package/src/core/animations/registry.js +54 -0
  10. package/src/core/animations/scroll.js +50 -0
  11. package/src/core/animations/tokens.js +58 -0
  12. package/src/core/animations/usage.md +301 -0
  13. package/src/core/animations/waapi.js +86 -0
  14. package/src/core/api/cache.js +120 -0
  15. package/src/core/api/caches/glob.js +24 -0
  16. package/src/core/api/caches/index.js +118 -0
  17. package/src/core/api/events/index.js +75 -0
  18. package/src/core/api/fetch.js +99 -0
  19. package/src/core/api/index.js +158 -0
  20. package/src/core/api/pipeline.js +98 -0
  21. package/src/core/api/plan.md +209 -0
  22. package/src/core/api/prefixes/index.js +66 -0
  23. package/src/core/api/retry.js +69 -0
  24. package/src/core/api/stream.js +127 -0
  25. package/src/core/api/upload.js +180 -0
  26. package/src/core/api/usage.md +206 -0
  27. package/src/core/events/bus.js +38 -0
  28. package/src/core/events/delegate.js +79 -0
  29. package/src/core/events/index.js +26 -0
  30. package/src/core/events/listen.js +50 -0
  31. package/src/core/events/missing.md +103 -0
  32. package/src/core/events/once.js +49 -0
  33. package/src/core/events/plan.md +177 -0
  34. package/src/core/events/types/index.js +34 -0
  35. package/src/core/events/usage.md +107 -0
  36. package/src/core/offline/bridge.js +51 -0
  37. package/src/core/offline/clock.js +100 -0
  38. package/src/core/offline/connectivity.js +116 -0
  39. package/src/core/offline/index.js +41 -0
  40. package/src/core/offline/missing.md +89 -0
  41. package/src/core/offline/plan.md +143 -0
  42. package/src/core/offline/queue.js +168 -0
  43. package/src/core/offline/state.js +18 -0
  44. package/src/core/offline/sync.js +106 -0
  45. package/src/core/offline/usage.md +273 -0
  46. package/src/core/platform/guard.js +104 -0
  47. package/src/core/platform/index.js +42 -0
  48. package/src/core/platform/missing.md +119 -0
  49. package/src/core/platform/platform.d.ts +88 -0
  50. package/src/core/platform/polyfills/anchor.js +79 -0
  51. package/src/core/platform/polyfills/navigation.js +142 -0
  52. package/src/core/platform/polyfills/popover.js +142 -0
  53. package/src/core/platform/polyfills/scheduler.js +194 -0
  54. package/src/core/platform/polyfills/shadow.js +35 -0
  55. package/src/core/platform/polyfills/urlpattern.js +119 -0
  56. package/src/core/platform/supports.js +186 -0
  57. package/src/core/platform/usage.md +287 -0
  58. package/src/core/router/cache.js +95 -0
  59. package/src/core/router/container.js +146 -0
  60. package/src/core/router/handler.js +52 -0
  61. package/src/core/router/history.js +120 -0
  62. package/src/core/router/index.js +158 -0
  63. package/src/core/router/intercept.js +376 -0
  64. package/src/core/router/match.js +145 -0
  65. package/src/core/router/missing.md +716 -0
  66. package/src/core/router/outlet.js +139 -0
  67. package/src/core/router/plan.md +370 -0
  68. package/src/core/router/sync/index.js +16 -0
  69. package/src/core/router/sync/tab.js +115 -0
  70. package/src/core/router/sync/transport.js +139 -0
  71. package/src/core/router/transitions.js +59 -0
  72. package/src/core/router/usage.md +773 -0
  73. package/src/core/security/crypto.js +159 -0
  74. package/src/core/security/index.js +49 -0
  75. package/src/core/security/missing.md +97 -0
  76. package/src/core/security/permissions.js +64 -0
  77. package/src/core/security/sanitize.js +100 -0
  78. package/src/core/security/usage.md +283 -0
  79. package/src/core/state/derived.js +117 -0
  80. package/src/core/state/index.js +23 -0
  81. package/src/core/state/missing.md +165 -0
  82. package/src/core/state/persist.js +284 -0
  83. package/src/core/state/store.js +308 -0
  84. package/src/core/state/sync.js +46 -0
  85. package/src/core/state/usage.md +440 -0
  86. package/src/core/storage/cache.js +83 -0
  87. package/src/core/storage/idb.js +196 -0
  88. package/src/core/storage/index.js +373 -0
  89. package/src/core/storage/lru.js +107 -0
  90. package/src/core/storage/missing.md +165 -0
  91. package/src/core/storage/opfs.js +190 -0
  92. package/src/core/storage/plan.md +69 -0
  93. package/src/core/storage/quota.js +69 -0
  94. package/src/core/storage/usage.md +226 -0
  95. package/src/core/ui/base.js +50 -0
  96. package/src/core/ui/define/container.js +82 -0
  97. package/src/core/ui/define/define.js +12 -0
  98. package/src/core/ui/define/element.js +390 -0
  99. package/src/core/ui/define/index.js +9 -0
  100. package/src/core/ui/define/orchestrator.js +105 -0
  101. package/src/core/ui/define/proxy.js +644 -0
  102. package/src/core/ui/define/state.js +6 -0
  103. package/src/core/ui/define/utils.js +134 -0
  104. package/src/core/ui/implementation.md +170 -0
  105. package/src/core/ui/index.js +41 -0
  106. package/src/core/ui/observe.js +117 -0
  107. package/src/core/ui/plan.md +510 -0
  108. package/src/core/ui/schedule.js +60 -0
  109. package/src/core/ui/template.js +37 -0
  110. package/src/core/ui/transitions.js +37 -0
  111. package/src/core/ui/ui.types.md +890 -0
  112. package/src/core/ui/usage.md +1124 -0
  113. package/src/core/ui/watch.md +346 -0
  114. package/src/core/workers/broadcast.js +138 -0
  115. package/src/core/workers/dedicated.js +153 -0
  116. package/src/core/workers/index.js +169 -0
  117. package/src/core/workers/locks.js +160 -0
  118. package/src/core/workers/offscreen.js +166 -0
  119. package/src/core/workers/plan.md +381 -0
  120. package/src/core/workers/pool.js +267 -0
  121. package/src/core/workers/shared.js +137 -0
  122. package/src/core/workers/usage.md +622 -0
  123. package/src/elements/base.js +12 -0
  124. package/src/elements/data/card/index.html +9 -0
  125. package/src/elements/data/card/index.js +19 -0
  126. package/src/elements/data/card/index.tags.json +1 -0
  127. package/src/elements/data/card/style.css +46 -0
  128. package/src/elements/data/chart/index.html +1 -0
  129. package/src/elements/data/chart/index.js +143 -0
  130. package/src/elements/data/chart/index.tags.json +1 -0
  131. package/src/elements/data/chart/style.css +13 -0
  132. package/src/elements/data/list/index.html +3 -0
  133. package/src/elements/data/list/index.js +19 -0
  134. package/src/elements/data/list/index.tags.json +1 -0
  135. package/src/elements/data/list/style.css +39 -0
  136. package/src/elements/data/stat/index.html +9 -0
  137. package/src/elements/data/stat/index.js +19 -0
  138. package/src/elements/data/stat/index.tags.json +1 -0
  139. package/src/elements/data/stat/style.css +50 -0
  140. package/src/elements/data/table/index.html +1 -0
  141. package/src/elements/data/table/index.js +16 -0
  142. package/src/elements/data/table/index.tags.json +1 -0
  143. package/src/elements/data/table/style.css +50 -0
  144. package/src/elements/feedback/alert/index.html +11 -0
  145. package/src/elements/feedback/alert/index.js +28 -0
  146. package/src/elements/feedback/alert/index.tags.json +1 -0
  147. package/src/elements/feedback/alert/style.css +75 -0
  148. package/src/elements/feedback/empty/index.html +13 -0
  149. package/src/elements/feedback/empty/index.js +34 -0
  150. package/src/elements/feedback/empty/index.tags.json +1 -0
  151. package/src/elements/feedback/empty/style.css +45 -0
  152. package/src/elements/feedback/progress/index.html +7 -0
  153. package/src/elements/feedback/progress/index.js +46 -0
  154. package/src/elements/feedback/progress/index.tags.json +1 -0
  155. package/src/elements/feedback/progress/style.css +36 -0
  156. package/src/elements/feedback/skeleton/index.html +1 -0
  157. package/src/elements/feedback/skeleton/index.js +78 -0
  158. package/src/elements/feedback/skeleton/index.tags.json +1 -0
  159. package/src/elements/feedback/skeleton/style.css +28 -0
  160. package/src/elements/feedback/toast/index.html +3 -0
  161. package/src/elements/feedback/toast/index.js +65 -0
  162. package/src/elements/feedback/toast/index.tags.json +1 -0
  163. package/src/elements/feedback/toast/style.css +36 -0
  164. package/src/elements/forms/checkbox/index.html +7 -0
  165. package/src/elements/forms/checkbox/index.js +104 -0
  166. package/src/elements/forms/checkbox/index.tags.json +1 -0
  167. package/src/elements/forms/checkbox/style.css +86 -0
  168. package/src/elements/forms/field/index.html +13 -0
  169. package/src/elements/forms/field/index.js +42 -0
  170. package/src/elements/forms/field/index.tags.json +1 -0
  171. package/src/elements/forms/field/style.css +42 -0
  172. package/src/elements/forms/form/index.html +3 -0
  173. package/src/elements/forms/form/index.js +122 -0
  174. package/src/elements/forms/form/index.tags.json +1 -0
  175. package/src/elements/forms/form/style.css +11 -0
  176. package/src/elements/forms/input/index.html +4 -0
  177. package/src/elements/forms/input/index.js +103 -0
  178. package/src/elements/forms/input/index.tags.json +1 -0
  179. package/src/elements/forms/input/style.css +39 -0
  180. package/src/elements/forms/radio/index.html +4 -0
  181. package/src/elements/forms/radio/index.js +109 -0
  182. package/src/elements/forms/radio/index.tags.json +1 -0
  183. package/src/elements/forms/radio/style.css +65 -0
  184. package/src/elements/forms/select/index.html +9 -0
  185. package/src/elements/forms/select/index.js +114 -0
  186. package/src/elements/forms/select/index.tags.json +1 -0
  187. package/src/elements/forms/select/style.css +95 -0
  188. package/src/elements/forms/textarea/index.html +4 -0
  189. package/src/elements/forms/textarea/index.js +115 -0
  190. package/src/elements/forms/textarea/index.tags.json +1 -0
  191. package/src/elements/forms/textarea/style.css +46 -0
  192. package/src/elements/forms/toggle/index.html +4 -0
  193. package/src/elements/forms/toggle/index.js +89 -0
  194. package/src/elements/forms/toggle/index.tags.json +1 -0
  195. package/src/elements/forms/toggle/style.css +63 -0
  196. package/src/elements/forms/upload/index.html +13 -0
  197. package/src/elements/forms/upload/index.js +120 -0
  198. package/src/elements/forms/upload/index.tags.json +1 -0
  199. package/src/elements/forms/upload/style.css +61 -0
  200. package/src/elements/index.js +71 -0
  201. package/src/elements/layout/app/index.html +7 -0
  202. package/src/elements/layout/app/index.js +16 -0
  203. package/src/elements/layout/app/index.tags.json +1 -0
  204. package/src/elements/layout/app/style.css +41 -0
  205. package/src/elements/layout/grid/index.html +3 -0
  206. package/src/elements/layout/grid/index.js +41 -0
  207. package/src/elements/layout/grid/index.tags.json +1 -0
  208. package/src/elements/layout/grid/style.css +12 -0
  209. package/src/elements/layout/header/index.html +8 -0
  210. package/src/elements/layout/header/index.js +16 -0
  211. package/src/elements/layout/header/index.tags.json +1 -0
  212. package/src/elements/layout/header/style.css +28 -0
  213. package/src/elements/layout/scroll/index.html +3 -0
  214. package/src/elements/layout/scroll/index.js +19 -0
  215. package/src/elements/layout/scroll/index.tags.json +1 -0
  216. package/src/elements/layout/scroll/style.css +24 -0
  217. package/src/elements/layout/sidebar/index.html +3 -0
  218. package/src/elements/layout/sidebar/index.js +24 -0
  219. package/src/elements/layout/sidebar/index.tags.json +1 -0
  220. package/src/elements/layout/sidebar/style.css +30 -0
  221. package/src/elements/layout/split/index.html +3 -0
  222. package/src/elements/layout/split/index.js +18 -0
  223. package/src/elements/layout/split/index.tags.json +1 -0
  224. package/src/elements/layout/split/style.css +28 -0
  225. package/src/elements/layout/stack/index.html +3 -0
  226. package/src/elements/layout/stack/index.js +31 -0
  227. package/src/elements/layout/stack/index.tags.json +1 -0
  228. package/src/elements/layout/stack/style.css +15 -0
  229. package/src/elements/layout/surface/index.html +3 -0
  230. package/src/elements/layout/surface/index.js +19 -0
  231. package/src/elements/layout/surface/index.tags.json +1 -0
  232. package/src/elements/layout/surface/style.css +29 -0
  233. package/src/elements/navigation/breadcrumb/index.html +5 -0
  234. package/src/elements/navigation/breadcrumb/index.js +16 -0
  235. package/src/elements/navigation/breadcrumb/index.tags.json +1 -0
  236. package/src/elements/navigation/breadcrumb/style.css +36 -0
  237. package/src/elements/navigation/nav/index.html +3 -0
  238. package/src/elements/navigation/nav/index.js +24 -0
  239. package/src/elements/navigation/nav/index.tags.json +1 -0
  240. package/src/elements/navigation/nav/style.css +38 -0
  241. package/src/elements/navigation/pagination/index.html +3 -0
  242. package/src/elements/navigation/pagination/index.js +94 -0
  243. package/src/elements/navigation/pagination/index.tags.json +1 -0
  244. package/src/elements/navigation/pagination/style.css +39 -0
  245. package/src/elements/navigation/steps/index.html +6 -0
  246. package/src/elements/navigation/steps/index.js +64 -0
  247. package/src/elements/navigation/steps/index.tags.json +1 -0
  248. package/src/elements/navigation/steps/style.css +78 -0
  249. package/src/elements/navigation/tabs/index.html +6 -0
  250. package/src/elements/navigation/tabs/index.js +132 -0
  251. package/src/elements/navigation/tabs/index.tags.json +1 -0
  252. package/src/elements/navigation/tabs/style.css +52 -0
  253. package/src/elements/overlay/dialog/index.html +5 -0
  254. package/src/elements/overlay/dialog/index.js +57 -0
  255. package/src/elements/overlay/dialog/index.tags.json +1 -0
  256. package/src/elements/overlay/dialog/style.css +31 -0
  257. package/src/elements/overlay/drawer/index.html +3 -0
  258. package/src/elements/overlay/drawer/index.js +56 -0
  259. package/src/elements/overlay/drawer/index.tags.json +1 -0
  260. package/src/elements/overlay/drawer/style.css +48 -0
  261. package/src/elements/overlay/menu/index.html +3 -0
  262. package/src/elements/overlay/menu/index.js +107 -0
  263. package/src/elements/overlay/menu/index.tags.json +1 -0
  264. package/src/elements/overlay/menu/style.css +43 -0
  265. package/src/elements/overlay/popover/index.html +3 -0
  266. package/src/elements/overlay/popover/index.js +44 -0
  267. package/src/elements/overlay/popover/index.tags.json +1 -0
  268. package/src/elements/overlay/popover/style.css +21 -0
  269. package/src/elements/overlay/sheet/index.html +8 -0
  270. package/src/elements/overlay/sheet/index.js +105 -0
  271. package/src/elements/overlay/sheet/index.tags.json +1 -0
  272. package/src/elements/overlay/sheet/style.css +64 -0
  273. package/src/elements/overlay/tooltip/index.html +6 -0
  274. package/src/elements/overlay/tooltip/index.js +16 -0
  275. package/src/elements/overlay/tooltip/index.tags.json +1 -0
  276. package/src/elements/overlay/tooltip/style.css +41 -0
  277. package/src/elements/primitives/avatar/index.html +2 -0
  278. package/src/elements/primitives/avatar/index.js +79 -0
  279. package/src/elements/primitives/avatar/index.tags.json +1 -0
  280. package/src/elements/primitives/avatar/style.css +36 -0
  281. package/src/elements/primitives/badge/index.html +3 -0
  282. package/src/elements/primitives/badge/index.js +20 -0
  283. package/src/elements/primitives/badge/index.tags.json +1 -0
  284. package/src/elements/primitives/badge/style.css +67 -0
  285. package/src/elements/primitives/button/index.html +3 -0
  286. package/src/elements/primitives/button/index.js +61 -0
  287. package/src/elements/primitives/button/index.tags.json +1 -0
  288. package/src/elements/primitives/button/style.css +66 -0
  289. package/src/elements/primitives/divider/index.html +1 -0
  290. package/src/elements/primitives/divider/index.js +43 -0
  291. package/src/elements/primitives/divider/index.tags.json +1 -0
  292. package/src/elements/primitives/divider/style.css +39 -0
  293. package/src/elements/primitives/icon/index.html +3 -0
  294. package/src/elements/primitives/icon/index.js +66 -0
  295. package/src/elements/primitives/icon/index.tags.json +1 -0
  296. package/src/elements/primitives/icon/style.css +20 -0
  297. package/src/elements/primitives/link/index.html +3 -0
  298. package/src/elements/primitives/link/index.js +129 -0
  299. package/src/elements/primitives/link/index.tags.json +1 -0
  300. package/src/elements/primitives/link/style.css +40 -0
  301. package/src/elements/primitives/spinner/index.html +1 -0
  302. package/src/elements/primitives/spinner/index.js +62 -0
  303. package/src/elements/primitives/spinner/index.tags.json +1 -0
  304. package/src/elements/primitives/spinner/style.css +20 -0
  305. package/src/elements/primitives/text/index.html +1 -0
  306. package/src/elements/primitives/text/index.js +79 -0
  307. package/src/elements/primitives/text/index.tags.json +1 -0
  308. package/src/elements/primitives/text/style.css +25 -0
  309. package/src/index.js +23 -0
  310. package/src/styles/base.css +66 -0
  311. package/src/styles/index.css +10 -0
  312. package/src/styles/layers.css +9 -0
  313. package/src/styles/reset.css +66 -0
  314. package/src/sw/activate.js +51 -0
  315. package/src/sw/expire.js +47 -0
  316. package/src/sw/index.js +28 -0
  317. package/src/sw/install.js +35 -0
  318. package/src/sw/push.js +58 -0
  319. package/src/sw/queue.js +60 -0
  320. package/src/sw/routes.js +71 -0
  321. package/src/sw/strategies.js +247 -0
  322. package/src/sw/sync.js +80 -0
  323. package/src/tokens/index.css +26 -0
  324. package/src/tokens/primitives/colors.css +54 -0
  325. package/src/tokens/primitives/motion.css +34 -0
  326. package/src/tokens/primitives/radius.css +16 -0
  327. package/src/tokens/primitives/shadow.css +34 -0
  328. package/src/tokens/primitives/spacing.css +27 -0
  329. package/src/tokens/primitives/typography.css +46 -0
  330. package/src/tokens/primitives/zindex.css +18 -0
  331. package/src/tokens/registered/colors.css +133 -0
  332. package/src/tokens/registered/dimensions.css +31 -0
  333. package/src/tokens/semantic/components.css +125 -0
  334. package/src/tokens/semantic/contrast.css +33 -0
  335. package/src/tokens/semantic/dark.css +61 -0
  336. package/src/tokens/semantic/light.css +64 -0
  337. package/types/core/animations/index.d.ts +52 -0
  338. package/types/core/api/index.d.ts +68 -0
  339. package/types/core/events/index.d.ts +50 -0
  340. package/types/core/offline/index.d.ts +68 -0
  341. package/types/core/platform/index.d.ts +60 -0
  342. package/types/core/router/index.d.ts +203 -0
  343. package/types/core/security/index.d.ts +33 -0
  344. package/types/core/state/index.d.ts +68 -0
  345. package/types/core/storage/index.d.ts +40 -0
  346. package/types/core/ui/index.d.ts +446 -0
  347. package/types/core/workers/index.d.ts +221 -0
  348. package/types/elements/index.d.ts +150 -0
  349. package/types/index.d.ts +18 -0
@@ -0,0 +1,440 @@
1
+ # Native State Usage Guide
2
+
3
+ The Native State layer provides a lightweight, reactive, and persistent state management engine. It encapsulates reactive stores, opt-in deep tracking, derived computations, cross-tab synchronization, transaction-safe IndexedDB persistence, automated TTL/LRU cache eviction, and build-time store immutability validation.
4
+
5
+ Status: this guide documents the implemented public state contract: `state.create`, `ReactiveStore`, `derived`, `sync`, `storage`, `PlatformStorage`, deep reactivity, custom snapshots/cloners, transaction queues, and the build-time immutability linter.
6
+
7
+ Import from the state entry point:
8
+
9
+ ```javascript
10
+ import { state } from '@adukiorg/anza/state';
11
+ ```
12
+
13
+ Or import individual components directly:
14
+
15
+ ```javascript
16
+ import { ReactiveStore, derived, sync, storage } from '@adukiorg/anza/state';
17
+ ```
18
+
19
+ ---
20
+
21
+ ## 1. Choosing an API
22
+
23
+ | Need | Use |
24
+ | --- | --- |
25
+ | Initialize a reactive store | `state.create` or `new ReactiveStore` |
26
+ | Get a store property | `store.get` |
27
+ | Update a store property | `store.set` |
28
+ | Subscribe to property changes | `store.subscribe` |
29
+ | Clear or initialize state | `store.reset` |
30
+ | Restore state from snapshot | `store.hydrate` |
31
+ | Capture a cloned snapshot | `store.snapshot` |
32
+ | Opt-in nested object tracking | `options.deep` |
33
+ | Sync properties across tabs | `store.sync` or `state.sync` |
34
+ | Sync property with channel | `store.broadcast` or `state.broadcast` |
35
+ | Computed read-only state | `store.derived` or `state.derived` |
36
+ | Connect to IndexedDB storage | `storage` or `new PlatformStorage` |
37
+ | Store value in disk cache | `storage.set` |
38
+ | Retrieve value from disk cache | `storage.get` |
39
+ | Delete disk cache entry | `storage.delete` |
40
+ | Query disk cache entries | `storage.query` |
41
+ | Schema migrations | `storage.registerMigrations` |
42
+ | Disk space estimate | `storage.estimate` |
43
+ | Request persistent storage | `storage.persist` |
44
+
45
+ ---
46
+
47
+ ## 2. Store Creation
48
+
49
+ Use `state.create(initial, options)` or `new ReactiveStore(initial, options)` to construct a reactive store.
50
+
51
+ ```javascript
52
+ const store = state.create({
53
+ active: false,
54
+ items: []
55
+ });
56
+ ```
57
+
58
+ Available options:
59
+
60
+ ```javascript
61
+ const store = new ReactiveStore({
62
+ user: { name: 'Alice', address: { city: 'Paris' } }
63
+ }, {
64
+ deep: true, // Enable deep reactivity proxying (default: false)
65
+ clone: (val) => myClone(val) // Custom cloner for snapshots (default: optimized fastClone)
66
+ });
67
+ ```
68
+
69
+ Rules:
70
+
71
+ - Pass plain objects or values that are structured-clone-safe.
72
+ - Custom cloners override the default snapshot copy behavior (useful for non-standard classes or complex structures).
73
+
74
+ ---
75
+
76
+ ## 3. Reading and Writing
77
+
78
+ Read values using `store.get(key)` and update values using `store.set(key, value)`.
79
+
80
+ ```javascript
81
+ // Reading properties
82
+ const active = store.get('active');
83
+
84
+ // Writing properties
85
+ store.set('active', true);
86
+ ```
87
+
88
+ Writes are batched and scheduled to notify subscribers inside a microtask. If you need to perform multiple updates sequentially without triggering multiple subscriber notifications, group them using `store.batch()`.
89
+
90
+ ```javascript
91
+ store.batch(() => {
92
+ store.set('active', true);
93
+ store.set('items', [1, 2, 3]);
94
+ });
95
+ // Subscribers are notified exactly once after the batch completes.
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 4. Subscription Management
101
+
102
+ Use `store.subscribe(key, callback, signal?)` to listen to property changes.
103
+
104
+ ```javascript
105
+ const dispose = store.subscribe('active', (next, key, prev) => {
106
+ console.log(`${key} changed from ${prev} to ${next}`);
107
+ });
108
+
109
+ // Teardown the subscription
110
+ dispose();
111
+ ```
112
+
113
+ When integrating subscriptions inside component lifecycles, pass an `AbortSignal` to ensure automatic cleanup on unmount:
114
+
115
+ ```javascript
116
+ ui.element('ui-panel', {
117
+ mount({ el, ctrl }) {
118
+ store.subscribe('active', (active) => {
119
+ el.toggleAttribute('active', active);
120
+ }, ctrl.signal);
121
+ }
122
+ });
123
+ ```
124
+
125
+ Rules:
126
+
127
+ - Subscriber callbacks receive `(next, key, prev)` — the current value, the key that changed, and the previous value.
128
+ - A callback subscribed to several keys is invoked once per flush, with one of the changed keys.
129
+ - Callbacks run asynchronously on microtask ticks unless batching is active.
130
+ - Aborted signals automatically clean up and remove the subscriber from the registry.
131
+
132
+ ---
133
+
134
+ ## 5. Deep Reactivity
135
+
136
+ By default, stores only track top-level property assignments. Set `deep: true` to enable tracking of nested mutations:
137
+
138
+ ```javascript
139
+ const store = new ReactiveStore({
140
+ profile: { name: 'Bob', settings: { theme: 'dark' } }
141
+ }, { deep: true });
142
+
143
+ store.subscribe('profile', (next) => {
144
+ console.log('Profile changed:', next.settings.theme);
145
+ });
146
+
147
+ // Mutating a nested key triggers the parent "profile" subscription
148
+ store.get('profile').settings.theme = 'light';
149
+ ```
150
+
151
+ Under the hood:
152
+
153
+ - The store wraps objects recursively in reactive `Proxy` traps.
154
+ - Mutations propagate up to mark the top-level keys as dirty.
155
+ - Mutated objects are cached in a `WeakMap` to avoid redundant proxy allocations.
156
+
157
+ ---
158
+
159
+ ## 6. Snapshots and Cloners
160
+
161
+ Use `store.snapshot()` to extract a copy of the entire current state. You can restore this state later using `store.hydrate(state)` or clear it using `store.reset(state)`.
162
+
163
+ ```javascript
164
+ const backup = store.snapshot();
165
+
166
+ // Clear and override the current state
167
+ store.reset({ active: false });
168
+
169
+ // Restore the backup
170
+ store.hydrate(backup);
171
+ ```
172
+
173
+ Cloning strategies:
174
+
175
+ - The default cloner uses an optimized `fastClone` path that avoids the overhead of JSON parsing or standard `structuredClone` for plain objects, arrays, and primitives. It falls back to constructor-based cloning for Date and RegExp.
176
+ - Provide a custom `clone` function in store options to handle domain-specific object structures.
177
+
178
+ ---
179
+
180
+ ## 7. Derived Computations
181
+
182
+ Use `derived` to calculate read-only computed values that automatically react to store changes.
183
+
184
+ ```javascript
185
+ const store = state.create({ count: 2, price: 10 });
186
+
187
+ // Define derived state using state.derived (standalone)
188
+ const total = state.derived(() => {
189
+ return store.get('count') * store.get('price');
190
+ });
191
+
192
+ // Or using store.derived (instance method)
193
+ const total = store.derived(() => {
194
+ return store.get('count') * store.get('price');
195
+ });
196
+
197
+ console.log(total.value); // 20
198
+
199
+ // Derived state is an observable exposing .value
200
+ const stop = total.subscribe((next) => {
201
+ console.log(`New total: ${next}`);
202
+ });
203
+
204
+ store.set('count', 3); // Triggers derived recalculation. total.value becomes 30.
205
+
206
+ stop();
207
+ ```
208
+
209
+ Rules:
210
+
211
+ - Derived functions capture store getters called during execution to automatically track dependencies.
212
+ - Subscriptions are dynamically added and removed as dependencies execute.
213
+ - Computation updates are batched and scheduled in microtasks.
214
+
215
+ ---
216
+
217
+ ## 8. Cross-Tab Sync
218
+
219
+ Use the `sync` or `broadcast` delegates to coordinate state changes between multiple open browser tabs.
220
+
221
+ ### State Synchronization
222
+
223
+ Sync coordinates state changes across same-origin tabs for specific keys using a BroadcastChannel:
224
+
225
+ ```javascript
226
+ // Using state.sync (standalone)
227
+ const stop = state.sync(store, ['theme', 'token'], {
228
+ channel: 'app-state-sync' // Optional custom channel name
229
+ });
230
+
231
+ // Using store.sync (instance method)
232
+ const stop = store.sync(['theme', 'token'], 'app-state-sync');
233
+
234
+ // To stop synchronization
235
+ stop();
236
+ ```
237
+
238
+ ### Broadcast Messaging
239
+
240
+ Use `broadcast` to emit specific key modifications or messages down a BroadcastChannel:
241
+
242
+ ```javascript
243
+ // Using state.broadcast (standalone)
244
+ const stop = state.broadcast(store, 'chat-events', ['message']);
245
+
246
+ // Using store.broadcast (instance method)
247
+ const stop = store.broadcast('chat-events', ['message']);
248
+
249
+ store.subscribe('message', (msg) => {
250
+ console.log('Sending message down channel:', msg);
251
+ });
252
+
253
+ stop();
254
+ ```
255
+
256
+ ---
257
+
258
+ ## 9. Disk Persistence Layer
259
+
260
+ Use `storage` (an instance of `PlatformStorage`) to store data persistently in IndexedDB.
261
+
262
+ ```javascript
263
+ // Register schema migrations
264
+ storage.registerMigrations([
265
+ (db) => {
266
+ db.createObjectStore('settings');
267
+ }
268
+ ]);
269
+
270
+ await storage.open();
271
+
272
+ // Write to store
273
+ await storage.set('settings', 'theme', 'dark');
274
+
275
+ // Read from store
276
+ const theme = await storage.get('settings', 'theme');
277
+
278
+ // Delete from store
279
+ await storage.delete('settings', 'theme');
280
+ ```
281
+
282
+ For custom database instances, construct your own `PlatformStorage`:
283
+
284
+ ```javascript
285
+ const myDB = new PlatformStorage();
286
+ myDB.setDatabaseName('custom-platform-db');
287
+ myDB.registerMigrations([
288
+ (db) => {
289
+ db.createObjectStore('cache');
290
+ }
291
+ ]);
292
+ await myDB.open();
293
+ ```
294
+
295
+ ---
296
+
297
+ ## 10. Transaction Queuing & Retries
298
+
299
+ All database writes (`set`, `delete`) are routed through a sequence-preserving `WriteQueue` to prevent transaction conflicts.
300
+
301
+ ```javascript
302
+ // Concurrent writes are queued sequentially
303
+ await Promise.all([
304
+ storage.set('settings', 'key1', 'val1'),
305
+ storage.set('settings', 'key2', 'val2')
306
+ ]);
307
+ ```
308
+
309
+ Features:
310
+
311
+ - **Write Ordering**: Guarantees that writes resolve in the exact order they were requested.
312
+ - **Auto-Retries**: If a write fails due to transient locks or database errors, the queue retries the operation up to 3 times with exponential backoff delays.
313
+
314
+ ---
315
+
316
+ ## 11. Cache Eviction & Space Recovery
317
+
318
+ `PlatformStorage` automatically wraps stored items inside metadata envelopes:
319
+
320
+ ```javascript
321
+ {
322
+ value: any,
323
+ lastAccessed: number,
324
+ expires: number | null
325
+ }
326
+ ```
327
+
328
+ This supports advanced storage pruning strategies:
329
+
330
+ ### Time-to-Live (TTL) Eviction
331
+
332
+ Specify a TTL (in milliseconds) during `set` to automatically expire records.
333
+
334
+ ```javascript
335
+ // Expire after 10 seconds
336
+ await storage.set('settings', 'token', 'abc', { ttl: 10000 });
337
+ ```
338
+
339
+ Expired records are automatically pruned from the database when new keys are written or checked.
340
+
341
+ ### Least Recently Used (LRU) Eviction
342
+
343
+ When origin storage usage exceeds **80%** of the browser allocation:
344
+
345
+ 1. A global `'quota'` event is dispatched on the `window` object containing the current `usage` and `quota`.
346
+ 2. The storage layer automatically deletes expired records first.
347
+ 3. If the ratio remains above 80%, it sorts active records by `lastAccessed` and evicts the oldest entries until usage falls below the threshold.
348
+
349
+ ```javascript
350
+ window.addEventListener('quota', (event) => {
351
+ const { usage, quota } = event.detail;
352
+ console.warn(`Storage space warning: ${usage} bytes used out of ${quota}`);
353
+ });
354
+ ```
355
+
356
+ ---
357
+
358
+ ## 12. Build-Time Immutability Warnings
359
+
360
+ Directly mutating properties retrieved from a store causes side-effects and breaks clean subscription flows. The compiler tools include an SWC-powered `ImmutabilityVisitor` to catch these issues at build-time.
361
+
362
+ ### Bad Practice (Triggers Warning)
363
+
364
+ ```javascript
365
+ const user = store.get('user');
366
+ user.name = 'Dave'; // Static compiler warning! Mutating store variable directly.
367
+ ```
368
+
369
+ ### Good Practice
370
+
371
+ ```javascript
372
+ const user = store.snapshot().user;
373
+ user.name = 'Dave';
374
+ store.set('user', user); // Correct: update via set()
375
+ ```
376
+
377
+ When building or running in dev mode, the compiler logs warnings:
378
+
379
+ ```text
380
+ [WARN] Immutability Violation in src/elements/profile.js: mutating property 'name' of store-retrieved variable 'user' directly. Use store.set() instead.
381
+ ```
382
+
383
+ ---
384
+
385
+ ## 13. Testing State Code
386
+
387
+ ### Stubbing Storage Quota
388
+
389
+ To test LRU eviction behavior under storage pressure, stub `navigator.storage.estimate` and dispatcher handlers:
390
+
391
+ ```javascript
392
+ let quotaFired = false;
393
+ const onQuota = () => { quotaFired = true; };
394
+ window.addEventListener('quota', onQuota);
395
+
396
+ const originalEstimate = navigator.storage?.estimate;
397
+ if (navigator.storage) {
398
+ navigator.storage.estimate = async () => ({
399
+ quota: 1000,
400
+ usage: 850 // >80% threshold
401
+ });
402
+ }
403
+
404
+ // Perform writes and verify eviction behavior...
405
+
406
+ window.removeEventListener('quota', onQuota);
407
+ navigator.storage.estimate = originalEstimate;
408
+ ```
409
+
410
+ ### Resetting Stores Between Tests
411
+
412
+ Always reset or clear stores and databases in test setup/teardown blocks:
413
+
414
+ ```javascript
415
+ beforeEach(() => {
416
+ store.reset({ active: false });
417
+ });
418
+
419
+ afterEach(async () => {
420
+ // Delete the test IndexedDB database
421
+ await new Promise((resolve) => {
422
+ const req = indexedDB.deleteDatabase('test-platform-db');
423
+ req.onsuccess = () => resolve();
424
+ req.onerror = () => resolve();
425
+ });
426
+ });
427
+ ```
428
+
429
+ ---
430
+
431
+ ## 14. Checklist
432
+
433
+ - Use `state.create()` to instantiate reactive stores.
434
+ - Use `store.batch()` when writing multiple keys sequentially to avoid redundant notifications.
435
+ - Use `store.subscribe()` to listen to updates, and pass `AbortSignal` for automated component lifecycle cleanup.
436
+ - Enable `deep: true` in options only if you need to track nested object mutations directly.
437
+ - Use `state.derived()` to represent computed, dependency-tracked read-only state.
438
+ - Keep store mutations out of direct assignments on values returned from `store.get()`. Use `store.snapshot()` or update via `store.set()`.
439
+ - Register migrations via `storage.registerMigrations` sequentially in order to set up your IndexedDB stores correctly.
440
+ - Clean up test databases and reset store states inside `afterEach` hooks to prevent cross-test data pollution.
@@ -0,0 +1,83 @@
1
+ /**
2
+ * src/core/storage/cache.js
3
+ *
4
+ * Cache API Wrapper.
5
+ * Provides a Promise-based cache client with seamless TTL (Time-To-Live)
6
+ * support by leveraging cloned Response header extensions.
7
+ *
8
+ * Source: doc 11 — Networking §6, doc 22 — Storage Architecture §5
9
+ */
10
+
11
+ export class CacheStorage {
12
+ constructor(name) {
13
+ this.name = name;
14
+ }
15
+
16
+ /**
17
+ * Opens the underlying browser cache.
18
+ */
19
+ open() {
20
+ return caches.open(this.name);
21
+ }
22
+
23
+ /**
24
+ * Retrieves a cached response, automatically evicting if expired.
25
+ */
26
+ async get(request) {
27
+ const cache = await this.open();
28
+ const req = typeof request === 'string' ? new Request(request) : request;
29
+ const res = await cache.match(req);
30
+
31
+ if (!res) return null;
32
+
33
+ // Evaluate custom TTL header
34
+ const expires = res.headers.get('x-expires-at');
35
+ if (expires && Date.now() > parseInt(expires, 10)) {
36
+ await cache.delete(req);
37
+ return null;
38
+ }
39
+
40
+ return res;
41
+ }
42
+
43
+ /**
44
+ * Caches a Response with an optional TTL (Time-to-Live).
45
+ */
46
+ async set(request, response, ttlMs) {
47
+ const cache = await this.open();
48
+ const req = typeof request === 'string' ? new Request(request) : request;
49
+
50
+ let finalResponse = response.clone();
51
+
52
+ // Attach custom expiry header if a TTL is provided and response allows header mutation
53
+ if (ttlMs && response.type !== 'opaque') {
54
+ const headers = new Headers(response.headers);
55
+ headers.set('x-expires-at', String(Date.now() + ttlMs));
56
+
57
+ // Construct a new response copying the original body stream and properties
58
+ finalResponse = new Response(response.body ? response.clone().body : null, {
59
+ status: response.status,
60
+ statusText: response.statusText,
61
+ headers
62
+ });
63
+ }
64
+
65
+ await cache.put(req, finalResponse);
66
+ }
67
+
68
+ /**
69
+ * Removes an entry from the cache.
70
+ */
71
+ async delete(request) {
72
+ const cache = await this.open();
73
+ const req = typeof request === 'string' ? new Request(request) : request;
74
+ return cache.delete(req);
75
+ }
76
+
77
+ /**
78
+ * Deletes the entire cache storage pool.
79
+ */
80
+ async clear() {
81
+ return caches.delete(this.name);
82
+ }
83
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * src/core/storage/idb.js
3
+ *
4
+ * Promise-wrapped IndexedDB Adapter.
5
+ * Encapsulates transactional storage operations, sequential migrations,
6
+ * and cursor/index queries.
7
+ *
8
+ * Source: doc 22 — Storage Architecture §1, §3
9
+ */
10
+
11
+ export class Database {
12
+ #db = null;
13
+
14
+ constructor(name, version, migrations = []) {
15
+ this.name = name;
16
+ this.version = version;
17
+ this.migrations = migrations; // array of migration functions
18
+ }
19
+
20
+ open() {
21
+ if (this.#db) return Promise.resolve(this.#db);
22
+
23
+ return new Promise((resolve, reject) => {
24
+ const request = indexedDB.open(this.name, this.version);
25
+
26
+ request.onblocked = () => {
27
+ console.warn(`IndexedDB database ${this.name} upgrade is blocked by another tab.`);
28
+ if (typeof window !== 'undefined') {
29
+ window.dispatchEvent(new CustomEvent('storage:blocked', { detail: { name: this.name } }));
30
+ }
31
+ };
32
+
33
+ request.onupgradeneeded = (event) => {
34
+ const db = request.result;
35
+ const oldVersion = event.oldVersion;
36
+ const newVersion = event.newVersion;
37
+
38
+ // Perform sequential version migrations (non-skipping)
39
+ for (let v = oldVersion; v < newVersion; v++) {
40
+ const migrate = this.migrations[v];
41
+ if (typeof migrate === 'function') {
42
+ try {
43
+ migrate(db);
44
+ } catch (err) {
45
+ console.error(`Migration to version ${v + 1} failed:`, err);
46
+ }
47
+ }
48
+ }
49
+ };
50
+
51
+ request.onsuccess = () => {
52
+ this.#db = request.result;
53
+ this.#db.onversionchange = () => {
54
+ console.warn(`IndexedDB database ${this.name} version change requested. Closing connection.`);
55
+ this.close();
56
+ if (typeof window !== 'undefined') {
57
+ window.dispatchEvent(new CustomEvent('storage:versionchange', { detail: { name: this.name } }));
58
+ }
59
+ };
60
+ resolve(this.#db);
61
+ };
62
+
63
+ request.onerror = () => {
64
+ reject(request.error);
65
+ };
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Closes the active IndexedDB connection.
71
+ */
72
+ close() {
73
+ if (this.#db) {
74
+ this.#db.close();
75
+ this.#db = null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Run a callback inside a multi-store transaction context.
81
+ */
82
+ transaction(storeNames, mode, callback) {
83
+ return this.open().then((db) => {
84
+ return new Promise((resolve, reject) => {
85
+ const tx = db.transaction(storeNames, mode);
86
+ const storeAccessor = (name) => tx.objectStore(name);
87
+
88
+ let result;
89
+ tx.oncomplete = () => resolve(result);
90
+ tx.onerror = () => reject(tx.error);
91
+ tx.onabort = () => reject(new Error('Transaction aborted'));
92
+
93
+ try {
94
+ const p = callback(storeAccessor, tx);
95
+ if (p && typeof p.then === 'function') {
96
+ p.then(
97
+ (val) => { result = val; },
98
+ (err) => {
99
+ try { tx.abort(); } catch {}
100
+ reject(err);
101
+ }
102
+ );
103
+ } else {
104
+ result = p;
105
+ }
106
+ } catch (err) {
107
+ try { tx.abort(); } catch {}
108
+ reject(err);
109
+ }
110
+ });
111
+ });
112
+ }
113
+
114
+
115
+ /**
116
+ * Helper to execute a single-store transaction.
117
+ */
118
+ #run(storeName, mode, callback) {
119
+ return this.open().then((db) => {
120
+ return new Promise((resolve, reject) => {
121
+ const tx = db.transaction(storeName, mode);
122
+ const store = tx.objectStore(storeName);
123
+
124
+ let result;
125
+ tx.oncomplete = () => resolve(result);
126
+ tx.onerror = () => reject(tx.error);
127
+ tx.onabort = () => reject(new Error('Transaction aborted'));
128
+
129
+ try {
130
+ result = callback(store, tx);
131
+ } catch (err) {
132
+ tx.abort();
133
+ reject(err);
134
+ }
135
+ });
136
+ });
137
+ }
138
+
139
+ get(storeName, key) {
140
+ let req;
141
+ return this.#run(storeName, 'readonly', (store) => {
142
+ req = store.get(key);
143
+ }).then(() => req.result ?? null);
144
+ }
145
+
146
+ set(storeName, key, value) {
147
+ return this.#run(storeName, 'readwrite', (store) => {
148
+ store.put(value, key);
149
+ }).then(() => {});
150
+ }
151
+
152
+ delete(storeName, key) {
153
+ return this.#run(storeName, 'readwrite', (store) => {
154
+ store.delete(key);
155
+ }).then(() => {});
156
+ }
157
+
158
+ clear(storeName) {
159
+ return this.#run(storeName, 'readwrite', (store) => {
160
+ store.clear();
161
+ }).then(() => {});
162
+ }
163
+
164
+ getAll(storeName) {
165
+ let req;
166
+ return this.#run(storeName, 'readonly', (store) => {
167
+ req = store.getAll();
168
+ }).then(() => req.result || []);
169
+ }
170
+
171
+ keys(storeName) {
172
+ let req;
173
+ return this.#run(storeName, 'readonly', (store) => {
174
+ req = store.getAllKeys();
175
+ }).then(() => req.result || []);
176
+ }
177
+
178
+ /**
179
+ * Advanced query matching using indices, ranges, directions, and limits.
180
+ */
181
+ query(storeName, { index, range, direction = 'next', limit = Infinity } = {}) {
182
+ const results = [];
183
+ return this.#run(storeName, 'readonly', (store) => {
184
+ const source = index ? store.index(index) : store;
185
+ const req = source.openCursor(range, direction);
186
+
187
+ req.onsuccess = (e) => {
188
+ const cursor = e.target.result;
189
+ if (cursor && results.length < limit) {
190
+ results.push(cursor.value);
191
+ cursor.continue();
192
+ }
193
+ };
194
+ }).then(() => results);
195
+ }
196
+ }