@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,75 @@
1
+ /**
2
+ * src/core/api/events/index.js
3
+ *
4
+ * Performant telemetry event emitter for all API network calls.
5
+ * Triggers hooks on timeouts, failures, specific HTTP status codes, or response content types.
6
+ *
7
+ * Source: doc 11 — Networking §2, core/api/plan.md
8
+ */
9
+
10
+ export class ApiEventEmitter {
11
+ #listeners = new Map();
12
+
13
+ /**
14
+ * Subscribes to a specific network or status event.
15
+ * Supports lifecycle-gated cleaning using AbortSignal.
16
+ *
17
+ * @param {string} event
18
+ * @param {Function} handler
19
+ * @param {AbortSignal} [signal]
20
+ * @returns {Function} Disposer
21
+ */
22
+ on(event, handler, signal) {
23
+ if (signal?.aborted) return () => {};
24
+
25
+ if (!this.#listeners.has(event)) {
26
+ this.#listeners.set(event, new Set());
27
+ }
28
+
29
+ const listener = { handler };
30
+ this.#listeners.get(event).add(listener);
31
+
32
+ let disposed = false;
33
+ const dispose = () => {
34
+ if (disposed) return;
35
+ disposed = true;
36
+
37
+ const set = this.#listeners.get(event);
38
+ if (set) {
39
+ set.delete(listener);
40
+ if (set.size === 0) {
41
+ this.#listeners.delete(event);
42
+ }
43
+ }
44
+ };
45
+
46
+ if (signal) {
47
+ signal.addEventListener('abort', dispose, { once: true });
48
+ }
49
+
50
+ return dispose;
51
+ }
52
+
53
+ /**
54
+ * Emits a telemetry event with a detail payload.
55
+ *
56
+ * @param {string} event
57
+ * @param {any} detail
58
+ */
59
+ emit(event, detail) {
60
+ const set = this.#listeners.get(event);
61
+ if (!set) return;
62
+
63
+ const apiEvent = { type: event, detail };
64
+
65
+ for (const listener of [...set]) {
66
+ try {
67
+ listener.handler(apiEvent);
68
+ } catch (err) {
69
+ console.error(`Error in API event listener for "${event}":`, err);
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ export const events = new ApiEventEmitter();
@@ -0,0 +1,99 @@
1
+ /**
2
+ * src/core/api/fetch.js
3
+ *
4
+ * Core fetch wrapper with AbortSignal, timeouts, and browser task priorities.
5
+ * Maps network and HTTP responses to a standard library-wide error shape.
6
+ *
7
+ * Source: doc 11 — Networking §3, §4, §9
8
+ */
9
+
10
+ export class PlatformError extends Error {
11
+ constructor({ code, message, cause, context, recoverable = true }) {
12
+ super(message);
13
+ this.name = 'PlatformError';
14
+ this.code = code;
15
+ this.cause = cause;
16
+ this.context = context || {};
17
+ this.recoverable = recoverable;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Executes a network fetch request with standard platform enhancements.
23
+ */
24
+ export async function execute(descriptor) {
25
+ const {
26
+ url,
27
+ timeout = 10000,
28
+ priority = 'user-visible',
29
+ signal,
30
+ ...fetchOpts
31
+ } = descriptor;
32
+
33
+ const controller = new AbortController();
34
+ const timeoutId = setTimeout(() => controller.abort('timeout'), timeout);
35
+
36
+ // Compose signals cleanly (leveraging AbortSignal.any where supported)
37
+ let activeSignal = controller.signal;
38
+ if (signal) {
39
+ if (typeof AbortSignal.any === 'function') {
40
+ activeSignal = AbortSignal.any([controller.signal, signal]);
41
+ } else {
42
+ if (signal.aborted) {
43
+ controller.abort(signal.reason);
44
+ } else {
45
+ signal.addEventListener('abort', () => {
46
+ controller.abort(signal.reason || 'aborted');
47
+ });
48
+ }
49
+ }
50
+ }
51
+
52
+ const runFetch = async () => {
53
+ try {
54
+ const response = await fetch(url, { ...fetchOpts, signal: activeSignal });
55
+ clearTimeout(timeoutId);
56
+
57
+ if (!response.ok) {
58
+ throw new PlatformError({
59
+ code: 'HTTP_ERROR',
60
+ message: `HTTP error ${response.status}: ${response.statusText}`,
61
+ context: { url, status: response.status, method: fetchOpts.method || 'GET' },
62
+ recoverable: response.status >= 500
63
+ });
64
+ }
65
+
66
+ return response;
67
+ } catch (err) {
68
+ clearTimeout(timeoutId);
69
+
70
+ if (err instanceof PlatformError) throw err;
71
+
72
+ const isTimeout = activeSignal.aborted && controller.signal.aborted;
73
+ throw new PlatformError({
74
+ code: isTimeout ? 'NETWORK_TIMEOUT' : 'NETWORK_ERROR',
75
+ message: err.message || (isTimeout ? 'Network request timed out' : 'Network request failed'),
76
+ cause: err,
77
+ context: { url, method: fetchOpts.method || 'GET' },
78
+ recoverable: true
79
+ });
80
+ }
81
+ };
82
+
83
+ // Run in browser task scheduler if supported (improves Interaction to Next Paint)
84
+ if (typeof globalThis.scheduler !== 'undefined' && typeof scheduler.postTask === 'function') {
85
+ return scheduler.postTask(runFetch, { priority, signal: activeSignal }).catch((err) => {
86
+ if (err instanceof PlatformError) throw err;
87
+ const isTimeout = activeSignal.aborted && controller.signal.aborted;
88
+ throw new PlatformError({
89
+ code: isTimeout ? 'NETWORK_TIMEOUT' : 'NETWORK_ERROR',
90
+ message: err.message || (isTimeout ? 'Network request timed out' : 'Network request failed'),
91
+ cause: err,
92
+ context: { url, method: fetchOpts.method || 'GET' },
93
+ recoverable: true
94
+ });
95
+ });
96
+ }
97
+
98
+ return runFetch();
99
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * src/core/api/index.js
3
+ *
4
+ * Public networking layer entry point.
5
+ * Composes request mutators through the outbound/inbound pipeline,
6
+ * applying caching strategies, transient error retries, and standard timeout controls.
7
+ *
8
+ * Source: doc 11 — Networking §2, §4
9
+ */
10
+
11
+ import { pipeline } from './pipeline.js';
12
+ import { execute, PlatformError } from './fetch.js';
13
+ import { retry } from './retry.js';
14
+ import { handle as handleCache } from './cache.js';
15
+ import { stream, createNDJSONTransform } from './stream.js';
16
+ import { upload } from './upload.js';
17
+ import { prefixes } from './prefixes/index.js';
18
+ import { events } from './events/index.js';
19
+ import { cache as apiCache } from './caches/index.js';
20
+
21
+ // Register global telemetry inbound interceptor to emit requests status/errors events
22
+ pipeline.inbound((responseOrError) => {
23
+ const requestId = responseOrError?.requestId;
24
+
25
+ if (responseOrError instanceof Error) {
26
+ const err = responseOrError;
27
+ const isTimeout = err.code === 'NETWORK_TIMEOUT';
28
+ if (isTimeout) {
29
+ events.emit('timeout', { error: err, requestId });
30
+ }
31
+ events.emit('error', { error: err, requestId });
32
+ events.emit('failed', { error: err, requestId });
33
+ } else {
34
+ const response = responseOrError;
35
+ const status = response.status;
36
+
37
+ events.emit(`status:${status}`, { response, requestId });
38
+
39
+ if (!response.ok) {
40
+ events.emit('failed', { response, requestId });
41
+ events.emit('error', { response, requestId });
42
+ } else {
43
+ const contentType = response.headers.get('Content-Type') || '';
44
+ if (contentType.includes('application/json')) {
45
+ events.emit('type:json', { response, requestId });
46
+ } else if (contentType.includes('text/event-stream')) {
47
+ events.emit('type:stream', { response, requestId });
48
+ } else if (contentType.includes('text/')) {
49
+ events.emit('type:text', { response, requestId });
50
+ }
51
+ }
52
+ }
53
+ return responseOrError;
54
+ });
55
+
56
+ /**
57
+ * Normalizes options and routes the request descriptor through the core pipeline.
58
+ */
59
+ async function request(url, method, body, opts = {}) {
60
+ const resolvedUrl = prefixes.resolve(url);
61
+ const headers = new Headers(opts.headers || {});
62
+
63
+ // Auto-serialize JSON bodies
64
+ let parsedBody = body;
65
+ if (body && typeof body === 'object' && !(body instanceof Blob) && !(body instanceof FormData)) {
66
+ parsedBody = JSON.stringify(body);
67
+ if (!headers.has('Content-Type')) {
68
+ headers.set('Content-Type', 'application/json');
69
+ }
70
+ }
71
+
72
+ // Generate unique request ID to scope request-specific event listeners
73
+ const requestId = typeof crypto !== 'undefined' && crypto.randomUUID
74
+ ? crypto.randomUUID()
75
+ : Math.random().toString(36).slice(2) + Date.now().toString(36);
76
+
77
+ const descriptor = {
78
+ requestId,
79
+ url: resolvedUrl,
80
+ method,
81
+ headers,
82
+ body: parsedBody,
83
+ signal: opts.signal,
84
+ priority: opts.priority || 'user-visible',
85
+ timeout: opts.timeout || 10000,
86
+ cache: opts.cache, // cache strategy name: 'cache-first' | 'network-first' | 'stale-while-revalidate'
87
+ retries: opts.retries ?? 3,
88
+ ...opts
89
+ };
90
+
91
+ // Register request-specific temporary listeners with automatic scoped cleanup
92
+ const disposes = [];
93
+ if (opts.on && typeof opts.on === 'object') {
94
+ for (const [event, handler] of Object.entries(opts.on)) {
95
+ if (typeof handler === 'function') {
96
+ const dispose = events.on(event, (e) => {
97
+ if (e.detail?.requestId === requestId) {
98
+ handler(e);
99
+ }
100
+ });
101
+ disposes.push(dispose);
102
+ }
103
+ }
104
+ }
105
+
106
+ try {
107
+ // Run through pipeline -> cache handler -> retry handler -> fetch executor
108
+ const response = await pipeline.run(descriptor, async (currentDesc) => {
109
+ return handleCache(currentDesc, async (cacheDesc) => {
110
+ return retry(
111
+ () => execute(cacheDesc),
112
+ {
113
+ attempts: cacheDesc.retries,
114
+ signal: cacheDesc.signal
115
+ }
116
+ );
117
+ });
118
+ });
119
+
120
+ // Automatically extract response payload if successful
121
+ const contentType = response.headers.get('Content-Type') || '';
122
+ if (contentType.includes('application/json')) {
123
+ return response.json();
124
+ }
125
+ return response.text();
126
+ } finally {
127
+ // Perform guaranteed automatic cleanup of temporary request-specific listeners
128
+ for (const dispose of disposes) {
129
+ dispose();
130
+ }
131
+ }
132
+ }
133
+
134
+ export const api = {
135
+ get: (url, opts) => request(url, 'GET', null, opts),
136
+ post: (url, body, opts) => request(url, 'POST', body, opts),
137
+ put: (url, body, opts) => request(url, 'PUT', body, opts),
138
+ patch: (url, body, opts) => request(url, 'PATCH', body, opts),
139
+ delete: (url, opts) => request(url, 'DELETE', null, opts),
140
+ stream,
141
+ upload,
142
+ pipeline,
143
+ PlatformError,
144
+
145
+ // Prefix registry singleton APIs
146
+ prefix: prefixes,
147
+
148
+ // Cache manager APIs
149
+ cache: apiCache,
150
+
151
+ // Event emitter hooks
152
+ on: (event, handler, signal) => events.on(event, handler, signal),
153
+ emit: (event, detail) => events.emit(event, detail)
154
+ };
155
+
156
+ export { pipeline, PlatformError, execute, retry, createNDJSONTransform, stream, upload, prefixes, events, apiCache as cache };
157
+
158
+
@@ -0,0 +1,98 @@
1
+ /**
2
+ * src/core/api/pipeline.js
3
+ *
4
+ * Composable interceptor chain.
5
+ * Manages request modification (outbound), short-circuiting (caching/mocking),
6
+ * and response normalization (inbound).
7
+ *
8
+ * Source: doc 11 — Networking §4, §5
9
+ */
10
+
11
+ export class Pipeline {
12
+ #outbound = [];
13
+ #inbound = [];
14
+
15
+ /**
16
+ * Registers an outbound (request) interceptor.
17
+ * Outbound interceptors receive a request descriptor and can:
18
+ * 1. Return a modified descriptor to pass to the next interceptor.
19
+ * 2. Return a Response instance to short-circuit the pipeline (e.g. cache hit).
20
+ */
21
+ outbound(interceptor) {
22
+ if (typeof interceptor !== 'function') {
23
+ throw new Error('Pipeline interceptor must be a function');
24
+ }
25
+ this.#outbound.push(interceptor);
26
+ return this;
27
+ }
28
+
29
+ /**
30
+ * Registers an inbound (response/error) interceptor.
31
+ * Inbound interceptors receive the Response (or Error) and return a Response
32
+ * or throw a normalized Error.
33
+ */
34
+ inbound(interceptor) {
35
+ if (typeof interceptor !== 'function') {
36
+ throw new Error('Pipeline interceptor must be a function');
37
+ }
38
+ this.#inbound.push(interceptor);
39
+ return this;
40
+ }
41
+
42
+ /**
43
+ * Executes the pipeline with a given request descriptor and a final fetch executor.
44
+ */
45
+ async run(descriptor, executeFetch) {
46
+ let current = { ...descriptor };
47
+
48
+ // 1. Outbound Pipeline
49
+ for (const interceptor of this.#outbound) {
50
+ const result = await interceptor(current);
51
+ if (result instanceof Response) {
52
+ // Short circuit: Return mock or cached response
53
+ try {
54
+ result.requestId = current.requestId;
55
+ } catch (_) {}
56
+ return this.#runInbound(result);
57
+ }
58
+ if (result) {
59
+ current = result;
60
+ }
61
+ }
62
+
63
+ // 2. Fetch Execution
64
+ let response;
65
+ try {
66
+ response = await executeFetch(current);
67
+ } catch (err) {
68
+ // Pass network/timeout errors to the inbound pipeline for normalization
69
+ try {
70
+ err.requestId = current.requestId;
71
+ } catch (_) {}
72
+ return this.#runInbound(err);
73
+ }
74
+
75
+ // 3. Inbound Pipeline
76
+ try {
77
+ response.requestId = current.requestId;
78
+ } catch (_) {}
79
+ return this.#runInbound(response);
80
+ }
81
+
82
+ async #runInbound(responseOrError) {
83
+ let current = responseOrError;
84
+ for (const interceptor of this.#inbound) {
85
+ try {
86
+ current = await interceptor(current);
87
+ } catch (err) {
88
+ current = err;
89
+ }
90
+ }
91
+ if (current instanceof Error) {
92
+ throw current;
93
+ }
94
+ return current;
95
+ }
96
+ }
97
+
98
+ export const pipeline = new Pipeline();
@@ -0,0 +1,209 @@
1
+ # API Client & Cache Enhancement Plan
2
+
3
+ This document outlines the blueprint for enhancing the `core.api` networking client with localized, fine-grained caching, prefix registration, namespace-level invalidation, and custom network/status-code events.
4
+
5
+ ---
6
+
7
+ ## 1. Architectural Architecture & Requirements
8
+
9
+ We will extend `core.api` to provide:
10
+
11
+ 1. **Default Zero-Cache Policy:** All calls default to no cache, running directly against the network.
12
+ 2. **TTL/Expiry-based API Caching:** Active caching when `expiry` or `ttl` (Time-To-Live) options are provided. Hits cache first, falls back to the network on a miss, and caches successful responses.
13
+ 3. **Namespace-level Glob Invalidation:** Support for clearing the entire cache, a single URL, or a glob namespace pattern (e.g. `/user/*`).
14
+ 4. **Outbound Prefix Resolution:** Initializing base prefixes once (optionally) to cleanly rewrite and resolve endpoint URLs.
15
+ 5. **Network Event Hub:** An integrated event listener system firing on generic network failures (errors, timeouts) or specific responses (status codes like `401`, `500`, or content types like `json`, `text`).
16
+
17
+ ---
18
+
19
+ ## 2. Structural & Folder Layout
20
+
21
+ In accordance with our naming and folder conventions (`RULE[user_global]`), we will group caching and events into highly structured subfolders under `src/core/api/`:
22
+
23
+ ```
24
+ src/core/api/
25
+ ├── caches/ # Subfolder for caching adapters & glob matching
26
+ │ ├── glob.js # Glob/Namespace pattern matching helper
27
+ │ └── index.js # Unified local API cache client
28
+ ├── events/ # Subfolder for network telemetry events
29
+ │ └── index.js # API event emitter implementation
30
+ ├── prefixes/ # Subfolder for prefix/base URL resolving
31
+ │ └── index.js # Prefix store and path normalization
32
+ ├── index.js # Core API client entry point
33
+ ├── fetch.js # Network request executor
34
+ ├── pipeline.js # Inbound/Outbound pipeline coordinator
35
+ ├── retry.js # Exponential backoff retry handler
36
+ ├── stream.js # Streams and NDJSON transform pipeline
37
+ └── upload.js # XMLHttpRequest-based upload gateway
38
+ ```
39
+
40
+ ---
41
+
42
+ ## 3. Technical Blueprint & Interface Design
43
+
44
+ ### Caches & Glob Purging (`src/core/api/caches/`)
45
+
46
+ The API cache uses the native browser Cache API with custom header tags to track record creation times and TTL.
47
+
48
+ ```javascript
49
+ // src/core/api/caches/index.js
50
+ export class ApiCache {
51
+ constructor(name = 'platform-api-cache') {
52
+ this.name = name;
53
+ }
54
+
55
+ async get(url) {
56
+ if (typeof caches === 'undefined') return null;
57
+ const store = await caches.open(this.name);
58
+ const cached = await store.match(url);
59
+ if (!cached) return null;
60
+
61
+ const expiresAt = cached.headers.get('x-expires-at');
62
+ if (expiresAt && Date.now() > Number(expiresAt)) {
63
+ await store.delete(url);
64
+ return null;
65
+ }
66
+ return cached.clone();
67
+ }
68
+
69
+ async set(url, response, ttlMs) {
70
+ if (typeof caches === 'undefined') return;
71
+ const store = await caches.open(this.name);
72
+ const headers = new Headers(response.headers);
73
+ headers.set('x-expires-at', String(Date.now() + ttlMs));
74
+
75
+ const cloned = new Response(response.body ? response.clone().body : null, {
76
+ status: response.status,
77
+ statusText: response.statusText,
78
+ headers
79
+ });
80
+ await store.put(url, cloned);
81
+ }
82
+
83
+ async delete(pattern) {
84
+ if (typeof caches === 'undefined') return;
85
+ const store = await caches.open(this.name);
86
+
87
+ if (pattern.includes('*')) {
88
+ const regex = globToRegex(pattern);
89
+ const keys = await store.keys();
90
+ for (const req of keys) {
91
+ if (regex.test(req.url) || regex.test(new URL(req.url).pathname)) {
92
+ await store.delete(req);
93
+ }
94
+ }
95
+ } else {
96
+ await store.delete(pattern);
97
+ }
98
+ }
99
+
100
+ async clear() {
101
+ if (typeof caches === 'undefined') return;
102
+ await caches.delete(this.name);
103
+ }
104
+ }
105
+ ```
106
+
107
+ Glob to Regex conversion helper:
108
+
109
+ ```javascript
110
+ // src/core/api/caches/glob.js
111
+ export function globToRegex(pattern) {
112
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
113
+ const wildcarded = escaped.replace(/\*/g, '.*');
114
+ return new RegExp(`^${wildcarded}$`);
115
+ }
116
+ ```
117
+
118
+ ### Prefix Resolver (`src/core/api/prefixes/`)
119
+
120
+ ```javascript
121
+ // src/core/api/prefixes/index.js
122
+ export class PrefixRegistry {
123
+ #prefixes = new Map();
124
+
125
+ add(name, value) {
126
+ this.#prefixes.set(name, value);
127
+ }
128
+
129
+ resolve(url) {
130
+ if (url.startsWith('http://') || url.startsWith('https://')) {
131
+ return url;
132
+ }
133
+ for (const [prefix, base] of this.#prefixes.entries()) {
134
+ if (url.startsWith(`/${prefix}/`)) {
135
+ return base + url.slice(prefix.length + 1);
136
+ }
137
+ if (url.startsWith(`${prefix}/`)) {
138
+ return base + '/' + url.slice(prefix.length);
139
+ }
140
+ }
141
+ // Default fallback to registered root or baseline domain
142
+ const root = this.#prefixes.get('root') || this.#prefixes.get('default');
143
+ if (root) {
144
+ return root + (url.startsWith('/') ? url : '/' + url);
145
+ }
146
+ return url;
147
+ }
148
+ }
149
+ ```
150
+
151
+ ### Telemetry Events (`src/core/api/events/`)
152
+
153
+ ```javascript
154
+ // src/core/api/events/index.js
155
+ export class ApiEventEmitter {
156
+ #listeners = new Map();
157
+
158
+ on(event, handler, signal) {
159
+ if (signal?.aborted) return () => {};
160
+ if (!this.#listeners.has(event)) {
161
+ this.#listeners.set(event, new Set());
162
+ }
163
+ const listener = { handler };
164
+ this.#listeners.get(event).add(listener);
165
+
166
+ const dispose = () => {
167
+ const set = this.#listeners.get(event);
168
+ if (set) {
169
+ set.delete(listener);
170
+ if (set.size === 0) this.#listeners.delete(event);
171
+ }
172
+ };
173
+
174
+ if (signal) {
175
+ signal.addEventListener('abort', dispose);
176
+ }
177
+ return dispose;
178
+ }
179
+
180
+ emit(event, detail) {
181
+ const set = this.#listeners.get(event);
182
+ if (!set) return;
183
+ const customEvent = { type: event, detail };
184
+ for (const listener of [...set]) {
185
+ try {
186
+ listener.handler(customEvent);
187
+ } catch (err) {
188
+ console.error(`Error in API event listener for "${event}":`, err);
189
+ }
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ ---
196
+
197
+ ## 4. Verification Plan
198
+
199
+ ### Automated Verification
200
+
201
+ - Write comprehensive test coverage validating:
202
+ - Cache hits on active TTL/Expiry options.
203
+ - Glob namespace clearing matches (e.g. `/user/*` successfully purges `/user/profile` and `/user/settings`).
204
+ - Outbound path prefix expansion.
205
+ - Correct event dispatching for generic `'error'`, `'timeout'`, specific codes (`status:401`), and content types (`type:json`).
206
+
207
+ ### Integration Check
208
+
209
+ - Assert network trace outputs and test page lifecycle triggers inside the browser environment.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * src/core/api/prefixes/index.js
3
+ *
4
+ * Prefix and Base URL registry for relative path expansion.
5
+ * Allows clean routing/prefix matching at connection startup.
6
+ *
7
+ * Source: doc 11 — Networking §2, core/api/plan.md
8
+ */
9
+
10
+ export class PrefixRegistry {
11
+ #prefixes = new Map();
12
+
13
+ /**
14
+ * Registers a new base URL prefix.
15
+ *
16
+ * @param {string} name
17
+ * @param {string} value
18
+ */
19
+ add(name, value) {
20
+ this.#prefixes.set(name, value);
21
+ }
22
+
23
+ /**
24
+ * Clears all registered prefixes.
25
+ */
26
+ clear() {
27
+ this.#prefixes.clear();
28
+ }
29
+
30
+ /**
31
+ * Resolves a URL prefix against registered base URLs.
32
+ *
33
+ * @param {string} url
34
+ * @returns {string}
35
+ */
36
+ resolve(url) {
37
+ if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) {
38
+ return url;
39
+ }
40
+
41
+ for (const [prefix, base] of this.#prefixes.entries()) {
42
+ if (url.startsWith(`/${prefix}/`)) {
43
+ const suffix = url.slice(prefix.length + 2);
44
+ const normalizedBase = base.endsWith('/') ? base : base + '/';
45
+ return normalizedBase + suffix;
46
+ }
47
+ if (url.startsWith(`${prefix}/`)) {
48
+ const suffix = url.slice(prefix.length + 1);
49
+ const normalizedBase = base.endsWith('/') ? base : base + '/';
50
+ return normalizedBase + suffix;
51
+ }
52
+ }
53
+
54
+ // Default fallback base
55
+ const root = this.#prefixes.get('root') || this.#prefixes.get('default');
56
+ if (root) {
57
+ const normalizedRoot = root.endsWith('/') ? root.slice(0, -1) : root;
58
+ const normalizedUrl = url.startsWith('/') ? url : '/' + url;
59
+ return normalizedRoot + normalizedUrl;
60
+ }
61
+
62
+ return url;
63
+ }
64
+ }
65
+
66
+ export const prefixes = new PrefixRegistry();