@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,284 @@
1
+ /**
2
+ * src/core/state/persist.js
3
+ *
4
+ * Transaction-aware IndexedDB persistence engine.
5
+ * Wraps low-level IDBRequest events with a clean Promise-based facade,
6
+ * manages chronologically ordered schema upgrades inside versionchange transactions,
7
+ * and exposes storage manager quota/persistence handlers.
8
+ *
9
+ * Source: doc 08 — State Management §5, §12, §13, §15
10
+ */
11
+
12
+ class WriteQueue {
13
+ #queue = Promise.resolve();
14
+
15
+ enqueue(task) {
16
+ this.#queue = this.#queue.then(async () => {
17
+ let retries = 3;
18
+ let delay = 50;
19
+ while (retries > 0) {
20
+ try {
21
+ return await task();
22
+ } catch (err) {
23
+ retries--;
24
+ if (retries === 0) throw err;
25
+ await new Promise((resolve) => setTimeout(resolve, delay));
26
+ delay *= 2;
27
+ }
28
+ }
29
+ });
30
+ return this.#queue;
31
+ }
32
+ }
33
+
34
+ export class PlatformStorage {
35
+ #db = null;
36
+ #migrations = [];
37
+ #version = 1;
38
+ #dbName = 'platform-db';
39
+ #writeQueue = new WriteQueue();
40
+
41
+ /**
42
+ * Configures custom database name (primarily for test isolation).
43
+ */
44
+ setDatabaseName(name) {
45
+ this.#dbName = name;
46
+ }
47
+
48
+ /**
49
+ * Registers custom schema migration handlers.
50
+ * Migrations are index-mapped to the sequential schema upgrades (oldVersion -> N).
51
+ */
52
+ registerMigrations(migrations) {
53
+ this.#migrations = migrations;
54
+ this.#version = migrations.length + 1;
55
+ }
56
+
57
+ /**
58
+ * Connects to IndexedDB, evaluating upgrades sequentially inside transactions.
59
+ */
60
+ async open() {
61
+ if (this.#db) return this.#db;
62
+
63
+ return new Promise((resolve, reject) => {
64
+ const request = this.#migrations.length > 0
65
+ ? indexedDB.open(this.#dbName, this.#version)
66
+ : indexedDB.open(this.#dbName);
67
+
68
+ request.onupgradeneeded = (event) => {
69
+ const db = event.target.result;
70
+ const tx = event.target.transaction;
71
+ const oldVersion = event.oldVersion;
72
+
73
+ for (let i = oldVersion; i < this.#migrations.length; i++) {
74
+ try {
75
+ this.#migrations[i](db, tx);
76
+ } catch (err) {
77
+ console.error(`Storage schema migration failed for version v${i + 1}:`, err);
78
+ tx.abort();
79
+ return reject(err);
80
+ }
81
+ }
82
+ };
83
+
84
+ request.onsuccess = (event) => {
85
+ this.#db = event.target.result;
86
+ resolve(this.#db);
87
+ };
88
+
89
+ request.onerror = (event) => {
90
+ reject(event.target.error);
91
+ };
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Retrieves a record from a specific store by its key.
97
+ */
98
+ async get(storeName, key) {
99
+ const db = await this.open();
100
+ return new Promise((resolve, reject) => {
101
+ const tx = db.transaction(storeName, 'readonly');
102
+ const store = tx.objectStore(storeName);
103
+ const request = store.get(key);
104
+
105
+ request.onsuccess = async () => {
106
+ const res = request.result;
107
+ if (res && typeof res === 'object' && 'value' in res && 'lastAccessed' in res) {
108
+ try {
109
+ await this.#updateAccess(storeName, key, res);
110
+ } catch (err) {
111
+ console.error('Failed to update access timestamp:', err);
112
+ }
113
+ resolve(res.value);
114
+ } else {
115
+ resolve(res || null);
116
+ }
117
+ };
118
+ request.onerror = () => reject(request.error);
119
+ });
120
+ }
121
+
122
+ async #updateAccess(storeName, key, envelope) {
123
+ const db = await this.open();
124
+ return new Promise((resolve, reject) => {
125
+ const tx = db.transaction(storeName, 'readwrite');
126
+ const store = tx.objectStore(storeName);
127
+ envelope.lastAccessed = Date.now();
128
+ const req = store.put(envelope, key);
129
+ req.onsuccess = () => resolve();
130
+ req.onerror = () => reject(req.error);
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Adds or updates a record inside a specific store.
136
+ */
137
+ async set(storeName, key, value, options = {}) {
138
+ return this.#writeQueue.enqueue(async () => {
139
+ const db = await this.open();
140
+ await this.#checkAndEvict(storeName);
141
+
142
+ const envelope = {
143
+ value,
144
+ lastAccessed: Date.now(),
145
+ expires: options.ttl ? Date.now() + options.ttl : null
146
+ };
147
+
148
+ return new Promise((resolve, reject) => {
149
+ const tx = db.transaction(storeName, 'readwrite');
150
+ const store = tx.objectStore(storeName);
151
+
152
+ // key is optional if store has inline keyPath
153
+ const request = key ? store.put(envelope, key) : store.put(envelope);
154
+
155
+ request.onsuccess = () => resolve();
156
+ request.onerror = () => reject(request.error);
157
+ });
158
+ });
159
+ }
160
+
161
+ async #checkAndEvict(storeName) {
162
+ const estimate = await this.estimate();
163
+ if (!estimate.quota || !estimate.usage) return;
164
+
165
+ const ratio = estimate.usage / estimate.quota;
166
+ if (ratio > 0.8) {
167
+ if (typeof window !== 'undefined') {
168
+ window.dispatchEvent(new CustomEvent('quota', {
169
+ detail: { usage: estimate.usage, quota: estimate.quota }
170
+ }));
171
+ }
172
+
173
+ const db = await this.open();
174
+ if (!db.objectStoreNames.contains(storeName)) return;
175
+
176
+ const records = await new Promise((resolve) => {
177
+ const tx = db.transaction(storeName, 'readonly');
178
+ const store = tx.objectStore(storeName);
179
+ const list = [];
180
+ store.openCursor().onsuccess = (event) => {
181
+ const cursor = event.target.result;
182
+ if (cursor) {
183
+ list.push({ key: cursor.key, envelope: cursor.value });
184
+ cursor.continue();
185
+ } else {
186
+ resolve(list);
187
+ }
188
+ };
189
+ });
190
+
191
+ const now = Date.now();
192
+ const expired = records.filter(
193
+ (r) => r.envelope && typeof r.envelope === 'object' && r.envelope.expires && r.envelope.expires < now
194
+ );
195
+
196
+ if (expired.length > 0) {
197
+ await Promise.all(expired.map((r) => this.delete(storeName, r.key)));
198
+ const newEst = await this.estimate();
199
+ if ((newEst.usage / newEst.quota) <= 0.8) return;
200
+ }
201
+
202
+ const activeWrapped = records
203
+ .filter((r) => r.envelope && typeof r.envelope === 'object' && 'lastAccessed' in r.envelope)
204
+ .sort((a, b) => a.envelope.lastAccessed - b.envelope.lastAccessed);
205
+
206
+ for (const record of activeWrapped) {
207
+ await this.delete(storeName, record.key);
208
+ const newEst = await this.estimate();
209
+ if ((newEst.usage / newEst.quota) <= 0.8) break;
210
+ }
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Deletes a record from a specific store.
216
+ */
217
+ async delete(storeName, key) {
218
+ const db = await this.open();
219
+ return new Promise((resolve, reject) => {
220
+ const tx = db.transaction(storeName, 'readwrite');
221
+ const store = tx.objectStore(storeName);
222
+ const request = store.delete(key);
223
+
224
+ request.onsuccess = () => resolve();
225
+ request.onerror = () => reject(request.error);
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Resolves a filtered list of records from a specific store.
231
+ */
232
+ async query(storeName, queryFn) {
233
+ const db = await this.open();
234
+ return new Promise((resolve, reject) => {
235
+ const tx = db.transaction(storeName, 'readonly');
236
+ const store = tx.objectStore(storeName);
237
+ const request = store.getAll();
238
+
239
+ request.onsuccess = () => {
240
+ const results = request.result || [];
241
+ const unwrapped = results.map((res) => {
242
+ if (res && typeof res === 'object' && 'value' in res && 'lastAccessed' in res) {
243
+ return res.value;
244
+ }
245
+ return res;
246
+ });
247
+ resolve(queryFn ? unwrapped.filter(queryFn) : unwrapped);
248
+ };
249
+ request.onerror = () => reject(request.error);
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Fetches browser disk space allocation parameters for this origin.
255
+ */
256
+ async estimate() {
257
+ if (typeof navigator !== 'undefined' && navigator.storage && navigator.storage.estimate) {
258
+ return navigator.storage.estimate();
259
+ }
260
+ return { quota: 0, usage: 0 };
261
+ }
262
+
263
+ /**
264
+ * Requests origin storage to be marked as persistent (exempt from browser eviction).
265
+ */
266
+ async persist() {
267
+ if (typeof navigator !== 'undefined' && navigator.storage && navigator.storage.persist) {
268
+ return navigator.storage.persist();
269
+ }
270
+ return false;
271
+ }
272
+
273
+ /**
274
+ * Checks whether the origin has persistent storage enabled.
275
+ */
276
+ async isPersisted() {
277
+ if (typeof navigator !== 'undefined' && navigator.storage && navigator.storage.persisted) {
278
+ return navigator.storage.persisted();
279
+ }
280
+ return false;
281
+ }
282
+ }
283
+
284
+ export const storage = new PlatformStorage();
@@ -0,0 +1,308 @@
1
+ /**
2
+ * src/core/state/store.js
3
+ *
4
+ * Proxy-based reactive state store.
5
+ * Tracks reactive read accesses (get) dynamically when an active subscriber
6
+ * context is present, and schedules microtask-batched triggers on writes.
7
+ * Supports manual batching (batch), serialization snapshots, and hydration.
8
+ *
9
+ * Source: doc 08 — State Management §4, §7, §8, §9
10
+ */
11
+
12
+ import { derived } from './derived.js';
13
+ import { sync } from './sync.js';
14
+
15
+ let activeSubscriber = null;
16
+
17
+ /**
18
+ * Sets the global active subscriber registry (used by reactive derived nodes).
19
+ */
20
+ export function setActiveSubscriber(subscriber) {
21
+ activeSubscriber = subscriber;
22
+ }
23
+
24
+ /**
25
+ * Returns the current active subscriber registry.
26
+ */
27
+ export function getActiveSubscriber() {
28
+ return activeSubscriber;
29
+ }
30
+
31
+ function fastClone(obj) {
32
+ if (obj === null || typeof obj !== 'object') return obj;
33
+ if (obj instanceof Date) return new Date(obj.getTime());
34
+ if (obj instanceof RegExp) return new RegExp(obj);
35
+ if (Array.isArray(obj)) {
36
+ return obj.map(fastClone);
37
+ }
38
+ if (Object.getPrototypeOf(obj) === Object.prototype) {
39
+ const copy = {};
40
+ for (const key in obj) {
41
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
42
+ copy[key] = fastClone(obj[key]);
43
+ }
44
+ }
45
+ return copy;
46
+ }
47
+ if (typeof structuredClone === 'function') {
48
+ return structuredClone(obj);
49
+ }
50
+ return JSON.parse(JSON.stringify(obj));
51
+ }
52
+
53
+ export class ReactiveStore {
54
+ #target;
55
+ #state;
56
+ #listeners = new Map(); // key -> Set<callback>
57
+ #batching = false;
58
+ #pendingNotifications = new Set();
59
+ #onMutationCallback = null;
60
+ #options;
61
+ #proxies;
62
+ #previousValues = new Map(); // Track previous values for subscribe callbacks
63
+
64
+ constructor(initial = {}, options = {}) {
65
+ const self = this;
66
+ this.#target = initial;
67
+ this.#options = options;
68
+ this.#proxies = new WeakMap();
69
+
70
+ this.#state = new Proxy(initial, {
71
+ get(target, key) {
72
+ self.#track(key);
73
+ const val = Reflect.get(target, key);
74
+ if (self.#options.deep && val !== null && typeof val === 'object' && !(val instanceof Date) && !(val instanceof RegExp)) {
75
+ return self.#createProxy(val, key);
76
+ }
77
+ return val;
78
+ },
79
+ set(target, key, value) {
80
+ const oldVal = Reflect.get(target, key);
81
+ if (Object.is(oldVal, value)) return true;
82
+ Reflect.set(target, key, value);
83
+ self.#trigger(key);
84
+ return true;
85
+ },
86
+ deleteProperty(target, key) {
87
+ if (Reflect.has(target, key)) {
88
+ Reflect.deleteProperty(target, key);
89
+ self.#trigger(key);
90
+ }
91
+ return true;
92
+ }
93
+ });
94
+ }
95
+
96
+ #createProxy(target, topLevelKey) {
97
+ if (target === null || typeof target !== 'object') return target;
98
+ if (target instanceof Date || target instanceof RegExp) return target;
99
+
100
+ if (this.#proxies.has(target)) {
101
+ return this.#proxies.get(target);
102
+ }
103
+
104
+ const self = this;
105
+ const proxy = new Proxy(target, {
106
+ get(t, key) {
107
+ if (key === '__raw__') return t;
108
+ self.#track(topLevelKey);
109
+ const val = Reflect.get(t, key);
110
+ if (val !== null && typeof val === 'object' && !(val instanceof Date) && !(val instanceof RegExp)) {
111
+ return self.#createProxy(val, topLevelKey);
112
+ }
113
+ return val;
114
+ },
115
+ set(t, key, val) {
116
+ const oldVal = Reflect.get(t, key);
117
+ if (Object.is(oldVal, val)) return true;
118
+ Reflect.set(t, key, val);
119
+ self.#trigger(topLevelKey);
120
+ if (self.#onMutationCallback) {
121
+ self.#onMutationCallback(topLevelKey, self.#state[topLevelKey], 'local');
122
+ }
123
+ return true;
124
+ },
125
+ deleteProperty(t, key) {
126
+ if (Reflect.has(t, key)) {
127
+ Reflect.deleteProperty(t, key);
128
+ self.#trigger(topLevelKey);
129
+ if (self.#onMutationCallback) {
130
+ self.#onMutationCallback(topLevelKey, self.#state[topLevelKey], 'local');
131
+ }
132
+ }
133
+ return true;
134
+ }
135
+ });
136
+
137
+ this.#proxies.set(target, proxy);
138
+ return proxy;
139
+ }
140
+
141
+ /**
142
+ * Retrieves a property value from the reactive state.
143
+ */
144
+ get(key) {
145
+ return this.#state[key];
146
+ }
147
+
148
+ /**
149
+ * Modifies a property value in the reactive state.
150
+ */
151
+ set(key, value, source = 'local') {
152
+ this.#previousValues.set(key, this.#state[key]);
153
+ this.#state[key] = value;
154
+ if (this.#onMutationCallback) {
155
+ this.#onMutationCallback(key, value, source);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Registers a callback to monitor all state mutations (used by BroadcastChannel sync).
161
+ */
162
+ onMutation(callback) {
163
+ this.#onMutationCallback = callback;
164
+ }
165
+
166
+ #track(key) {
167
+ if (activeSubscriber) {
168
+ activeSubscriber.add({ store: this, key });
169
+ }
170
+ }
171
+
172
+ #trigger(key) {
173
+ this.#pendingNotifications.add(key);
174
+ if (!this.#batching) {
175
+ this.#scheduleQueue();
176
+ }
177
+ }
178
+
179
+ #scheduleQueue() {
180
+ queueMicrotask(() => {
181
+ if (this.#pendingNotifications.size === 0) return;
182
+ const keys = [...this.#pendingNotifications];
183
+ this.#pendingNotifications.clear();
184
+
185
+ // Map each callback to one representative changed key so subscribers are
186
+ // notified once per flush while still receiving the current value + key.
187
+ const callbackKey = new Map();
188
+ for (const key of keys) {
189
+ const set = this.#listeners.get(key);
190
+ if (set) {
191
+ for (const cb of set) {
192
+ if (!callbackKey.has(cb)) callbackKey.set(cb, key);
193
+ }
194
+ }
195
+ }
196
+
197
+ for (const [cb, key] of callbackKey) {
198
+ try {
199
+ const current = this.get(key);
200
+ const previous = this.#previousValues.get(key);
201
+ cb(current, key, previous);
202
+ this.#previousValues.set(key, current);
203
+ } catch (err) {
204
+ console.error('Error executing store change subscription:', err);
205
+ }
206
+ }
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Subscribes to changes on a specific state key.
212
+ */
213
+ subscribe(key, callback, signal) {
214
+ if (signal?.aborted) return () => {};
215
+
216
+ if (!this.#listeners.has(key)) {
217
+ this.#listeners.set(key, new Set());
218
+ }
219
+ this.#listeners.get(key).add(callback);
220
+
221
+ const dispose = () => {
222
+ const set = this.#listeners.get(key);
223
+ if (set) {
224
+ set.delete(callback);
225
+ if (set.size === 0) {
226
+ this.#listeners.delete(key);
227
+ }
228
+ }
229
+ };
230
+
231
+ if (signal) {
232
+ signal.addEventListener('abort', dispose);
233
+ }
234
+
235
+ return dispose;
236
+ }
237
+
238
+ /**
239
+ * Batches multiple mutations atomically in a single microtask notification.
240
+ */
241
+ batch(fn) {
242
+ this.#batching = true;
243
+ try {
244
+ fn();
245
+ } finally {
246
+ this.#batching = false;
247
+ this.#scheduleQueue();
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Serializes a deep-cloned copy of the current store state.
253
+ */
254
+ snapshot() {
255
+ if (this.#options?.clone) {
256
+ return this.#options.clone(this.#target);
257
+ }
258
+ return fastClone(this.#target);
259
+ }
260
+
261
+ /**
262
+ * Re-hydrates the store state from a snapshot without firing redundant triggers.
263
+ */
264
+ hydrate(snapshot) {
265
+ this.batch(() => {
266
+ for (const [key, value] of Object.entries(snapshot)) {
267
+ this.#state[key] = value;
268
+ }
269
+ });
270
+ }
271
+
272
+ /**
273
+ * Cleans and resets the store with new initial values.
274
+ */
275
+ reset(initial = {}) {
276
+ this.batch(() => {
277
+ for (const key of Object.keys(this.#state)) {
278
+ delete this.#state[key];
279
+ }
280
+ for (const [key, value] of Object.entries(initial)) {
281
+ this.#state[key] = value;
282
+ }
283
+ });
284
+ }
285
+
286
+ /**
287
+ * Replicates designated store keys across active browser tabs.
288
+ */
289
+ broadcast(channelName, keys = []) {
290
+ return sync(this, keys, channelName);
291
+ }
292
+
293
+ /**
294
+ * Syncs designated store keys across active browser tabs (alias for broadcast).
295
+ */
296
+ sync(keys = [], channelName) {
297
+ return this.broadcast(channelName, keys);
298
+ }
299
+
300
+ /**
301
+ * Creates a reactive derived node linked to this store.
302
+ */
303
+ derived(keys, compute) {
304
+ const fn = typeof keys === 'function' ? keys : compute;
305
+ return derived(fn);
306
+ }
307
+ }
308
+
@@ -0,0 +1,46 @@
1
+ /**
2
+ * src/core/state/sync.js
3
+ *
4
+ * Cross-tab state synchronization.
5
+ * Leverages native BroadcastChannel to replicate designated state keys
6
+ * between active browser tabs while avoiding update cycles.
7
+ *
8
+ * Source: doc 08 — State Management §11
9
+ */
10
+
11
+ /**
12
+ * Synchronizes reactive store mutations across multiple active browser tabs.
13
+ */
14
+ export function sync(store, keys = [], channelName = 'platform-state-sync') {
15
+ if (typeof window === 'undefined' || typeof BroadcastChannel === 'undefined') {
16
+ return () => {};
17
+ }
18
+
19
+ const channel = new BroadcastChannel(channelName);
20
+
21
+ // Replicate state mutations to other tabs
22
+ store.onMutation((key, value, source) => {
23
+ if (source === 'local') {
24
+ // Sync only whitelisted keys (or all keys if whitelist is empty)
25
+ if (keys.length === 0 || keys.includes(key)) {
26
+ channel.postMessage({ key, value });
27
+ }
28
+ }
29
+ });
30
+
31
+ // Receive replication messages from other tabs
32
+ channel.onmessage = (event) => {
33
+ if (!event.data) return;
34
+ const { key, value } = event.data;
35
+
36
+ if (keys.length === 0 || keys.includes(key)) {
37
+ // Apply values with a 'broadcast' source tag to prevent circular replication echo
38
+ store.set(key, value, 'broadcast');
39
+ }
40
+ };
41
+
42
+ // Return a cleanup disposer function
43
+ return () => {
44
+ channel.close();
45
+ };
46
+ }