decantr 0.9.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 (382) hide show
  1. package/AGENTS.md +868 -0
  2. package/CHANGELOG.md +255 -0
  3. package/CLAUDE.md +178 -0
  4. package/LICENSE +21 -0
  5. package/README.md +229 -0
  6. package/cli/art.js +127 -0
  7. package/cli/commands/a11y.js +61 -0
  8. package/cli/commands/audit.js +225 -0
  9. package/cli/commands/build.js +38 -0
  10. package/cli/commands/dev.js +18 -0
  11. package/cli/commands/doctor.js +197 -0
  12. package/cli/commands/figma-sync.js +48 -0
  13. package/cli/commands/figma-tokens.js +55 -0
  14. package/cli/commands/generate.js +26 -0
  15. package/cli/commands/init.js +116 -0
  16. package/cli/commands/lint.js +209 -0
  17. package/cli/commands/mcp.js +530 -0
  18. package/cli/commands/migrate.js +175 -0
  19. package/cli/commands/test.js +38 -0
  20. package/cli/commands/validate.js +354 -0
  21. package/cli/index.js +113 -0
  22. package/package.json +95 -0
  23. package/reference/atoms.md +517 -0
  24. package/reference/behaviors.md +384 -0
  25. package/reference/build-tooling.md +275 -0
  26. package/reference/color-guidelines.md +965 -0
  27. package/reference/component-lifecycle.md +137 -0
  28. package/reference/compound-spacing.md +95 -0
  29. package/reference/decantation-process.md +499 -0
  30. package/reference/dev-server-routes.md +93 -0
  31. package/reference/form-system.md +253 -0
  32. package/reference/i18n.md +336 -0
  33. package/reference/icons.md +576 -0
  34. package/reference/llm-primer.md +953 -0
  35. package/reference/plugins.md +252 -0
  36. package/reference/registry-consumption.md +76 -0
  37. package/reference/router.md +217 -0
  38. package/reference/shells.md +116 -0
  39. package/reference/spatial-guidelines.md +541 -0
  40. package/reference/ssr.md +234 -0
  41. package/reference/state-data.md +215 -0
  42. package/reference/state-patterns.md +166 -0
  43. package/reference/state.md +194 -0
  44. package/reference/style-system.md +110 -0
  45. package/reference/tokens.md +460 -0
  46. package/src/app.js +19 -0
  47. package/src/chart/_animate.js +266 -0
  48. package/src/chart/_base.js +109 -0
  49. package/src/chart/_data.js +209 -0
  50. package/src/chart/_format.js +106 -0
  51. package/src/chart/_interact.js +364 -0
  52. package/src/chart/_palette.js +105 -0
  53. package/src/chart/_renderer.js +52 -0
  54. package/src/chart/_scene.js +262 -0
  55. package/src/chart/_shared.js +371 -0
  56. package/src/chart/index.js +637 -0
  57. package/src/chart/layouts/_layout-base.js +328 -0
  58. package/src/chart/layouts/cartesian.js +148 -0
  59. package/src/chart/layouts/hierarchy.js +562 -0
  60. package/src/chart/layouts/polar.js +101 -0
  61. package/src/chart/renderers/canvas.js +179 -0
  62. package/src/chart/renderers/svg.js +256 -0
  63. package/src/chart/renderers/webgpu.js +715 -0
  64. package/src/chart/types/_type-base.js +26 -0
  65. package/src/chart/types/area.js +134 -0
  66. package/src/chart/types/bar.js +173 -0
  67. package/src/chart/types/box-plot.js +125 -0
  68. package/src/chart/types/bubble.js +63 -0
  69. package/src/chart/types/candlestick.js +115 -0
  70. package/src/chart/types/chord.js +85 -0
  71. package/src/chart/types/combination.js +108 -0
  72. package/src/chart/types/funnel.js +68 -0
  73. package/src/chart/types/gauge.js +163 -0
  74. package/src/chart/types/heatmap.js +98 -0
  75. package/src/chart/types/histogram.js +71 -0
  76. package/src/chart/types/line.js +111 -0
  77. package/src/chart/types/org-chart.js +93 -0
  78. package/src/chart/types/pie.js +81 -0
  79. package/src/chart/types/radar.js +96 -0
  80. package/src/chart/types/radial.js +68 -0
  81. package/src/chart/types/range-area.js +55 -0
  82. package/src/chart/types/range-bar.js +61 -0
  83. package/src/chart/types/sankey.js +73 -0
  84. package/src/chart/types/scatter.js +66 -0
  85. package/src/chart/types/sparkline.js +81 -0
  86. package/src/chart/types/sunburst.js +69 -0
  87. package/src/chart/types/swimlane.js +88 -0
  88. package/src/chart/types/treemap.js +62 -0
  89. package/src/chart/types/waterfall.js +100 -0
  90. package/src/components/_base.js +1658 -0
  91. package/src/components/_behaviors.js +1140 -0
  92. package/src/components/_primitives.js +534 -0
  93. package/src/components/_qr-encoder.js +539 -0
  94. package/src/components/accordion.js +207 -0
  95. package/src/components/affix.js +62 -0
  96. package/src/components/alert-dialog.js +75 -0
  97. package/src/components/alert.js +47 -0
  98. package/src/components/aspect-ratio.js +24 -0
  99. package/src/components/avatar-group.js +55 -0
  100. package/src/components/avatar.js +38 -0
  101. package/src/components/back-top.js +75 -0
  102. package/src/components/badge.js +74 -0
  103. package/src/components/banner.js +68 -0
  104. package/src/components/breadcrumb.js +162 -0
  105. package/src/components/button.js +115 -0
  106. package/src/components/calendar.js +131 -0
  107. package/src/components/card.js +192 -0
  108. package/src/components/carousel.js +98 -0
  109. package/src/components/cascader.js +261 -0
  110. package/src/components/checkbox.js +80 -0
  111. package/src/components/chip.js +81 -0
  112. package/src/components/code-block.js +82 -0
  113. package/src/components/collapsible.js +50 -0
  114. package/src/components/color-palette.js +438 -0
  115. package/src/components/color-picker.js +314 -0
  116. package/src/components/combobox.js +181 -0
  117. package/src/components/command.js +174 -0
  118. package/src/components/comment.js +206 -0
  119. package/src/components/context-menu.js +76 -0
  120. package/src/components/data-table.js +724 -0
  121. package/src/components/date-picker.js +217 -0
  122. package/src/components/date-range-picker.js +244 -0
  123. package/src/components/datetime-picker.js +271 -0
  124. package/src/components/descriptions.js +68 -0
  125. package/src/components/drawer.js +179 -0
  126. package/src/components/dropdown.js +88 -0
  127. package/src/components/empty.js +41 -0
  128. package/src/components/float-button.js +90 -0
  129. package/src/components/form.js +106 -0
  130. package/src/components/hover-card.js +49 -0
  131. package/src/components/icon.js +87 -0
  132. package/src/components/image.js +97 -0
  133. package/src/components/index.js +117 -0
  134. package/src/components/input-group.js +75 -0
  135. package/src/components/input-number.js +155 -0
  136. package/src/components/input-otp.js +178 -0
  137. package/src/components/input.js +91 -0
  138. package/src/components/kbd.js +36 -0
  139. package/src/components/label.js +25 -0
  140. package/src/components/list.js +118 -0
  141. package/src/components/masked-input.js +236 -0
  142. package/src/components/mentions.js +165 -0
  143. package/src/components/menu.js +259 -0
  144. package/src/components/message.js +80 -0
  145. package/src/components/modal.js +147 -0
  146. package/src/components/navigation-menu.js +166 -0
  147. package/src/components/notification.js +84 -0
  148. package/src/components/pagination.js +104 -0
  149. package/src/components/placeholder.js +132 -0
  150. package/src/components/popconfirm.js +70 -0
  151. package/src/components/popover.js +58 -0
  152. package/src/components/progress.js +61 -0
  153. package/src/components/qrcode.js +251 -0
  154. package/src/components/radiogroup.js +120 -0
  155. package/src/components/range-slider.js +176 -0
  156. package/src/components/rate.js +186 -0
  157. package/src/components/resizable.js +83 -0
  158. package/src/components/result.js +57 -0
  159. package/src/components/scroll-area.js +43 -0
  160. package/src/components/segmented.js +97 -0
  161. package/src/components/select.js +165 -0
  162. package/src/components/separator.js +31 -0
  163. package/src/components/shell.js +407 -0
  164. package/src/components/skeleton.js +39 -0
  165. package/src/components/slider.js +141 -0
  166. package/src/components/sortable-list.js +176 -0
  167. package/src/components/space.js +42 -0
  168. package/src/components/spinner.js +112 -0
  169. package/src/components/splitter.js +147 -0
  170. package/src/components/statistic.js +136 -0
  171. package/src/components/steps.js +99 -0
  172. package/src/components/switch.js +95 -0
  173. package/src/components/table.js +44 -0
  174. package/src/components/tabs.js +216 -0
  175. package/src/components/tag.js +115 -0
  176. package/src/components/textarea.js +82 -0
  177. package/src/components/time-picker.js +153 -0
  178. package/src/components/time-range-picker.js +170 -0
  179. package/src/components/timeline.js +226 -0
  180. package/src/components/toast.js +71 -0
  181. package/src/components/toggle.js +213 -0
  182. package/src/components/tooltip.js +57 -0
  183. package/src/components/tour.js +159 -0
  184. package/src/components/transfer.js +163 -0
  185. package/src/components/tree-select.js +274 -0
  186. package/src/components/tree.js +141 -0
  187. package/src/components/typography.js +136 -0
  188. package/src/components/upload.js +118 -0
  189. package/src/components/visually-hidden.js +20 -0
  190. package/src/components/watermark.js +124 -0
  191. package/src/core/index.js +539 -0
  192. package/src/core/lifecycle.js +69 -0
  193. package/src/css/atoms.js +651 -0
  194. package/src/css/components.js +940 -0
  195. package/src/css/derive.js +1296 -0
  196. package/src/css/index.js +265 -0
  197. package/src/css/runtime.js +268 -0
  198. package/src/css/styles/addons/bioluminescent.js +93 -0
  199. package/src/css/styles/addons/clay.js +70 -0
  200. package/src/css/styles/addons/clean.js +57 -0
  201. package/src/css/styles/addons/command-center.js +143 -0
  202. package/src/css/styles/addons/dopamine.js +83 -0
  203. package/src/css/styles/addons/editorial.js +80 -0
  204. package/src/css/styles/addons/glassmorphism.js +99 -0
  205. package/src/css/styles/addons/liquid-glass.js +105 -0
  206. package/src/css/styles/addons/prismatic.js +100 -0
  207. package/src/css/styles/addons/retro.js +63 -0
  208. package/src/css/styles/auradecantism.js +96 -0
  209. package/src/css/theme-registry.js +444 -0
  210. package/src/data/entity.js +281 -0
  211. package/src/data/index.js +13 -0
  212. package/src/data/persist.js +225 -0
  213. package/src/data/query.js +839 -0
  214. package/src/data/realtime.js +299 -0
  215. package/src/data/url.js +177 -0
  216. package/src/data/worker.js +134 -0
  217. package/src/explorer/archetypes.js +243 -0
  218. package/src/explorer/atoms.js +228 -0
  219. package/src/explorer/charts.js +497 -0
  220. package/src/explorer/components.js +129 -0
  221. package/src/explorer/foundations.js +949 -0
  222. package/src/explorer/icons.js +178 -0
  223. package/src/explorer/patterns.js +247 -0
  224. package/src/explorer/recipes.js +194 -0
  225. package/src/explorer/shared/pattern-examples.js +1337 -0
  226. package/src/explorer/shared/showcase-renderer.js +958 -0
  227. package/src/explorer/shared/spec-table.js +41 -0
  228. package/src/explorer/shared/usage-links.js +87 -0
  229. package/src/explorer/shell-config.js +10 -0
  230. package/src/explorer/shells.js +551 -0
  231. package/src/explorer/styles.js +161 -0
  232. package/src/explorer/tokens.js +262 -0
  233. package/src/explorer/tools.js +525 -0
  234. package/src/form/index.js +804 -0
  235. package/src/i18n/index.js +251 -0
  236. package/src/icons/essential.js +479 -0
  237. package/src/icons/index.js +53 -0
  238. package/src/plugins/index.js +282 -0
  239. package/src/registry/archetypes/content-site.json +71 -0
  240. package/src/registry/archetypes/docs-explorer.json +23 -0
  241. package/src/registry/archetypes/ecommerce.json +104 -0
  242. package/src/registry/archetypes/financial-dashboard.json +77 -0
  243. package/src/registry/archetypes/index.json +41 -0
  244. package/src/registry/archetypes/portfolio.json +82 -0
  245. package/src/registry/archetypes/recipe-community.json +159 -0
  246. package/src/registry/archetypes/saas-dashboard.json +86 -0
  247. package/src/registry/architect/cross-cutting.json +45 -0
  248. package/src/registry/architect/domains/ecommerce.json +294 -0
  249. package/src/registry/architect/domains/financial-services.json +302 -0
  250. package/src/registry/architect/index.json +26 -0
  251. package/src/registry/architect/traits.json +379 -0
  252. package/src/registry/atoms.json +16 -0
  253. package/src/registry/chart-showcase.json +160 -0
  254. package/src/registry/chart.json +136 -0
  255. package/src/registry/components.json +8616 -0
  256. package/src/registry/core.json +216 -0
  257. package/src/registry/css.json +319 -0
  258. package/src/registry/data.json +135 -0
  259. package/src/registry/foundations.json +11 -0
  260. package/src/registry/icons.json +463 -0
  261. package/src/registry/index.json +101 -0
  262. package/src/registry/patterns/activity-feed.json +37 -0
  263. package/src/registry/patterns/article-content.json +27 -0
  264. package/src/registry/patterns/auth-form.json +37 -0
  265. package/src/registry/patterns/author-card.json +20 -0
  266. package/src/registry/patterns/card-grid.json +127 -0
  267. package/src/registry/patterns/category-nav.json +26 -0
  268. package/src/registry/patterns/chart-grid.json +36 -0
  269. package/src/registry/patterns/chat-interface.json +37 -0
  270. package/src/registry/patterns/checklist-card.json +55 -0
  271. package/src/registry/patterns/comparison-panel.json +27 -0
  272. package/src/registry/patterns/component-showcase.json +24 -0
  273. package/src/registry/patterns/contact-form.json +31 -0
  274. package/src/registry/patterns/cta-section.json +20 -0
  275. package/src/registry/patterns/data-table.json +37 -0
  276. package/src/registry/patterns/detail-header.json +83 -0
  277. package/src/registry/patterns/detail-panel.json +27 -0
  278. package/src/registry/patterns/explorer-shell.json +22 -0
  279. package/src/registry/patterns/filter-bar.json +33 -0
  280. package/src/registry/patterns/filter-sidebar.json +27 -0
  281. package/src/registry/patterns/form-sections.json +110 -0
  282. package/src/registry/patterns/goal-tracker.json +27 -0
  283. package/src/registry/patterns/hero.json +107 -0
  284. package/src/registry/patterns/index.json +47 -0
  285. package/src/registry/patterns/kpi-grid.json +36 -0
  286. package/src/registry/patterns/media-gallery.json +20 -0
  287. package/src/registry/patterns/order-history.json +20 -0
  288. package/src/registry/patterns/pagination.json +19 -0
  289. package/src/registry/patterns/photo-to-recipe.json +36 -0
  290. package/src/registry/patterns/pipeline-tracker.json +28 -0
  291. package/src/registry/patterns/post-list.json +27 -0
  292. package/src/registry/patterns/pricing-table.json +32 -0
  293. package/src/registry/patterns/scorecard.json +28 -0
  294. package/src/registry/patterns/search-bar.json +20 -0
  295. package/src/registry/patterns/specimen-grid.json +19 -0
  296. package/src/registry/patterns/stat-card.json +55 -0
  297. package/src/registry/patterns/stats-bar.json +55 -0
  298. package/src/registry/patterns/steps-card.json +55 -0
  299. package/src/registry/patterns/table-of-contents.json +19 -0
  300. package/src/registry/patterns/testimonials.json +21 -0
  301. package/src/registry/patterns/timeline.json +27 -0
  302. package/src/registry/patterns/token-inspector.json +21 -0
  303. package/src/registry/patterns/wizard.json +27 -0
  304. package/src/registry/recipe-auradecantism.json +69 -0
  305. package/src/registry/recipe-clean.json +65 -0
  306. package/src/registry/recipe-command-center.json +78 -0
  307. package/src/registry/router.json +73 -0
  308. package/src/registry/schema/README.md +197 -0
  309. package/src/registry/skeletons.json +259 -0
  310. package/src/registry/state.json +137 -0
  311. package/src/registry/tokens.json +40 -0
  312. package/src/router/hash.js +17 -0
  313. package/src/router/history.js +18 -0
  314. package/src/router/index.js +598 -0
  315. package/src/ssr/index.js +922 -0
  316. package/src/state/arrays.js +181 -0
  317. package/src/state/devtools.js +647 -0
  318. package/src/state/index.js +498 -0
  319. package/src/state/middleware.js +288 -0
  320. package/src/state/scheduler.js +206 -0
  321. package/src/state/store.js +300 -0
  322. package/src/tags/index.js +19 -0
  323. package/src/tannins/auth.js +396 -0
  324. package/src/test/dom.js +352 -0
  325. package/src/test/index.js +62 -0
  326. package/src/test/state.js +306 -0
  327. package/tools/a11y-audit.js +487 -0
  328. package/tools/analyzer.js +315 -0
  329. package/tools/audit.js +706 -0
  330. package/tools/builder.js +1422 -0
  331. package/tools/css-extract.js +188 -0
  332. package/tools/dev-server.js +316 -0
  333. package/tools/dts-gen.js +1260 -0
  334. package/tools/figma-components.js +329 -0
  335. package/tools/figma-patterns.js +516 -0
  336. package/tools/figma-plugin/code.js +453 -0
  337. package/tools/figma-plugin/manifest.json +14 -0
  338. package/tools/figma-plugin/ui.html +268 -0
  339. package/tools/figma-render.js +293 -0
  340. package/tools/figma-tokens.js +712 -0
  341. package/tools/figma-upload.js +318 -0
  342. package/tools/generate.js +738 -0
  343. package/tools/icons.js +133 -0
  344. package/tools/init-templates.js +265 -0
  345. package/tools/install-hooks.sh +5 -0
  346. package/tools/migrations/0.5.0.js +53 -0
  347. package/tools/migrations/0.6.0.js +95 -0
  348. package/tools/minify.js +170 -0
  349. package/tools/pre-commit +4 -0
  350. package/tools/registry.js +662 -0
  351. package/tools/reset-playground.js +61 -0
  352. package/tools/starter-templates/content-site/app.js +49 -0
  353. package/tools/starter-templates/content-site/essence.js +19 -0
  354. package/tools/starter-templates/content-site/pages.js +31 -0
  355. package/tools/starter-templates/ecommerce/app.js +50 -0
  356. package/tools/starter-templates/ecommerce/essence.js +19 -0
  357. package/tools/starter-templates/ecommerce/pages.js +31 -0
  358. package/tools/starter-templates/landing-page/app.js +38 -0
  359. package/tools/starter-templates/landing-page/essence.js +18 -0
  360. package/tools/starter-templates/landing-page/pages.js +21 -0
  361. package/tools/starter-templates/portfolio/app.js +45 -0
  362. package/tools/starter-templates/portfolio/essence.js +19 -0
  363. package/tools/starter-templates/portfolio/pages.js +33 -0
  364. package/tools/starter-templates/saas-dashboard/app.js +70 -0
  365. package/tools/starter-templates/saas-dashboard/essence.js +19 -0
  366. package/tools/starter-templates/saas-dashboard/pages.js +31 -0
  367. package/tools/verify-pack.js +203 -0
  368. package/types/chart.d.ts +77 -0
  369. package/types/components.d.ts +587 -0
  370. package/types/core.d.ts +89 -0
  371. package/types/css.d.ts +149 -0
  372. package/types/data.d.ts +238 -0
  373. package/types/form.d.ts +164 -0
  374. package/types/i18n.d.ts +51 -0
  375. package/types/icons.d.ts +27 -0
  376. package/types/index.d.ts +13 -0
  377. package/types/router.d.ts +116 -0
  378. package/types/ssr.d.ts +102 -0
  379. package/types/state.d.ts +83 -0
  380. package/types/tags.d.ts +62 -0
  381. package/types/tannins.d.ts +63 -0
  382. package/types/test.d.ts +48 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Decantr Realtime Primitives
3
+ * WebSocket and EventSource wrappers built on Decantr signals.
4
+ *
5
+ * @module decantr/data/realtime
6
+ *
7
+ * Exports:
8
+ * createWebSocket(url, options?) — Reactive WebSocket with auto-reconnect
9
+ * createEventSource(url, options?) — Reactive Server-Sent Events wrapper
10
+ */
11
+ import { createSignal, batch } from '../state/index.js';
12
+
13
+ // ─── CONSTANTS ──────────────────────────────────────────────────
14
+
15
+ const MAX_MESSAGES = 100;
16
+ const MAX_BACKOFF = 30000;
17
+
18
+ // ─── createWebSocket ────────────────────────────────────────────
19
+
20
+ /**
21
+ * Create a reactive WebSocket connection with auto-reconnect and message buffering.
22
+ *
23
+ * @param {string} url — WebSocket endpoint (ws:// or wss://)
24
+ * @param {object} [options]
25
+ * @param {number} [options.reconnectDelay=1000] — Initial reconnect delay in ms
26
+ * @param {number} [options.maxRetries=5] — Maximum reconnect attempts
27
+ * @param {boolean} [options.buffer=true] — Buffer sends while disconnected
28
+ * @param {(data: any) => any} [options.parse] — Message parser (default: identity)
29
+ * @param {string[]} [options.protocols] — WebSocket sub-protocols
30
+ * @returns {{
31
+ * lastMessage: () => any,
32
+ * messages: () => any[],
33
+ * status: () => 'connecting'|'open'|'closed'|'reconnecting',
34
+ * send: (data: any) => void,
35
+ * close: () => void,
36
+ * reconnect: () => void,
37
+ * on: (handler: (msg: any) => void) => () => void
38
+ * }}
39
+ */
40
+ export function createWebSocket(url, options = {}) {
41
+ const {
42
+ reconnectDelay = 1000,
43
+ maxRetries = 5,
44
+ buffer = true,
45
+ parse = (d) => d,
46
+ protocols
47
+ } = options;
48
+
49
+ const [lastMessage, setLastMessage] = createSignal(null);
50
+ const [messages, setMessages] = createSignal([]);
51
+ const [status, setStatus] = createSignal('connecting');
52
+
53
+ /** @type {((msg: any) => void)[]} */
54
+ const handlers = [];
55
+ /** @type {any[]} */
56
+ const sendBuffer = [];
57
+ /** @type {{ ws: WebSocket|null, attempts: number, timer: number|null, stopped: boolean }} */
58
+ const state = { ws: null, attempts: 0, timer: null, stopped: false };
59
+
60
+ /**
61
+ * Flush the send buffer once the socket is open.
62
+ * @param {WebSocket} ws
63
+ */
64
+ function flushBuffer(ws) {
65
+ while (sendBuffer.length > 0) {
66
+ ws.send(sendBuffer.shift());
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Connect (or reconnect) to the WebSocket server.
72
+ */
73
+ function connect() {
74
+ if (state.stopped) return;
75
+ const ws = protocols
76
+ ? new WebSocket(url, protocols)
77
+ : new WebSocket(url);
78
+
79
+ state.ws = ws;
80
+
81
+ ws.addEventListener('open', () => {
82
+ batch(() => {
83
+ state.attempts = 0;
84
+ setStatus('open');
85
+ });
86
+ flushBuffer(ws);
87
+ });
88
+
89
+ ws.addEventListener('message', (ev) => {
90
+ const parsed = parse(ev.data);
91
+ batch(() => {
92
+ setLastMessage(parsed);
93
+ setMessages((prev) => {
94
+ const next = prev.concat(parsed);
95
+ return next.length > MAX_MESSAGES ? next.slice(next.length - MAX_MESSAGES) : next;
96
+ });
97
+ });
98
+ for (let i = 0; i < handlers.length; i++) handlers[i](parsed);
99
+ });
100
+
101
+ ws.addEventListener('close', () => {
102
+ if (state.stopped) {
103
+ setStatus('closed');
104
+ return;
105
+ }
106
+ scheduleReconnect();
107
+ });
108
+
109
+ ws.addEventListener('error', () => {
110
+ // Error is always followed by close — reconnect logic lives there.
111
+ // Explicitly close to ensure the close event fires consistently.
112
+ ws.close();
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Schedule a reconnect attempt with exponential backoff + jitter.
118
+ */
119
+ function scheduleReconnect() {
120
+ if (state.stopped) return;
121
+ if (state.attempts >= maxRetries) {
122
+ setStatus('closed');
123
+ return;
124
+ }
125
+ setStatus('reconnecting');
126
+ const delay = Math.min(
127
+ reconnectDelay * Math.pow(2, state.attempts) + Math.random() * 1000,
128
+ MAX_BACKOFF
129
+ );
130
+ state.attempts++;
131
+ state.timer = setTimeout(() => {
132
+ state.timer = null;
133
+ connect();
134
+ }, delay);
135
+ }
136
+
137
+ /**
138
+ * Send data through the WebSocket. Buffers if disconnected and `buffer` is enabled.
139
+ * @param {any} data
140
+ */
141
+ function send(data) {
142
+ if (state.ws && state.ws.readyState === WebSocket.OPEN) {
143
+ state.ws.send(data);
144
+ } else if (buffer) {
145
+ sendBuffer.push(data);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Close the connection and stop all reconnection attempts.
151
+ */
152
+ function close() {
153
+ state.stopped = true;
154
+ if (state.timer !== null) {
155
+ clearTimeout(state.timer);
156
+ state.timer = null;
157
+ }
158
+ if (state.ws) {
159
+ state.ws.close();
160
+ state.ws = null;
161
+ }
162
+ setStatus('closed');
163
+ }
164
+
165
+ /**
166
+ * Manually trigger a reconnect. Resets attempt counter.
167
+ */
168
+ function reconnect() {
169
+ if (state.timer !== null) {
170
+ clearTimeout(state.timer);
171
+ state.timer = null;
172
+ }
173
+ if (state.ws) {
174
+ state.stopped = true; // prevent auto-reconnect from close handler
175
+ state.ws.close();
176
+ state.ws = null;
177
+ }
178
+ state.stopped = false;
179
+ state.attempts = 0;
180
+ setStatus('connecting');
181
+ connect();
182
+ }
183
+
184
+ /**
185
+ * Register a message handler. Returns an unsubscribe function.
186
+ * @param {(msg: any) => void} handler
187
+ * @returns {() => void}
188
+ */
189
+ function on(handler) {
190
+ handlers.push(handler);
191
+ return () => {
192
+ const idx = handlers.indexOf(handler);
193
+ if (idx !== -1) handlers.splice(idx, 1);
194
+ };
195
+ }
196
+
197
+ // Kick off initial connection
198
+ connect();
199
+
200
+ return { lastMessage, messages, status, send, close, reconnect, on };
201
+ }
202
+
203
+ // ─── createEventSource ──────────────────────────────────────────
204
+
205
+ /**
206
+ * Create a reactive EventSource (Server-Sent Events) wrapper.
207
+ *
208
+ * @param {string} url — SSE endpoint
209
+ * @param {object} [options]
210
+ * @param {string[]} [options.events] — Event types to listen for (default: ['message'])
211
+ * @param {boolean} [options.withCredentials=false] — Send credentials with request
212
+ * @returns {{
213
+ * lastEvent: () => { type: string, data: string }|null,
214
+ * status: () => 'connecting'|'open'|'closed',
215
+ * close: () => void,
216
+ * on: (eventType: string, handler: (ev: { type: string, data: string }) => void) => () => void
217
+ * }}
218
+ */
219
+ export function createEventSource(url, options = {}) {
220
+ const { events, withCredentials = false } = options;
221
+
222
+ const [lastEvent, setLastEvent] = createSignal(null);
223
+ const [status, setStatus] = createSignal('connecting');
224
+
225
+ /** @type {Map<string, ((ev: {type: string, data: string}) => void)[]>} */
226
+ const handlerMap = new Map();
227
+
228
+ const source = new EventSource(url, { withCredentials });
229
+
230
+ source.addEventListener('open', () => {
231
+ setStatus('open');
232
+ });
233
+
234
+ source.addEventListener('error', () => {
235
+ // EventSource auto-reconnects; reflect readyState
236
+ if (source.readyState === EventSource.CLOSED) {
237
+ setStatus('closed');
238
+ } else {
239
+ setStatus('connecting');
240
+ }
241
+ });
242
+
243
+ /**
244
+ * Internal handler factory for a given event type.
245
+ * @param {string} type
246
+ * @returns {(ev: MessageEvent) => void}
247
+ */
248
+ function makeListener(type) {
249
+ return (ev) => {
250
+ const record = { type, data: ev.data };
251
+ setLastEvent(record);
252
+ const list = handlerMap.get(type);
253
+ if (list) {
254
+ for (let i = 0; i < list.length; i++) list[i](record);
255
+ }
256
+ };
257
+ }
258
+
259
+ // Attach listeners for requested event types
260
+ const types = events && events.length > 0 ? events : ['message'];
261
+ for (let i = 0; i < types.length; i++) {
262
+ source.addEventListener(types[i], makeListener(types[i]));
263
+ }
264
+
265
+ /**
266
+ * Close the EventSource connection.
267
+ */
268
+ function close() {
269
+ source.close();
270
+ setStatus('closed');
271
+ }
272
+
273
+ /**
274
+ * Register a handler for a specific event type. Returns an unsubscribe function.
275
+ * If the event type was not in the initial `events` list, it is dynamically added.
276
+ *
277
+ * @param {string} eventType
278
+ * @param {(ev: { type: string, data: string }) => void} handler
279
+ * @returns {() => void}
280
+ */
281
+ function on(eventType, handler) {
282
+ if (!handlerMap.has(eventType)) {
283
+ handlerMap.set(eventType, []);
284
+ // Attach a native listener if this type wasn't in the initial set
285
+ if (!types.includes(eventType)) {
286
+ source.addEventListener(eventType, makeListener(eventType));
287
+ }
288
+ }
289
+ handlerMap.get(eventType).push(handler);
290
+ return () => {
291
+ const list = handlerMap.get(eventType);
292
+ if (!list) return;
293
+ const idx = list.indexOf(handler);
294
+ if (idx !== -1) list.splice(idx, 1);
295
+ };
296
+ }
297
+
298
+ return { lastEvent, status, close, on };
299
+ }
@@ -0,0 +1,177 @@
1
+ import { createSignal, batch, untrack } from '../state/index.js';
2
+
3
+ /* Built-in parsers -------------------------------------------------------- */
4
+
5
+ /** @type {{ parse: (v: string) => string, serialize: (v: string) => string }} */
6
+ const string = { parse: v => v, serialize: v => v };
7
+ /** @type {{ parse: (v: string) => number, serialize: (v: number) => string }} */
8
+ const integer = { parse: v => parseInt(v, 10), serialize: v => String(v) };
9
+ /** @type {{ parse: (v: string) => number, serialize: (v: number) => string }} */
10
+ const float = { parse: v => parseFloat(v), serialize: v => String(v) };
11
+ /** @type {{ parse: (v: string) => boolean, serialize: (v: boolean) => string }} */
12
+ const boolean = { parse: v => v === 'true', serialize: v => v ? 'true' : 'false' };
13
+ /** @type {{ parse: (v: string) => any, serialize: (v: any) => string }} */
14
+ const json = { parse: v => JSON.parse(v), serialize: v => JSON.stringify(v) };
15
+ /** @type {{ parse: (v: string) => Date, serialize: (v: Date) => string }} */
16
+ const date = { parse: v => new Date(v), serialize: v => v.toISOString() };
17
+
18
+ /**
19
+ * Factory parser that validates against an allowed set of values.
20
+ * @template {string} T
21
+ * @param {T[]} values
22
+ * @returns {{ parse: (v: string) => T | undefined, serialize: (v: T) => string }}
23
+ */
24
+ function enumParser(values) {
25
+ const allowed = new Set(values);
26
+ return { parse: v => allowed.has(v) ? /** @type {T} */ (v) : undefined, serialize: v => v };
27
+ }
28
+
29
+ export const parsers = { string, integer, float, boolean, json, date, enum: enumParser };
30
+
31
+ /* Internal helpers -------------------------------------------------------- */
32
+
33
+ /** Detect hash-based routing (`#/...`). */
34
+ function isHashMode() {
35
+ return typeof window !== 'undefined' && window.location.hash.startsWith('#/');
36
+ }
37
+
38
+ /**
39
+ * Read current URL search params, respecting routing mode.
40
+ * @returns {URLSearchParams}
41
+ */
42
+ function readParams() {
43
+ if (isHashMode()) {
44
+ const hash = window.location.hash.slice(1);
45
+ const qi = hash.indexOf('?');
46
+ return new URLSearchParams(qi >= 0 ? hash.slice(qi + 1) : '');
47
+ }
48
+ return new URLSearchParams(window.location.search);
49
+ }
50
+
51
+ /**
52
+ * Write search params back to URL, preserving routing mode.
53
+ * @param {URLSearchParams} params
54
+ * @param {boolean} push — true = pushState, false = replaceState
55
+ */
56
+ function writeParams(params, push) {
57
+ const qs = params.toString();
58
+ const nav = push ? 'pushState' : 'replaceState';
59
+ if (isHashMode()) {
60
+ let hash = window.location.hash.slice(1);
61
+ const qi = hash.indexOf('?');
62
+ const path = qi >= 0 ? hash.slice(0, qi) : hash;
63
+ window.history[nav](null, '', window.location.pathname + window.location.search + '#' + path + (qs ? '?' + qs : ''));
64
+ } else {
65
+ window.history[nav](null, '', window.location.pathname + (qs ? '?' + qs : ''));
66
+ }
67
+ }
68
+
69
+ /* Throttled URL writer (shared across all signals) ------------------------ */
70
+
71
+ /** @type {number | null} */
72
+ let _flushTimer = null;
73
+ /** @type {Array<{ key: string, value: string | null, push: boolean }>} */
74
+ let _pendingWrites = [];
75
+
76
+ function scheduleFlush() {
77
+ if (_flushTimer !== null) return;
78
+ _flushTimer = setTimeout(() => {
79
+ _flushTimer = null;
80
+ const writes = _pendingWrites;
81
+ _pendingWrites = [];
82
+ if (writes.length === 0) return;
83
+ const params = readParams();
84
+ let push = false;
85
+ for (const w of writes) {
86
+ if (w.value === null) params.delete(w.key);
87
+ else params.set(w.key, w.value);
88
+ if (w.push) push = true;
89
+ }
90
+ writeParams(params, push);
91
+ }, 50);
92
+ }
93
+
94
+ /* createURLSignal --------------------------------------------------------- */
95
+
96
+ /**
97
+ * Create a reactive signal backed by a URL search parameter.
98
+ * @template T
99
+ * @param {string} key — URL param name
100
+ * @param {{ parse: (v: string) => T, serialize: (v: T) => string }} parser
101
+ * @param {{ defaultValue?: T, push?: boolean }} [options]
102
+ * @returns {[() => T, (v: T | ((prev: T) => T)) => void]}
103
+ */
104
+ export function createURLSignal(key, parser, options = {}) {
105
+ const { defaultValue, push = false } = options;
106
+
107
+ function fromURL() {
108
+ const raw = readParams().get(key);
109
+ if (raw === null) return defaultValue;
110
+ const parsed = parser.parse(raw);
111
+ return parsed === undefined ? defaultValue : parsed;
112
+ }
113
+
114
+ const [get, set] = createSignal(fromURL());
115
+
116
+ // URL -> signal: sync on browser navigation
117
+ const onNav = () => {
118
+ const next = fromURL();
119
+ if (!Object.is(untrack(get), next)) set(next);
120
+ };
121
+ if (typeof window !== 'undefined') {
122
+ window.addEventListener('popstate', onNav);
123
+ window.addEventListener('hashchange', onNav);
124
+ }
125
+
126
+ /**
127
+ * Update signal value and schedule a throttled URL write.
128
+ * @param {T | ((prev: T) => T)} v
129
+ */
130
+ function setter(v) {
131
+ const prev = untrack(get);
132
+ const next = typeof v === 'function' ? /** @type {Function} */ (v)(prev) : v;
133
+ set(next);
134
+ const serialized = Object.is(next, defaultValue) ? null : parser.serialize(next);
135
+ _pendingWrites.push({ key, value: serialized, push });
136
+ scheduleFlush();
137
+ }
138
+
139
+ return [get, setter];
140
+ }
141
+
142
+ /* createURLStore ---------------------------------------------------------- */
143
+
144
+ /**
145
+ * Create a reactive store backed by multiple URL search parameters.
146
+ * @template {Record<string, { parser: { parse: (v: string) => any, serialize: (v: any) => string }, defaultValue?: any }>} S
147
+ * @param {S} schema
148
+ * @returns {Record<string, any>} — getter/setter pairs, plus values() and reset()
149
+ */
150
+ export function createURLStore(schema) {
151
+ const keys = Object.keys(schema);
152
+ /** @type {Record<string, [Function, Function]>} */
153
+ const signals = {};
154
+ const store = {};
155
+
156
+ for (const key of keys) {
157
+ const { parser, defaultValue, ...rest } = schema[key];
158
+ const [get, set] = createURLSignal(key, parser, { defaultValue, ...rest });
159
+ signals[key] = [get, set];
160
+ store[key] = get;
161
+ store['set' + key.charAt(0).toUpperCase() + key.slice(1)] = set;
162
+ }
163
+
164
+ /** @returns {Record<string, any>} All current values as a plain object. */
165
+ store.values = () => {
166
+ const out = {};
167
+ for (const key of keys) out[key] = signals[key][0]();
168
+ return out;
169
+ };
170
+
171
+ /** Reset all params to their defaults. */
172
+ store.reset = () => {
173
+ batch(() => { for (const key of keys) signals[key][1](schema[key].defaultValue); });
174
+ };
175
+
176
+ return store;
177
+ }
@@ -0,0 +1,134 @@
1
+ import { createSignal, createEffect } from '../state/index.js';
2
+
3
+ /** @type {number} */
4
+ let nextId = 1;
5
+
6
+ /**
7
+ * Persistent bridge to a Web Worker with reactive state.
8
+ *
9
+ * Sends messages using `{ id, payload }` protocol and expects
10
+ * `{ id, result }` or `{ id, error }` responses.
11
+ *
12
+ * @param {Worker} worker
13
+ * @returns {{
14
+ * result: () => any,
15
+ * busy: () => boolean,
16
+ * error: () => any,
17
+ * send: (data: any) => number,
18
+ * terminate: () => void
19
+ * }}
20
+ *
21
+ * @example
22
+ * ```js
23
+ * const ws = createWorkerSignal(new Worker('./compute.js'));
24
+ * ws.send({ type: 'process', data: [1,2,3] });
25
+ * // ws.result() — last result from worker
26
+ * // ws.busy() — true while worker is processing
27
+ * // ws.error() — last error (if any)
28
+ * ```
29
+ */
30
+ export function createWorkerSignal(worker) {
31
+ const [result, setResult] = createSignal(undefined);
32
+ const [busy, setBusy] = createSignal(false);
33
+ const [error, setError] = createSignal(undefined);
34
+
35
+ /** @type {Set<number>} */
36
+ const pending = new Set();
37
+
38
+ function onMessage(e) {
39
+ const msg = e.data;
40
+ if (msg && typeof msg.id === 'number') {
41
+ pending.delete(msg.id);
42
+ if ('error' in msg) {
43
+ setError(msg.error);
44
+ } else {
45
+ setResult(msg.result);
46
+ setError(undefined);
47
+ }
48
+ if (pending.size === 0) setBusy(false);
49
+ }
50
+ }
51
+
52
+ function onError(e) {
53
+ setError(e.message || e);
54
+ pending.clear();
55
+ setBusy(false);
56
+ }
57
+
58
+ worker.addEventListener('message', onMessage);
59
+ worker.addEventListener('error', onError);
60
+
61
+ /**
62
+ * Send a message to the worker.
63
+ * @param {any} data
64
+ * @returns {number} message id
65
+ */
66
+ function send(data) {
67
+ const id = nextId++;
68
+ pending.add(id);
69
+ setBusy(true);
70
+ setError(undefined);
71
+ worker.postMessage({ id, payload: data });
72
+ return id;
73
+ }
74
+
75
+ /** Terminate the worker and clean up listeners. */
76
+ function terminate() {
77
+ worker.removeEventListener('message', onMessage);
78
+ worker.removeEventListener('error', onError);
79
+ worker.terminate();
80
+ pending.clear();
81
+ setBusy(false);
82
+ }
83
+
84
+ return { result, busy, error, send, terminate };
85
+ }
86
+
87
+ /**
88
+ * One-shot reactive computation via a Web Worker.
89
+ *
90
+ * When `input` is a signal getter, re-sends to the worker whenever
91
+ * the value changes. Returns a resource-like object.
92
+ *
93
+ * @param {Worker} worker
94
+ * @param {(() => any) | any} input - Signal getter or plain value
95
+ * @returns {{
96
+ * data: () => any,
97
+ * loading: () => boolean,
98
+ * error: () => any,
99
+ * refetch: () => void
100
+ * }}
101
+ *
102
+ * @example
103
+ * ```js
104
+ * const result = createWorkerQuery(new Worker('./heavy.js'), () => ({ data: largeArray() }));
105
+ * // result.data() — computed result
106
+ * // result.loading() — true while computing
107
+ * // result.error() — last error
108
+ * // result.refetch() — re-run with current input
109
+ * ```
110
+ */
111
+ export function createWorkerQuery(worker, input) {
112
+ const ws = createWorkerSignal(worker);
113
+ const isGetter = typeof input === 'function';
114
+
115
+ /** Resolve current input value and send to worker. */
116
+ function run() {
117
+ const value = isGetter ? input() : input;
118
+ ws.send(value);
119
+ }
120
+
121
+ // If input is reactive, track changes and re-send automatically.
122
+ if (isGetter) {
123
+ createEffect(() => { run(); });
124
+ } else {
125
+ run();
126
+ }
127
+
128
+ return {
129
+ data: ws.result,
130
+ loading: ws.busy,
131
+ error: ws.error,
132
+ refetch: run
133
+ };
134
+ }