@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,100 @@
1
+ /**
2
+ * src/core/offline/clock.js
3
+ *
4
+ * Logical Lamport Clock Engine.
5
+ * Manages persistent client actor UUIDs and monotonic logical counters
6
+ * for conflict-free write replication.
7
+ *
8
+ * Source: doc 13 — Offline and Background §4
9
+ */
10
+
11
+ import { storage } from '../storage/index.js';
12
+ import { uuid } from '../security/index.js';
13
+
14
+ let currentClock = 0;
15
+ let currentActor = '';
16
+ let initialized = false;
17
+ let initPromise = null;
18
+
19
+ /**
20
+ * Initializes the logical clock engine.
21
+ */
22
+ export async function init() {
23
+ if (initialized) return;
24
+ if (initPromise) return initPromise;
25
+
26
+ initPromise = (async () => {
27
+ try {
28
+ let actorVal = await storage.get('offline:actor', 'idb');
29
+ if (!actorVal) {
30
+ actorVal = uuid();
31
+ await storage.set('offline:actor', actorVal, 'idb');
32
+ }
33
+ currentActor = actorVal;
34
+
35
+ const clockVal = await storage.get('offline:clock', 'idb');
36
+ if (typeof clockVal === 'number') {
37
+ currentClock = clockVal;
38
+ } else {
39
+ currentClock = 0;
40
+ await storage.set('offline:clock', 0, 'idb');
41
+ }
42
+ initialized = true;
43
+ } catch (err) {
44
+ console.error('Failed to initialize logical clock:', err);
45
+ if (!currentActor) currentActor = uuid();
46
+ } finally {
47
+ initPromise = null;
48
+ }
49
+ })();
50
+
51
+ return initPromise;
52
+ }
53
+
54
+ // Auto-boot init
55
+ if (typeof window !== 'undefined') {
56
+ Promise.resolve().then(() => init().catch(console.error));
57
+ }
58
+
59
+ /**
60
+ * Returns the persistent actor UUID for this client.
61
+ */
62
+ export async function actor() {
63
+ await init();
64
+ return currentActor;
65
+ }
66
+
67
+ /**
68
+ * Increments and returns the logical clock count.
69
+ */
70
+ export async function tick() {
71
+ await init();
72
+ currentClock++;
73
+ storage.set('offline:clock', currentClock, 'idb').catch((err) => {
74
+ console.warn('Failed to persist logical clock:', err);
75
+ });
76
+ return currentClock;
77
+ }
78
+
79
+ /**
80
+ * Synchronizes the local clock against a remote clock.
81
+ */
82
+ export async function sync(remoteTime) {
83
+ await init();
84
+ if (remoteTime > currentClock) {
85
+ currentClock = remoteTime + 1;
86
+ storage.set('offline:clock', currentClock, 'idb').catch((err) => {
87
+ console.warn('Failed to persist synchronized logical clock:', err);
88
+ });
89
+ }
90
+ return currentClock;
91
+ }
92
+
93
+ /**
94
+ * Generates a logical clock stamp.
95
+ */
96
+ export async function stamp() {
97
+ const act = await actor();
98
+ const clk = await tick();
99
+ return { actor: act, clock: clk };
100
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * src/core/offline/connectivity.js
3
+ *
4
+ * Resilient Connectivity Monitor.
5
+ * Extends the unreliable navigator.onLine by performing debounced, rate-limited
6
+ * HTTP HEAD probes to verify genuine external internet access.
7
+ *
8
+ * Source: doc 13 — Offline and Background §4
9
+ */
10
+
11
+ import { state } from './state.js';
12
+ import { bus } from '../events/bus.js';
13
+ import { names } from '../events/types/index.js';
14
+
15
+ let lastCheck = 0;
16
+ let isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
17
+ let inFlightProbe = null;
18
+
19
+ const listeners = new Set();
20
+
21
+ /**
22
+ * Triggers a network connectivity check. Shares in-flight requests and throttles checks
23
+ * to no more than once per 10 seconds.
24
+ */
25
+ export async function check(force = false) {
26
+ if (typeof window === 'undefined') return true;
27
+
28
+ const now = Date.now();
29
+ // Return cached state if requested within the 10-second limit and not forced
30
+ if (!force && now - lastCheck < 10000) {
31
+ return isOnline;
32
+ }
33
+
34
+ if (inFlightProbe) return inFlightProbe;
35
+
36
+ inFlightProbe = (async () => {
37
+ lastCheck = now;
38
+ try {
39
+ // Dispatch HEAD request with cache-busting parameter to verify external access
40
+ await fetch('/favicon.ico?_probe=' + now, {
41
+ method: 'HEAD',
42
+ cache: 'no-store',
43
+ mode: 'no-cors' // Prevents CORS blockages
44
+ });
45
+ isOnline = true;
46
+ } catch (err) {
47
+ // Network failures or offline state trigger failure
48
+ isOnline = false;
49
+ } finally {
50
+ inFlightProbe = null;
51
+ // Sync reactive state
52
+ state.set('online', isOnline);
53
+ state.set('status', isOnline ? 'online' : 'offline');
54
+
55
+ // Emit standardized connectivity event on the global bus
56
+ bus.emit(isOnline ? names.connectivity.online : names.connectivity.offline, { online: isOnline });
57
+
58
+ // Broadcast state to all active subscribers
59
+ for (const listener of listeners) {
60
+ try {
61
+ listener(isOnline);
62
+ } catch (err) {
63
+ console.error('Error in connectivity listener:', err);
64
+ }
65
+ }
66
+ }
67
+ return isOnline;
68
+ })();
69
+
70
+ return inFlightProbe;
71
+ }
72
+
73
+ /**
74
+ * Subscribes to connectivity changes.
75
+ */
76
+ export function subscribe(fn, signal) {
77
+ if (signal?.aborted) return () => {};
78
+
79
+ listeners.add(fn);
80
+
81
+ // Eagerly feed current status to the subscriber
82
+ fn(isOnline);
83
+
84
+ const dispose = () => {
85
+ listeners.delete(fn);
86
+ if (signal) {
87
+ signal.removeEventListener('abort', dispose);
88
+ }
89
+ };
90
+
91
+ if (signal) {
92
+ signal.addEventListener('abort', dispose, { once: true });
93
+ }
94
+
95
+ return dispose;
96
+ }
97
+
98
+ // Bind native browser listeners to trigger immediate HEAD probe checks
99
+ if (typeof window !== 'undefined') {
100
+ window.addEventListener('online', () => check(true));
101
+ window.addEventListener('offline', () => {
102
+ isOnline = false;
103
+ state.set('online', false);
104
+ state.set('status', 'offline');
105
+ bus.emit(names.connectivity.offline, { online: false });
106
+ for (const listener of listeners) {
107
+ try {
108
+ listener(false);
109
+ } catch (err) {
110
+ console.error('Error in connectivity offline listener:', err);
111
+ }
112
+ }
113
+ });
114
+ }
115
+ export { isOnline };
116
+
@@ -0,0 +1,41 @@
1
+ /**
2
+ * src/core/offline/index.js
3
+ *
4
+ * Public offline base entry point.
5
+ * Combines Service Worker messaging, IndexedDB task queuing, Background Sync execution,
6
+ * and reliable HEAD-probe connectivity monitors.
7
+ *
8
+ * Source: doc 13 — Offline and Background §1, §3
9
+ */
10
+
11
+ import { check, subscribe } from './connectivity.js';
12
+ import { queue } from './queue.js';
13
+ import { sync } from './sync.js';
14
+ import { bridge } from './bridge.js';
15
+ import { state } from './state.js';
16
+ import { actor, tick, sync as clockSync, stamp } from './clock.js';
17
+
18
+ export const clock = {
19
+ actor,
20
+ tick,
21
+ sync: clockSync,
22
+ stamp
23
+ };
24
+
25
+ export const offline = {
26
+ check,
27
+ subscribe,
28
+ queue,
29
+ sync,
30
+ state,
31
+ clock,
32
+ /**
33
+ * Shorthand to send messages directly to the active Service Worker bridge.
34
+ */
35
+ send(task, payload) {
36
+ return bridge.send(task, payload);
37
+ }
38
+ };
39
+
40
+ export { check, subscribe, queue, sync, bridge, state };
41
+
@@ -0,0 +1,89 @@
1
+ # Offline Missing Support
2
+
3
+ This document tracks remaining support, runtime gaps, concurrency constraints, serialization enhancements, and state/storage synchronization work for `src/core/offline`. Implemented behavior belongs in `usage.md` (or core documentation); unsupported, under-tested, or performance-gated behavior belongs here until it is built and verified.
4
+
5
+ ---
6
+
7
+ ## 0. Toolchain and Naming Rules
8
+
9
+ Heavy compilation, static analysis, and code generation belong in `tools/`, not in browser runtime modules.
10
+
11
+ Current toolchain offline support requirements:
12
+
13
+ - Statically parse and validate serialized HTTP request bodies.
14
+ - Enforce idempotency key annotations on form submissions at build time.
15
+
16
+ Naming rules for offline support:
17
+
18
+ - Prefer one-word files: `connectivity.js`, `queue.js`, `sync.js`, `bridge.js`, `clock.js`, `state.js`, `index.js`.
19
+ - Plural folders: `tests/`, `migrations/`.
20
+ - Single-word methods: `check`, `subscribe`, `push`, `list`, `update`, `delete`, `clear`, `register`, `send`, `tick`.
21
+ - Single-word properties in state: `online`, `status`, `pending`, `actor`, `clock`.
22
+ - Avoid compound names where single words carry full meaning in context.
23
+
24
+ ---
25
+
26
+ ## 1. State and Storage Synchronization Gaps
27
+
28
+ ### 1.1. Reactive State Integration
29
+
30
+ - **Status**: Runtime gap.
31
+ - **Files**:
32
+ - `src/core/offline/connectivity.js`
33
+ - `src/core/offline/queue.js`
34
+ - `src/core/offline/state.js` [NEW]
35
+ - `src/core/offline/index.js`
36
+ - **Problem**:
37
+ The connectivity monitor evaluates the network state via HEAD probes but does not expose it reactively using the `core.state` system. The offline queue manages deferred tasks inside IndexedDB but does not publish queue metrics (such as the number of pending tasks). Callers cannot bind UI elements (like sync indicators or queue counters) directly to the reactive state layer.
38
+ - **Expected Support**:
39
+ - Introduce `src/core/offline/state.js` exposing a reactive `state` store containing:
40
+ - `online`: boolean indicating connectivity.
41
+ - `status`: `'online' | 'offline' | 'unknown'` representing current reachability.
42
+ - `pending`: number of elements currently in the sync queue.
43
+ - Automatically update `online` and `status` in the state store whenever connectivity changes.
44
+ - Automatically update `pending` in the state store whenever items are pushed, updated, deleted, or cleared in the queue.
45
+
46
+ ### 1.2. Write-Ahead Journaling and Storage Durability
47
+
48
+ - **Status**: Durability gap.
49
+ - **Files**:
50
+ - `src/core/offline/queue.js`
51
+ - **Problem**:
52
+ The offline queue writes directly to a raw IndexedDB connection (`new Database(...)` from `../storage/idb.js`). It bypasses the crash-durable journaling and quota warnings implemented in the unified `storage` gateway. Under sudden tab closure or power failure, queued tasks may fail to commit or corrupt database state.
53
+ - **Expected Support**:
54
+ - Integrate `OfflineQueue` with `localStorage`-based write-ahead journaling for queue writes.
55
+ - Automatically recover/replay uncommitted journal records during offline engine initialization.
56
+ - Listen to `onQuotaWarning` subscriptions to prevent queue pushes and trigger warning events when client storage usage exceeds 80%.
57
+
58
+ ---
59
+
60
+ ## 2. Concurrency and Fallback Coordination Gaps
61
+
62
+ ### 2.1. Web Locks for Cross-Tab Fallback Coordination
63
+
64
+ - **Status**: Concurrency gap.
65
+ - **Files**:
66
+ - `src/core/offline/sync.js`
67
+ - **Problem**:
68
+ On browsers without native Background Sync support (Firefox, Safari), the manual fallback listens for `online` transitions and triggers queue replay via custom event dispatch. In a multi-tab session, all open tabs will simultaneously receive the event and execute `replayQueue()`. This causes redundant HTTP requests, database write conflicts, and out-of-order network replay.
69
+ - **Expected Support**:
70
+ - Coordinate manual sync replay across tabs using an exclusive Web Lock (`navigator.locks.request`) named `"offline:sync"`.
71
+ - Only the tab that successfully acquires the lock executes the replay loop, preventing concurrent replay attempts.
72
+
73
+ ---
74
+
75
+ ## 3. Logical Clocks and Conflict Resolution Gaps
76
+
77
+ ### 3.1. Lamport Logical Clocks Engine
78
+
79
+ - **Status**: Architectural gap.
80
+ - **Files**:
81
+ - `src/core/offline/clock.js` [NEW]
82
+ - `src/core/offline/index.js`
83
+ - **Problem**:
84
+ To support reliable offline write replication, every queued operation must be stamped with a tuple of `(actor_id, lamport_timestamp, sequence_number)`. This ensures Last-Write-Wins (LWW) and CRDT merges resolve deterministically regardless of wall-clock drift. Currently, there is no implementation of actor registration or Lamport clock management.
85
+ - **Expected Support**:
86
+ - Implement a logical Lamport clock engine in `clock.js` exporting:
87
+ - `actor`: a persistent UUID identifying the current client, stored in unified storage.
88
+ - `tick()`: increments and returns the local logical clock count.
89
+ - `sync(remoteTime)`: advances the local logical clock count to be greater than any incoming server/peer clock.
@@ -0,0 +1,143 @@
1
+ # Offline & Background Capabilities Architecture Plan
2
+
3
+ This document outlines the design, implementation, and optimization specifications for the resilient Offline and Background Capabilities engine under `core.offline` and the Service Worker runtime in the `@adukiorg/anza` library.
4
+
5
+ ---
6
+
7
+ ## 1. Architectural Strategy & Core Requirements
8
+
9
+ Our offline-first architecture rests on the principle that **network absence is a normal mode of operation, not an error state**. The architecture decouples the initiation of an operation from its network execution, guaranteeing consistency, durability, and a highly responsive user experience under any connectivity condition.
10
+
11
+ ### Key Pillars
12
+
13
+ 1. **HEAD-Probe Connectivity Monitor (`connectivity.js`):** Resilient monitoring extending browser `navigator.onLine` with debounced, throttled (10s) HEAD probes using cache-busting headers. Support for memory-safe `subscribe` patterns utilizing the dual-cleanup pattern for `AbortSignal` listeners.
14
+ 2. **IndexedDB Tasks Journal (`queue.js`):** Chronicled persistent queue leveraging IndexedDB to buffer and serialize background tasks when offline. We will enhance the deserialization module to gracefully stringify plain object payloads when `Content-Type: application/json` is specified, resolving a critical integration mismatch.
15
+ 3. **Background Sync & Fallback (`sync.js`):** Native `SyncManager` registrations for Chromium-based browsers, paired with immediate event-driven fallbacks on `online` and custom triggers for Firefox and Safari. Memory-safe `onSyncFallback` listener registrations.
16
+ 4. **Service Worker Message Bridge (`bridge.js`):** High-concurrency message corridors utilizing native `MessageChannel` for direct request/response dispatching to the active Service Worker controller.
17
+ 5. **FIFO Replay Loop & Dead-Letter Management (`src/sw/sync.js`):** Chronological, sequential task processing with transaction safety, automatic retry limits (max 5), Dead-Letter Queue (DLQ) transitions, and tab-wide success/failure broadcasts.
18
+ 6. **Conflict Resolution Architecture:** Standardized conflict resolution guidelines using logical Lamport timestamps `(actor_id, lamport_timestamp, sequence_number)` instead of physical wall clocks, supporting Last-Write-Wins (LWW) registers and Grow-Only (G-Set) or Observed-Remove (OR-Set) CRDT configurations.
19
+
20
+ ---
21
+
22
+ ## 2. Component Blueprint & Files Layout
23
+
24
+ All files inside the offline module strictly adhere to the `RULE[user_global]` lowercase, single-word naming structure:
25
+
26
+ ```
27
+ src/core/offline/
28
+ ├── index.js # Public offline entry point & unified facade
29
+ ├── connectivity.js # Resilient connectivity check and listener subscriptions
30
+ ├── queue.js # Persistent IndexedDB offline task journal
31
+ ├── sync.js # Chromium Background Sync manager & Safari/Firefox online listeners
32
+ ├── bridge.js # Bidirectional MessageChannel Service Worker corridor
33
+ └── plan.md # This planning document
34
+ ```
35
+
36
+ ---
37
+
38
+ ## 3. Detailed Component Designs
39
+
40
+ ### 3.1. Resilient Connectivity Prober (`connectivity.js`)
41
+
42
+ * **HEAD Probe with Cache-Busting:** Performs a rate-limited `fetch` request using the `HEAD` method to the local favicon or a health-check path (`/favicon.ico?_probe=Date.now()`) with `mode: 'no-cors'` and `cache: 'no-store'`.
43
+ * **Shared In-Flight Promise:** Share single active in-flight check promise to prevent redundant parallel network requests.
44
+ * **Leak-Free Dual Abort Cleanup:**
45
+
46
+ ```javascript
47
+ export function subscribe(fn, signal) {
48
+ if (signal?.aborted) return () => {};
49
+ listeners.add(fn);
50
+ fn(isOnline);
51
+
52
+ const dispose = () => {
53
+ listeners.delete(fn);
54
+ if (signal) {
55
+ signal.removeEventListener('abort', dispose);
56
+ }
57
+ };
58
+
59
+ if (signal) {
60
+ signal.addEventListener('abort', dispose, { once: true });
61
+ }
62
+
63
+ return dispose;
64
+ }
65
+ ```
66
+
67
+ ### 3.2. Idempotent Offline Queue (`queue.js`)
68
+
69
+ * **IndexedDB-Backed Buffer:** Uses the `Database` client from `../storage/idb.js` under the store `tasks` inside `platform-offline-queue`.
70
+ * **JSON Body Deserialization Fix:** In `src/sw/queue.js` (used during SW replay), if the serialized request `body` is not a string, Blob, or ArrayBuffer (e.g. is a plain object written by `ui-form`), it must be stringified if the header is `application/json`.
71
+
72
+ ```javascript
73
+ export function deserializeRequest(serialized) {
74
+ const options = {
75
+ method: serialized.method,
76
+ headers: new Headers(serialized.headers)
77
+ };
78
+
79
+ if (serialized.body) {
80
+ const isJson = options.headers.get('content-type')?.includes('application/json');
81
+ if (isJson && typeof serialized.body === 'object' && !(serialized.body instanceof ArrayBuffer) && !(serialized.body instanceof Blob)) {
82
+ options.body = JSON.stringify(serialized.body);
83
+ } else {
84
+ options.body = serialized.body;
85
+ }
86
+ }
87
+
88
+ return new Request(serialized.url, options);
89
+ }
90
+ ```
91
+
92
+ ### 3.3. Background Sync & Fallback Coordinator (`sync.js`)
93
+
94
+ * **Sync API Registration:** Enqueues a sync tag (e.g., `'pending'`) with the browser's Background Sync API if supported.
95
+ * **Browser-Safe Event Fallbacks:** Hooks into the window's `online` state on Safari/Firefox and dispatches a custom event (`core:sync-fallback`) detailed with the pending tag.
96
+ * **Leak-Free `onSyncFallback`:** Applies the dual-cleanup unsubscription pattern for the `core:sync-fallback` abort listener to prevent memory accumulation in long-lived client tabs.
97
+
98
+ ### 3.4. Service Worker Messaging Corridor (`bridge.js`)
99
+
100
+ * **Response Channel Routing:** Utilizes transferable `MessageChannel` ports to match worker-side responses back to origin promises, avoiding race conditions in concurrent request dispatching.
101
+ * **State Check:** Gracefully rejects with informative errors if the browser does not support Service Workers or if `navigator.serviceWorker.controller` is absent.
102
+
103
+ ---
104
+
105
+ ## 4. Conflict Resolution & Idempotency Guidelines
106
+
107
+ To resolve concurrent offline edits reliably:
108
+
109
+ 1. **Logical Clocks (Lamport):** Every offline operation writes a tuple `(actor_id, lamport_timestamp, sequence_number)`.
110
+ * `actor_id`: Persisted UUID of the current device.
111
+ * `lamport_timestamp`: Monotonically increasing logical count that advances locally and synchronizes on remote data delivery.
112
+ 2. **Merge Semantics:**
113
+ * **LWW-Register:** Scalar fields use Last-Write-Wins based on Lamport comparisons (lexicographical actor ID as a final tiebreaker).
114
+ * **G-Set & OR-Set:** Collections (e.g. logs, attachment list mutations) use CRDT merge rules to ensure complete commutative, associative, and idempotent finality.
115
+ 3. **Idempotency Keys:** Every queued task has an `idempotencyKey` UUID. The replay engine re-submits this key to the server which returns cached responses for duplicates, securing absolute safety across multiple retries.
116
+
117
+ ---
118
+
119
+ ## 5. Verification & Testing Strategy
120
+
121
+ ### 5.1. Automated Unit Testing (`tests/core/offline/`)
122
+
123
+ We will create a comprehensive suite of unit tests validating the offline capabilities:
124
+
125
+ 1. **`connectivity.test.js`**:
126
+ * Verify rate-limiting and cached status returns within the 10-second window.
127
+ * Assert `subscribe` triggers immediate callback feed.
128
+ * Assert that manual disposal and signal abort both clean up correctly without memory leaks.
129
+ 2. **`queue.test.js`**:
130
+ * Assert FIFO ordering on `list()`.
131
+ * Assert push and delete operations execute successfully on IndexedDB.
132
+ * Verify that `deserializeRequest` successfully stringifies plain object JSON payloads.
133
+ 3. **`sync.test.js`**:
134
+ * Verify Background Sync registers successfully if supported.
135
+ * Assert that Safari/Firefox manual online trigger successfully dispatches the `core:sync-fallback` custom event.
136
+ * Assert `onSyncFallback` correctly registers listeners and cleans up its abort hooks seamlessly.
137
+ 4. **`bridge.test.js`**:
138
+ * Assert that bridge messages throw when controller is absent.
139
+
140
+ ### 5.2. Manual & Network Validation
141
+
142
+ * Simulating offline states via DevTools and verifying that submissions to `<ui-form>` with the `offline` attribute are correctly buffered inside the IndexedDB queue.
143
+ * Verifying that as soon as the network state recovers, the replayer is triggered and sequentially processes tasks, broadcasting `sync-success` events back to the UI element.
@@ -0,0 +1,168 @@
1
+ /**
2
+ * src/core/offline/queue.js
3
+ *
4
+ * Offline Operations Journal.
5
+ * Manages an IndexedDB-backed task queue to serialize, buffer, and track background
6
+ * tasks when offline, enforcing idempotency keys and retry limits.
7
+ *
8
+ * Source: doc 13 — Offline and Background §5
9
+ */
10
+
11
+ import { Database } from '../storage/idb.js';
12
+ import { state } from './state.js';
13
+ import { quota } from '../storage/quota.js';
14
+ import { uuid } from '../security/index.js';
15
+
16
+ const db = new Database('platform-offline-queue', 1, [
17
+ (dbInstance) => {
18
+ dbInstance.createObjectStore('tasks');
19
+ }
20
+ ]);
21
+
22
+ // Replay uncommitted tasks from localStorage journal
23
+ async function replayJournal() {
24
+ if (typeof localStorage === 'undefined') return;
25
+ const keysToRemove = [];
26
+ for (let i = 0; i < localStorage.length; i++) {
27
+ const k = localStorage.key(i);
28
+ if (k && k.startsWith('offline:journal:')) {
29
+ try {
30
+ const raw = localStorage.getItem(k);
31
+ if (raw) {
32
+ const item = JSON.parse(raw);
33
+ console.log(`Replaying offline queue journal task: ${item.id}`);
34
+ await db.set('tasks', item.id, item);
35
+ }
36
+ } catch (err) {
37
+ console.error(`Failed to replay offline journal entry ${k}:`, err);
38
+ }
39
+ keysToRemove.push(k);
40
+ }
41
+ }
42
+ for (const k of keysToRemove) {
43
+ localStorage.removeItem(k);
44
+ }
45
+ }
46
+
47
+ // Update the reactive state pending task count
48
+ async function syncPending() {
49
+ try {
50
+ const list = await db.getAll('tasks');
51
+ state.set('pending', list.length);
52
+ } catch (err) {
53
+ console.error('Failed to sync pending queue count:', err);
54
+ }
55
+ }
56
+
57
+ // Boot setup
58
+ if (typeof window !== 'undefined') {
59
+ Promise.resolve().then(async () => {
60
+ try {
61
+ await db.open();
62
+ await replayJournal();
63
+ await syncPending();
64
+ } catch (err) {
65
+ console.error('Failed to initialize offline queue boot setup:', err);
66
+ }
67
+ });
68
+ }
69
+
70
+ export class OfflineQueue {
71
+ /**
72
+ * Enqueues an offline task with idempotency controls.
73
+ */
74
+ async push(taskName, payload = null, options = {}) {
75
+ // Proactive quota check; usage > 80% blocks enqueuing
76
+ const isExceeded = await quota.check();
77
+ if (isExceeded) {
78
+ throw new DOMException('Storage quota limit exceeded (usage > 80%)', 'QuotaExceededError');
79
+ }
80
+
81
+ await db.open();
82
+ const id = options.idempotencyKey || uuid();
83
+
84
+ const item = {
85
+ id,
86
+ task: taskName,
87
+ payload,
88
+ timestamp: Date.now(),
89
+ retries: 0,
90
+ maxRetries: options.maxRetries ?? 5
91
+ };
92
+
93
+ const journalKey = `offline:journal:${id}`;
94
+ if (typeof localStorage !== 'undefined') {
95
+ try {
96
+ localStorage.setItem(journalKey, JSON.stringify(item));
97
+ } catch (err) {
98
+ console.warn('Failed to write offline journal:', err);
99
+ }
100
+ }
101
+
102
+ try {
103
+ await db.set('tasks', id, item);
104
+ await syncPending();
105
+ } finally {
106
+ if (typeof localStorage !== 'undefined') {
107
+ localStorage.removeItem(journalKey);
108
+ }
109
+ }
110
+
111
+ return id;
112
+ }
113
+
114
+ /**
115
+ * Retrieves all items currently stored in the queue.
116
+ */
117
+ async list() {
118
+ await db.open();
119
+ const list = await db.getAll('tasks');
120
+ // Sort oldest tasks first for chronological processing
121
+ return list.sort((a, b) => a.timestamp - b.timestamp);
122
+ }
123
+
124
+ /**
125
+ * Updates a task state in the queue.
126
+ */
127
+ async update(task) {
128
+ await db.open();
129
+ const journalKey = `offline:journal:${task.id}`;
130
+ if (typeof localStorage !== 'undefined') {
131
+ try {
132
+ localStorage.setItem(journalKey, JSON.stringify(task));
133
+ } catch (err) {
134
+ console.warn('Failed to write offline journal on update:', err);
135
+ }
136
+ }
137
+
138
+ try {
139
+ await db.set('tasks', task.id, task);
140
+ await syncPending();
141
+ } finally {
142
+ if (typeof localStorage !== 'undefined') {
143
+ localStorage.removeItem(journalKey);
144
+ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Evicts a resolved task from the queue.
150
+ */
151
+ async delete(id) {
152
+ await db.open();
153
+ await db.delete('tasks', id);
154
+ await syncPending();
155
+ }
156
+
157
+ /**
158
+ * Fully clears the queue.
159
+ */
160
+ async clear() {
161
+ await db.open();
162
+ await db.clear('tasks');
163
+ await syncPending();
164
+ }
165
+ }
166
+
167
+ export const queue = new OfflineQueue();
168
+
@@ -0,0 +1,18 @@
1
+ /**
2
+ * src/core/offline/state.js
3
+ *
4
+ * Reactive state store for connectivity and queue metrics.
5
+ *
6
+ * Source: doc 08 — State Management, doc 13 — Offline and Background
7
+ */
8
+
9
+ import { ReactiveStore } from '../state/store.js';
10
+
11
+ const isBrowser = typeof navigator !== 'undefined';
12
+ const initOnline = isBrowser ? navigator.onLine : true;
13
+
14
+ export const state = new ReactiveStore({
15
+ online: initOnline,
16
+ status: initOnline ? 'online' : 'offline',
17
+ pending: 0
18
+ });