snice 4.13.0 → 4.14.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 (253) hide show
  1. package/dist/cdn/accordion/snice-accordion.js +1 -1
  2. package/dist/cdn/accordion/snice-accordion.min.js +1 -1
  3. package/dist/cdn/alert/snice-alert.js +1 -1
  4. package/dist/cdn/alert/snice-alert.min.js +1 -1
  5. package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
  6. package/dist/cdn/app-tiles/snice-app-tiles.min.js +1 -1
  7. package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
  8. package/dist/cdn/audio-recorder/snice-audio-recorder.min.js +1 -1
  9. package/dist/cdn/avatar/snice-avatar.js +1 -1
  10. package/dist/cdn/avatar/snice-avatar.min.js +1 -1
  11. package/dist/cdn/badge/snice-badge.js +1 -1
  12. package/dist/cdn/badge/snice-badge.min.js +1 -1
  13. package/dist/cdn/banner/snice-banner.js +1 -1
  14. package/dist/cdn/banner/snice-banner.min.js +1 -1
  15. package/dist/cdn/book/snice-book.js +1 -1
  16. package/dist/cdn/book/snice-book.min.js +1 -1
  17. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
  18. package/dist/cdn/breadcrumbs/snice-breadcrumbs.min.js +1 -1
  19. package/dist/cdn/button/snice-button.js +1 -1
  20. package/dist/cdn/button/snice-button.min.js +1 -1
  21. package/dist/cdn/calendar/snice-calendar.js +1 -1
  22. package/dist/cdn/calendar/snice-calendar.min.js +1 -1
  23. package/dist/cdn/camera/snice-camera.js +1 -1
  24. package/dist/cdn/camera/snice-camera.min.js +1 -1
  25. package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
  26. package/dist/cdn/camera-annotate/snice-camera-annotate.min.js +1 -1
  27. package/dist/cdn/candlestick/snice-candlestick.js +1 -1
  28. package/dist/cdn/candlestick/snice-candlestick.min.js +1 -1
  29. package/dist/cdn/card/snice-card.js +1 -1
  30. package/dist/cdn/card/snice-card.min.js +1 -1
  31. package/dist/cdn/carousel/snice-carousel.js +1 -1
  32. package/dist/cdn/carousel/snice-carousel.min.js +1 -1
  33. package/dist/cdn/chart/snice-chart.js +1 -1
  34. package/dist/cdn/chart/snice-chart.min.js +1 -1
  35. package/dist/cdn/chat/snice-chat.js +1 -1
  36. package/dist/cdn/chat/snice-chat.min.js +1 -1
  37. package/dist/cdn/checkbox/snice-checkbox.js +1 -1
  38. package/dist/cdn/checkbox/snice-checkbox.min.js +1 -1
  39. package/dist/cdn/chip/snice-chip.js +1 -1
  40. package/dist/cdn/chip/snice-chip.min.js +1 -1
  41. package/dist/cdn/code-block/snice-code-block.js +2 -2
  42. package/dist/cdn/code-block/snice-code-block.js.map +1 -1
  43. package/dist/cdn/code-block/snice-code-block.min.js +2 -2
  44. package/dist/cdn/code-block/snice-code-block.min.js.map +1 -1
  45. package/dist/cdn/color-display/snice-color-display.js +1 -1
  46. package/dist/cdn/color-display/snice-color-display.min.js +1 -1
  47. package/dist/cdn/color-picker/snice-color-picker.js +1 -1
  48. package/dist/cdn/color-picker/snice-color-picker.min.js +1 -1
  49. package/dist/cdn/command-palette/snice-command-palette.js +1 -1
  50. package/dist/cdn/command-palette/snice-command-palette.min.js +1 -1
  51. package/dist/cdn/comments/snice-comments.js +1 -1
  52. package/dist/cdn/comments/snice-comments.min.js +1 -1
  53. package/dist/cdn/countdown/snice-countdown.js +1 -1
  54. package/dist/cdn/countdown/snice-countdown.min.js +1 -1
  55. package/dist/cdn/cropper/snice-cropper.js +1 -1
  56. package/dist/cdn/cropper/snice-cropper.min.js +1 -1
  57. package/dist/cdn/date-picker/snice-date-picker.js +1 -1
  58. package/dist/cdn/date-picker/snice-date-picker.min.js +1 -1
  59. package/dist/cdn/diff/snice-diff.js +1 -1
  60. package/dist/cdn/diff/snice-diff.min.js +1 -1
  61. package/dist/cdn/divider/snice-divider.js +1 -1
  62. package/dist/cdn/divider/snice-divider.min.js +1 -1
  63. package/dist/cdn/doc/snice-doc.js +1 -1
  64. package/dist/cdn/doc/snice-doc.min.js +1 -1
  65. package/dist/cdn/draw/snice-draw.js +1 -1
  66. package/dist/cdn/draw/snice-draw.min.js +1 -1
  67. package/dist/cdn/drawer/snice-drawer.js +1 -1
  68. package/dist/cdn/drawer/snice-drawer.min.js +1 -1
  69. package/dist/cdn/empty-state/snice-empty-state.js +1 -1
  70. package/dist/cdn/empty-state/snice-empty-state.min.js +1 -1
  71. package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
  72. package/dist/cdn/file-gallery/snice-file-gallery.min.js +1 -1
  73. package/dist/cdn/file-upload/snice-file-upload.js +1 -1
  74. package/dist/cdn/file-upload/snice-file-upload.min.js +1 -1
  75. package/dist/cdn/flip-card/snice-flip-card.js +1 -1
  76. package/dist/cdn/flip-card/snice-flip-card.min.js +1 -1
  77. package/dist/cdn/flow/snice-flow.js +1 -1
  78. package/dist/cdn/flow/snice-flow.min.js +1 -1
  79. package/dist/cdn/funnel/snice-funnel.js +1 -1
  80. package/dist/cdn/funnel/snice-funnel.min.js +1 -1
  81. package/dist/cdn/gantt/snice-gantt.js +1 -1
  82. package/dist/cdn/gantt/snice-gantt.min.js +1 -1
  83. package/dist/cdn/gauge/snice-gauge.js +1 -1
  84. package/dist/cdn/gauge/snice-gauge.min.js +1 -1
  85. package/dist/cdn/heatmap/snice-heatmap.js +1 -1
  86. package/dist/cdn/heatmap/snice-heatmap.min.js +1 -1
  87. package/dist/cdn/image/snice-image.js +1 -1
  88. package/dist/cdn/image/snice-image.min.js +1 -1
  89. package/dist/cdn/input/snice-input.js +1 -1
  90. package/dist/cdn/input/snice-input.min.js +1 -1
  91. package/dist/cdn/kanban/snice-kanban.js +1 -1
  92. package/dist/cdn/kanban/snice-kanban.min.js +1 -1
  93. package/dist/cdn/kpi/snice-kpi.js +1 -1
  94. package/dist/cdn/kpi/snice-kpi.min.js +1 -1
  95. package/dist/cdn/layout/snice-layout.js +1 -1
  96. package/dist/cdn/layout/snice-layout.min.js +1 -1
  97. package/dist/cdn/link/snice-link.js +1 -1
  98. package/dist/cdn/link/snice-link.min.js +1 -1
  99. package/dist/cdn/link-preview/snice-link-preview.js +1 -1
  100. package/dist/cdn/link-preview/snice-link-preview.min.js +1 -1
  101. package/dist/cdn/list/snice-list.js +1 -1
  102. package/dist/cdn/list/snice-list.min.js +1 -1
  103. package/dist/cdn/location/snice-location.js +1 -1
  104. package/dist/cdn/location/snice-location.min.js +1 -1
  105. package/dist/cdn/login/snice-login.js +1 -1
  106. package/dist/cdn/login/snice-login.min.js +1 -1
  107. package/dist/cdn/map/snice-map.js +1 -1
  108. package/dist/cdn/map/snice-map.min.js +1 -1
  109. package/dist/cdn/markdown/snice-markdown.js +1 -1
  110. package/dist/cdn/markdown/snice-markdown.min.js +1 -1
  111. package/dist/cdn/masonry/snice-masonry.js +1 -1
  112. package/dist/cdn/masonry/snice-masonry.min.js +1 -1
  113. package/dist/cdn/menu/snice-menu.js +1 -1
  114. package/dist/cdn/menu/snice-menu.min.js +1 -1
  115. package/dist/cdn/modal/snice-modal.js +1 -1
  116. package/dist/cdn/modal/snice-modal.min.js +1 -1
  117. package/dist/cdn/music-player/snice-music-player.js +1 -1
  118. package/dist/cdn/music-player/snice-music-player.min.js +1 -1
  119. package/dist/cdn/nav/snice-nav.js +1 -1
  120. package/dist/cdn/nav/snice-nav.min.js +1 -1
  121. package/dist/cdn/network-graph/snice-network-graph.js +1 -1
  122. package/dist/cdn/network-graph/snice-network-graph.min.js +1 -1
  123. package/dist/cdn/notification-center/snice-notification-center.js +1 -1
  124. package/dist/cdn/notification-center/snice-notification-center.min.js +1 -1
  125. package/dist/cdn/org-chart/snice-org-chart.js +1 -1
  126. package/dist/cdn/org-chart/snice-org-chart.min.js +1 -1
  127. package/dist/cdn/pagination/snice-pagination.js +1 -1
  128. package/dist/cdn/pagination/snice-pagination.min.js +1 -1
  129. package/dist/cdn/paint/snice-paint.js +1 -1
  130. package/dist/cdn/paint/snice-paint.min.js +1 -1
  131. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
  132. package/dist/cdn/pdf-viewer/snice-pdf-viewer.min.js +1 -1
  133. package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
  134. package/dist/cdn/podcast-player/snice-podcast-player.min.js +1 -1
  135. package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
  136. package/dist/cdn/pricing-table/snice-pricing-table.min.js +1 -1
  137. package/dist/cdn/progress/snice-progress.js +1 -1
  138. package/dist/cdn/progress/snice-progress.min.js +1 -1
  139. package/dist/cdn/qr-code/README.md +2 -2
  140. package/dist/cdn/qr-code/snice-qr-code.js +149 -20
  141. package/dist/cdn/qr-code/snice-qr-code.js.map +1 -1
  142. package/dist/cdn/qr-code/snice-qr-code.min.js +3 -3
  143. package/dist/cdn/qr-code/snice-qr-code.min.js.map +1 -1
  144. package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
  145. package/dist/cdn/qr-reader/snice-qr-reader.min.js +1 -1
  146. package/dist/cdn/radio/snice-radio.js +1 -1
  147. package/dist/cdn/radio/snice-radio.min.js +1 -1
  148. package/dist/cdn/rating/snice-rating.js +1 -1
  149. package/dist/cdn/rating/snice-rating.min.js +1 -1
  150. package/dist/cdn/recipe/snice-recipe.js +1 -1
  151. package/dist/cdn/recipe/snice-recipe.min.js +1 -1
  152. package/dist/cdn/runtime/snice-runtime.esm.js +4 -4
  153. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  154. package/dist/cdn/runtime/snice-runtime.esm.min.js +3 -3
  155. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  156. package/dist/cdn/runtime/snice-runtime.js +4 -4
  157. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  158. package/dist/cdn/runtime/snice-runtime.min.js +3 -3
  159. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  160. package/dist/cdn/sankey/snice-sankey.js +1 -1
  161. package/dist/cdn/sankey/snice-sankey.min.js +1 -1
  162. package/dist/cdn/select/snice-select.js +1 -1
  163. package/dist/cdn/select/snice-select.min.js +1 -1
  164. package/dist/cdn/skeleton/snice-skeleton.js +1 -1
  165. package/dist/cdn/skeleton/snice-skeleton.min.js +1 -1
  166. package/dist/cdn/slider/snice-slider.js +2 -2
  167. package/dist/cdn/slider/snice-slider.js.map +1 -1
  168. package/dist/cdn/slider/snice-slider.min.js +5 -5
  169. package/dist/cdn/slider/snice-slider.min.js.map +1 -1
  170. package/dist/cdn/sortable/snice-sortable.js +1 -1
  171. package/dist/cdn/sortable/snice-sortable.min.js +1 -1
  172. package/dist/cdn/sparkline/snice-sparkline.js +1 -1
  173. package/dist/cdn/sparkline/snice-sparkline.min.js +1 -1
  174. package/dist/cdn/spinner/snice-spinner.js +1 -1
  175. package/dist/cdn/spinner/snice-spinner.min.js +1 -1
  176. package/dist/cdn/split-pane/snice-split-pane.js +1 -1
  177. package/dist/cdn/split-pane/snice-split-pane.min.js +1 -1
  178. package/dist/cdn/spotlight/snice-spotlight.js +1 -1
  179. package/dist/cdn/spotlight/snice-spotlight.min.js +1 -1
  180. package/dist/cdn/spreadsheet/snice-spreadsheet.js +1 -1
  181. package/dist/cdn/spreadsheet/snice-spreadsheet.min.js +1 -1
  182. package/dist/cdn/stepper/snice-stepper.js +1 -1
  183. package/dist/cdn/stepper/snice-stepper.min.js +1 -1
  184. package/dist/cdn/switch/snice-switch.js +1 -1
  185. package/dist/cdn/switch/snice-switch.min.js +1 -1
  186. package/dist/cdn/table/snice-table.js +1 -1
  187. package/dist/cdn/table/snice-table.min.js +1 -1
  188. package/dist/cdn/tabs/snice-tabs.js +1 -1
  189. package/dist/cdn/tabs/snice-tabs.min.js +1 -1
  190. package/dist/cdn/tag-input/snice-tag-input.js +1 -1
  191. package/dist/cdn/tag-input/snice-tag-input.min.js +1 -1
  192. package/dist/cdn/terminal/snice-terminal.js +1 -1
  193. package/dist/cdn/terminal/snice-terminal.min.js +1 -1
  194. package/dist/cdn/testimonial/snice-testimonial.js +1 -1
  195. package/dist/cdn/testimonial/snice-testimonial.min.js +1 -1
  196. package/dist/cdn/textarea/snice-textarea.js +1 -1
  197. package/dist/cdn/textarea/snice-textarea.min.js +1 -1
  198. package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
  199. package/dist/cdn/time-range-picker/snice-time-range-picker.min.js +1 -1
  200. package/dist/cdn/timeline/snice-timeline.js +1 -1
  201. package/dist/cdn/timeline/snice-timeline.min.js +1 -1
  202. package/dist/cdn/timer/snice-timer.js +1 -1
  203. package/dist/cdn/timer/snice-timer.min.js +1 -1
  204. package/dist/cdn/toast/snice-toast.js +1 -1
  205. package/dist/cdn/toast/snice-toast.min.js +1 -1
  206. package/dist/cdn/tooltip/snice-tooltip.js +1 -1
  207. package/dist/cdn/tooltip/snice-tooltip.min.js +1 -1
  208. package/dist/cdn/tree/snice-tree.js +1 -1
  209. package/dist/cdn/tree/snice-tree.min.js +1 -1
  210. package/dist/cdn/treemap/snice-treemap.js +1 -1
  211. package/dist/cdn/treemap/snice-treemap.min.js +1 -1
  212. package/dist/cdn/video-player/snice-video-player.js +1 -1
  213. package/dist/cdn/video-player/snice-video-player.min.js +1 -1
  214. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
  215. package/dist/cdn/virtual-scroller/snice-virtual-scroller.min.js +1 -1
  216. package/dist/cdn/waterfall/snice-waterfall.js +1 -1
  217. package/dist/cdn/waterfall/snice-waterfall.min.js +1 -1
  218. package/dist/cdn/weather/snice-weather.js +1 -1
  219. package/dist/cdn/weather/snice-weather.min.js +1 -1
  220. package/dist/components/code-block/snice-code-block.js +1 -1
  221. package/dist/components/code-block/snice-code-block.js.map +1 -1
  222. package/dist/components/qr-code/qrcode.d.ts +1 -0
  223. package/dist/components/qr-code/qrcode.js +16 -8
  224. package/dist/components/qr-code/qrcode.js.map +1 -1
  225. package/dist/components/qr-code/snice-qr-code.d.ts +5 -2
  226. package/dist/components/qr-code/snice-qr-code.js +132 -11
  227. package/dist/components/qr-code/snice-qr-code.js.map +1 -1
  228. package/dist/components/qr-code/snice-qr-code.types.d.ts +3 -2
  229. package/dist/components/slider/snice-slider.js +1 -1
  230. package/dist/components/slider/snice-slider.js.map +1 -1
  231. package/dist/index.cjs +2 -2
  232. package/dist/index.esm.js +2 -2
  233. package/dist/index.iife.js +2 -2
  234. package/dist/symbols.cjs +1 -1
  235. package/dist/symbols.esm.js +1 -1
  236. package/dist/transitions.cjs +1 -1
  237. package/dist/transitions.esm.js +1 -1
  238. package/dist/types/request-options.d.ts +1 -1
  239. package/docs/ai/api.md +1 -1
  240. package/docs/ai/components/code-block.md +1 -1
  241. package/docs/ai/decorators.md +2 -2
  242. package/docs/ai/patterns.md +17 -6
  243. package/docs/code-block.md +1 -3
  244. package/docs/controllers.md +98 -391
  245. package/docs/elements.md +131 -117
  246. package/docs/events.md +74 -83
  247. package/docs/fetcher.md +64 -76
  248. package/docs/observe.md +13 -33
  249. package/docs/placards.md +6 -16
  250. package/docs/request-response.md +171 -693
  251. package/docs/routing.md +67 -136
  252. package/package.json +1 -1
  253. package/docs/migration-v2-to-v3.md +0 -569
@@ -1,8 +1,9 @@
1
1
  # Request/Response API Documentation
2
2
 
3
- Request/Response provides bidirectional request/response communication between elements and controllers using async generators.
3
+ Request/Response provides request/response communication between elements and controllers using async generators.
4
4
 
5
5
  ## Table of Contents
6
+ - [Why Request/Response?](#why-requestresponse)
6
7
  - [Basic Concept](#basic-concept)
7
8
  - [Request/Response Decorators](#requestresponse-decorators)
8
9
  - [Element-Side Requests](#element-side-requests)
@@ -11,15 +12,27 @@ Request/Response provides bidirectional request/response communication between e
11
12
  - [Error Handling](#error-handling)
12
13
  - [Advanced Patterns](#advanced-patterns)
13
14
 
15
+ ## Why Request/Response?
16
+
17
+ Components are **generic**. A `<product-card>` renders a card — it doesn't know or care whether its data comes from a REST API, a GraphQL endpoint, a WebSocket, or a test fixture. Controllers are **specific**. They wire a particular data source, API, or business rule to a generic component.
18
+
19
+ The `@request`/`@respond` pattern keeps this separation clean:
20
+
21
+ - **The element says *what* it needs** (e.g., "I need product data for this ID") without knowing *how* to get it.
22
+ - **The controller decides *how*** — makes the API call, applies business logic, caches results, whatever is needed.
23
+ - **Swapping controllers changes behavior without touching the component.** Attach a mock controller for tests, a real API controller in production, or a WebSocket controller for live updates — the element is the same.
24
+
25
+ This makes components reusable across projects and testable in isolation. An element with `@request('fetch-product')` works with *any* controller that `@respond`s to `'fetch-product'` — no imports, no interfaces, no coupling.
26
+
14
27
  ## Basic Concept
15
28
 
16
- Request/Response enables a request/response pattern between elements and their controllers:
29
+ Request/Response enables a single request/response round-trip between elements and their controllers:
17
30
 
18
- 1. **Element** sends a request using `yield`
19
- 2. **Controller** receives the request and returns a response
20
- 3. **Element** receives the response and can process it
31
+ 1. **Element** yields a request payload — "here's what I need"
32
+ 2. **Controller** receives the payload and returns a response — "here's the data"
33
+ 3. **Element** receives the response and updates its visual state
21
34
 
22
- This pattern is implemented using async generators and custom events.
35
+ This pattern is implemented using async generators and custom events. Each `@request` method supports **one yield** per invocation — the generator yields a request payload, the controller responds, and the generator receives the response.
23
36
 
24
37
  ## Request/Response Decorators
25
38
 
@@ -27,7 +40,7 @@ This pattern is implemented using async generators and custom events.
27
40
 
28
41
  ```typescript
29
42
  function request(requestName: string, options?: RequestOptions): MethodDecorator
30
- function response(responseName: string, options?: RespondOptions): MethodDecorator
43
+ function respond(requestName: string, options?: RespondOptions): MethodDecorator
31
44
 
32
45
  interface RequestOptions extends EventInit {
33
46
  timeout?: number; // Response timeout in ms (default: 120000ms = 2 minutes)
@@ -41,812 +54,277 @@ interface RespondOptions {
41
54
  throttle?: number; // Throttle responses by specified ms
42
55
  }
43
56
 
44
- // Type helper to eliminate TypeScript warnings in request generators
45
- // Use this for method signatures - it handles both the generator implementation and promise return
46
- type Response<T> = AsyncGenerator<any, T, any> | Promise<T>;
57
+ // Recommended type helper for request generator return types:
58
+ type RequestResult<T> = AsyncGenerator<any, T, any> | Promise<T>;
59
+ // Define this in your project it satisfies both the generator and the caller
47
60
  ```
48
61
 
49
62
  #### Response Debounce/Throttle
50
63
 
51
- Response handlers can also be debounced or throttled to prevent excessive processing:
64
+ Response handlers can be debounced or throttled:
52
65
 
53
66
  ```typescript
54
- @controller('heavy-processing-controller')
67
+ @controller('processing-controller')
55
68
  class ProcessingController implements IController {
69
+ element: HTMLElement | null = null;
70
+ async attach() {}
71
+ async detach() {}
56
72
 
57
- @respond('@api/expensive-calculation', { debounce: 1000 })
58
- async calculateResults(params: any) {
59
- // Debounced by 1 second - rapid requests will only trigger the latest
60
- return await performExpensiveCalculation(params);
73
+ @respond('search', { debounce: 300 })
74
+ async handleSearch(query: { term: string }) {
75
+ return await fetch(`/api/search?q=${encodeURIComponent(query.term)}`).then(r => r.json());
61
76
  }
62
77
 
63
- @respond('@api/real-time-updates', { throttle: 500 })
64
- async handleUpdates(data: any) {
65
- // Throttled to max 2 requests per second
66
- return await processUpdate(data);
78
+ @respond('analytics', { throttle: 1000 })
79
+ async handleAnalytics(event: any) {
80
+ return await fetch('/api/track', { method: 'POST', body: JSON.stringify(event) });
67
81
  }
68
82
  }
69
83
  ```
70
84
 
71
85
  ## Element-Side Requests
72
86
 
73
- Elements use async generators to make requests:
87
+ Elements use async generators to make requests. The element stays visual — it yields data up to the controller and renders the response:
74
88
 
75
89
  ```typescript
76
- import { element, request, Response, render, html } from 'snice';
90
+ import { element, request, property, render, html } from 'snice';
91
+
92
+ type RequestResult<T> = AsyncGenerator<any, T, any> | Promise<T>;
77
93
 
78
- @element('user-card')
79
- class UserCard extends HTMLElement {
80
- userId = 123;
94
+ @element('product-card')
95
+ class ProductCard extends HTMLElement {
96
+ @property() productId = '';
97
+ @property() name = '';
98
+ @property() price = '';
99
+
100
+ @request('fetch-product')
101
+ async *loadProduct(): RequestResult<void> {
102
+ const product = await (yield { id: this.productId });
103
+ this.name = product.name;
104
+ this.price = product.price;
105
+ }
81
106
 
82
107
  @render()
83
108
  renderContent() {
84
109
  return html`
85
- <div class="user-info">
86
- <button @click=${this.loadUser}>Load User</button>
87
- <div class="content"></div>
110
+ <div class="card">
111
+ <h3>${this.name || 'Loading...'}</h3>
112
+ <p>${this.price}</p>
113
+ <button @click=${this.loadProduct}>Refresh</button>
88
114
  </div>
89
115
  `;
90
116
  }
91
-
92
- @request('fetch-user')
93
- async *fetchUserData(): Response<{ success: boolean; user: any }> {
94
- // Yield sends the request, await waits for response
95
- const user = await (yield { userId: this.userId });
96
-
97
- // Process the response
98
- this.displayUser(user);
99
-
100
- // Return final value (optional)
101
- return { success: true, user };
102
- }
103
-
104
- async loadUser() {
105
- try {
106
- const result = await this.fetchUserData();
107
- console.log('Load complete:', result);
108
- } catch (error) {
109
- console.error('Failed to load user:', error);
110
- }
111
- }
112
-
113
- displayUser(user: any) {
114
- const content = this.shadowRoot?.querySelector('.content');
115
- if (content) {
116
- content.innerHTML = `
117
- <h3>${user.name}</h3>
118
- <p>${user.email}</p>
119
- `;
120
- }
121
- }
122
117
  }
123
118
  ```
124
119
 
125
- ### Multiple Yields
126
-
127
- Elements can yield multiple times in a single channel:
128
-
129
- ```typescript
130
- @element('multi-request')
131
- class MultiRequest extends HTMLElement {
132
- @request('multi-data')
133
- async *fetchMultipleData() {
134
- // First request
135
- const userData = await (yield { type: 'user', id: 1 });
136
- console.log('Got user:', userData);
137
-
138
- // Second request based on first response
139
- const postsData = await (yield { type: 'posts', userId: userData.id });
140
- console.log('Got posts:', postsData);
141
-
142
- // Third request
143
- const commentsData = await (yield { type: 'comments', postIds: postsData.map((p: any) => p.id) });
144
- console.log('Got comments:', commentsData);
145
-
146
- // Return combined result
147
- return {
148
- user: userData,
149
- posts: postsData,
150
- comments: commentsData
151
- };
152
- }
153
-
154
- @render()
155
- renderContent() {
156
- return html`<button @click=${this.fetchData}>Fetch All Data</button>`;
157
- }
158
-
159
- async fetchData() {
160
- const result = await this.fetchMultipleData();
161
- console.log('All data loaded:', result);
162
- }
163
- }
164
- ```
120
+ **How it works:**
121
+ 1. `yield { id: this.productId }` dispatches a bubbling custom event with the payload
122
+ 2. A `@respond('fetch-product')` handler (typically in a controller) catches it and returns data
123
+ 3. `await (yield ...)` resolves with the response
124
+ 4. The element updates its properties, triggering a re-render
165
125
 
166
126
  ## Controller-Side Responses
167
127
 
168
- Controllers handle requests and provide responses:
128
+ Controllers handle requests — this is where business logic, API calls, and data management belong:
169
129
 
170
130
  ```typescript
171
131
  import { controller, respond, IController } from 'snice';
172
132
 
173
- @controller('user-controller')
174
- class UserController implements IController {
133
+ @controller('product-controller')
134
+ class ProductController implements IController {
175
135
  element: HTMLElement | null = null;
136
+ async attach() {}
137
+ async detach() {}
176
138
 
177
- async attach(element: HTMLElement) {
178
- console.log('User controller attached');
179
- }
180
-
181
- async detach(element: HTMLElement) {
182
- console.log('User controller detached');
183
- }
184
-
185
- @respond('fetch-user')
186
- async handleFetchUser(request: { userId: number }) {
187
- // Simulate API call
188
- const response = await fetch(`/api/users/${request.userId}`);
189
- const user = await response.json();
190
-
191
- // Return response to element
192
- return user;
193
- }
194
-
195
- @respond('multi-data')
196
- async handleMultiData(request: any) {
197
- switch (request.type) {
198
- case 'user':
199
- return await this.fetchUser(request.id);
200
- case 'posts':
201
- return await this.fetchPosts(request.userId);
202
- case 'comments':
203
- return await this.fetchComments(request.postIds);
204
- default:
205
- throw new Error(`Unknown request type: ${request.type}`);
206
- }
207
- }
208
-
209
- private async fetchUser(id: number) {
210
- // Simulate API call
211
- return { id, name: 'John Doe', email: 'john@example.com' };
212
- }
213
-
214
- private async fetchPosts(userId: number) {
215
- // Simulate API call
216
- return [
217
- { id: 1, userId, title: 'Post 1' },
218
- { id: 2, userId, title: 'Post 2' }
219
- ];
220
- }
221
-
222
- private async fetchComments(postIds: number[]) {
223
- // Simulate API call
224
- return postIds.flatMap(postId => [
225
- { id: 1, postId, text: 'Comment 1' },
226
- { id: 2, postId, text: 'Comment 2' }
227
- ]);
139
+ @respond('fetch-product')
140
+ async handleFetchProduct(request: { id: string }) {
141
+ const response = await fetch(`/api/products/${request.id}`);
142
+ return await response.json();
228
143
  }
229
144
  }
230
145
  ```
231
146
 
232
- ## Request/Response Options
233
-
234
- ### RequestOptions
147
+ **Architecture:** Elements never call `fetch()` or manage data directly. They yield requests upward and render whatever comes back. Controllers own the data layer.
235
148
 
236
- ```typescript
237
- interface RequestOptions extends EventInit {
238
- timeout?: number; // Response timeout in ms (default: 120000ms = 2 minutes)
239
- discoveryTimeout?: number; // Handler discovery timeout in ms (default: 50ms)
240
- debounce?: number; // Debounce requests by specified ms
241
- throttle?: number; // Throttle requests by specified ms
242
- }
243
- ```
149
+ ## Request/Response Options
244
150
 
245
- #### Timeout Behavior (IMPORTANT)
151
+ ### Timeout Behavior
246
152
 
247
- The timeout system has **two separate timeouts** for different phases:
153
+ The timeout system has **two separate timeouts**:
248
154
 
249
- - **Discovery timeout** (`discoveryTimeout`): 50ms (default) - Fast timeout to find a handler
250
- - **Response timeout** (`timeout`): 2 minutes (default) - Total time allowed for the request
155
+ - **Discovery timeout** (`discoveryTimeout`): 50ms default finds a handler quickly
156
+ - **Response timeout** (`timeout`): 2 minutes default total time for the response
251
157
 
252
158
  ```typescript
253
- @request('@api/heavy-processing', {
254
- discoveryTimeout: 50, // 50ms to find handler (fast)
255
- timeout: 30000 // 30s total timeout for processing
159
+ @request('heavy-computation', {
160
+ discoveryTimeout: 50, // 50ms to find handler
161
+ timeout: 30000 // 30s for actual processing
256
162
  })
257
- async *processData() {
258
- // Will timeout in 50ms if no handler exists
259
- // Will timeout in 30s total if processing takes too long
260
- const result = await (yield data);
261
- return result;
262
- }
263
- ```
264
-
265
- **Why two timeouts?**
266
- - **Discovery**: Should be very fast (dozens of milliseconds) - just finding if anyone can handle the request
267
- - **Response**: Should be human-scale (seconds/minutes) - actual work takes time
268
-
269
- #### Debounce Support
270
-
271
- Prevents rapid successive requests by delaying execution:
272
-
273
- ```typescript
274
- @request('@api/search', { debounce: 300 })
275
- async *search() {
276
- // Debounced by 300ms - rapid calls will cancel previous ones
277
- const results = await (yield query);
278
- return results;
163
+ async *compute(): RequestResult<any> {
164
+ return await (yield data);
279
165
  }
280
166
  ```
281
167
 
282
- #### Throttle Support
283
-
284
- Limits request frequency to maximum rate:
168
+ ### Debounce/Throttle
285
169
 
286
170
  ```typescript
287
- @request('@api/analytics', { throttle: 1000 })
288
- async *trackEvent() {
289
- // Throttled to max 1 request per second
290
- const response = await (yield eventData);
291
- return response;
171
+ // Debounce: wait for typing to stop before searching
172
+ @request('search', { debounce: 300 })
173
+ async *search(): RequestResult<any[]> {
174
+ return await (yield { query: this.searchTerm });
292
175
  }
293
- ```
294
176
 
295
- ### Timeout Configuration
296
-
297
- ```typescript
298
- @element('timeout-example')
299
- class TimeoutExample extends HTMLElement {
300
- // Quick discovery, short total timeout for fast operations
301
- @request('quick-data', {
302
- discoveryTimeout: 25, // Very fast discovery
303
- timeout: 1000 // 1 second total
304
- })
305
- async *fetchQuickData() {
306
- const data = await (yield { quick: true });
307
- return data;
308
- }
309
-
310
- // Standard discovery, longer timeout for slow operations
311
- @request('slow-data', {
312
- discoveryTimeout: 50, // Default discovery
313
- timeout: 30000 // 30 seconds total
314
- })
315
- async *fetchSlowData() {
316
- const data = await (yield { slow: true });
317
- return data;
318
- }
319
-
320
- // Use defaults (50ms discovery, 2 minutes total)
321
- @request('default-data')
322
- async *fetchDefaultData() {
323
- const data = await (yield { default: true });
324
- return data;
325
- }
326
-
327
- // Custom event options with timeouts
328
- @request('private-data', {
329
- discoveryTimeout: 100, // Slower discovery
330
- timeout: 60000, // 1 minute total
331
- bubbles: false, // Don't bubble
332
- cancelable: true // Can be canceled
333
- })
334
- async *fetchPrivateData() {
335
- const data = await (yield { private: true });
336
- return data;
337
- }
338
-
339
- @render()
340
- renderContent() {
341
- return html`<div>Timeout examples</div>`;
342
- }
177
+ // Throttle: limit analytics to 1 per second
178
+ @request('track', { throttle: 1000 })
179
+ async *trackEvent(): RequestResult<void> {
180
+ await (yield { event: 'scroll', position: window.scrollY });
343
181
  }
344
182
  ```
345
183
 
346
184
  ## Error Handling
347
185
 
348
- ### Handling Timeouts
186
+ ### Element-Side
349
187
 
350
188
  ```typescript
351
- @element('timeout-handler')
352
- class TimeoutHandler extends HTMLElement {
353
- @request('data', {
354
- discoveryTimeout: 50,
355
- timeout: 5000
356
- })
357
- async *fetchData() {
358
- try {
359
- const data = await (yield { request: 'data' });
360
- return { success: true, data };
361
- } catch (error: any) {
362
- // Handle different types of timeout errors
363
- if (error.message.includes('timed out after') && error.message.includes('no handler found')) {
364
- console.error('No handler found for request');
365
- return { success: false, error: 'no_handler' };
366
- } else if (error.message.includes('timed out after')) {
367
- console.error('Request processing timed out');
368
- return { success: false, error: 'timeout' };
369
- }
370
- throw error;
371
- }
372
- }
189
+ @element('safe-loader')
190
+ class SafeLoader extends HTMLElement {
191
+ @property() error = '';
192
+ @property() data: any = null;
373
193
 
374
- async loadData() {
194
+ @request('load-data', { timeout: 5000 })
195
+ async *loadData(): RequestResult<void> {
375
196
  try {
376
- const result = await this.fetchData();
377
- if (!result.success) {
378
- this.showError('Failed to load data');
197
+ this.data = await (yield { id: this.dataId });
198
+ this.error = '';
199
+ } catch (err: any) {
200
+ if (err.message.includes('no handler found')) {
201
+ this.error = 'Service unavailable';
202
+ } else if (err.message.includes('timed out')) {
203
+ this.error = 'Request timed out';
204
+ } else {
205
+ this.error = err.message;
379
206
  }
380
- } catch (error) {
381
- this.showError('Unexpected error');
382
207
  }
383
208
  }
384
209
 
385
- showError(message: string) {
386
- console.error(message);
387
- }
388
-
389
210
  @render()
390
211
  renderContent() {
391
- return html`<button @click=${this.loadData}>Load Data</button>`;
392
- }
393
- }
394
- ```
395
-
396
- ### Controller Error Handling
397
-
398
- ```typescript
399
- @controller('error-controller')
400
- class ErrorController implements IController {
401
- element: HTMLElement | null = null;
402
-
403
- async attach(element: HTMLElement) {}
404
- async detach(element: HTMLElement) {}
405
-
406
- @respond('risky-operation')
407
- async handleRiskyOperation(request: any) {
408
- try {
409
- // Validate request
410
- if (!request.id) {
411
- throw new Error('ID is required');
412
- }
413
-
414
- // Perform operation
415
- const result = await this.performOperation(request.id);
416
-
417
- return { success: true, result };
418
- } catch (error: any) {
419
- // Return error info instead of throwing
420
- return {
421
- success: false,
422
- error: error.message,
423
- code: error.code || 'UNKNOWN_ERROR'
424
- };
425
- }
426
- }
427
-
428
- private async performOperation(id: string) {
429
- // Simulate operation that might fail
430
- if (Math.random() > 0.5) {
431
- throw new Error('Random failure');
432
- }
433
- return { id, processed: true };
212
+ return html`
213
+ <if ${this.error}>
214
+ <div class="error">${this.error}</div>
215
+ </if>
216
+ <if ${this.data}>
217
+ <div class="content">${this.data.title}</div>
218
+ </if>
219
+ `;
434
220
  }
435
221
  }
436
222
  ```
437
223
 
438
- ## Advanced Patterns
439
-
440
- ### Authentication Request/Response
224
+ ### Controller-Side
441
225
 
442
226
  ```typescript
443
- // Element side
444
- @element('protected-content')
445
- class ProtectedContent extends HTMLElement {
446
- @request('authenticate')
447
- async *authenticate() {
448
- // Send credentials
449
- const authResult = await (yield {
450
- username: 'user@example.com',
451
- password: 'secret'
452
- });
453
-
454
- if (!authResult.success) {
455
- throw new Error(authResult.error);
456
- }
457
-
458
- // Store token
459
- localStorage.setItem('token', authResult.token);
460
-
461
- return authResult;
462
- }
463
-
464
- @request('fetch-protected')
465
- async *fetchProtectedData() {
466
- const token = localStorage.getItem('token');
467
-
468
- if (!token) {
469
- // Need to authenticate first
470
- await this.authenticate();
471
- }
472
-
473
- // Fetch with token
474
- const data = await (yield {
475
- resource: 'protected',
476
- token: localStorage.getItem('token')
477
- });
478
-
479
- return data;
480
- }
481
-
482
- @render()
483
- renderContent() {
484
- return html`<button @click=${this.loadProtected}>Load Protected Data</button>`;
485
- }
486
-
487
- async loadProtected() {
488
- try {
489
- const data = await this.fetchProtectedData();
490
- console.log('Protected data:', data);
491
- } catch (error) {
492
- console.error('Failed to load:', error);
493
- }
494
- }
495
- }
496
-
497
- // Controller side
498
- @controller('auth-controller')
499
- class AuthController implements IController {
227
+ @controller('resilient-controller')
228
+ class ResilientController implements IController {
500
229
  element: HTMLElement | null = null;
501
- private tokens = new Map<string, any>();
502
-
503
- async attach(element: HTMLElement) {}
504
- async detach(element: HTMLElement) {}
505
-
506
- @respond('authenticate')
507
- async handleAuth(credentials: any) {
508
- // Validate credentials
509
- if (credentials.username === 'user@example.com' &&
510
- credentials.password === 'secret') {
511
-
512
- const token = this.generateToken();
513
- const user = { id: 1, name: 'User' };
514
-
515
- this.tokens.set(token, user);
230
+ async attach() {}
231
+ async detach() {}
516
232
 
517
- return {
518
- success: true,
519
- token,
520
- user
521
- };
233
+ @respond('load-data')
234
+ async handleLoadData(request: { id: string }) {
235
+ if (!request.id) {
236
+ throw new Error('ID is required');
522
237
  }
523
238
 
524
- return {
525
- success: false,
526
- error: 'Invalid credentials'
527
- };
528
- }
529
-
530
- @respond('fetch-protected')
531
- async handleFetchProtected(request: any) {
532
- // Validate token
533
- const user = this.tokens.get(request.token);
534
-
535
- if (!user) {
536
- throw new Error('Invalid or expired token');
239
+ const response = await fetch(`/api/data/${request.id}`);
240
+ if (!response.ok) {
241
+ throw new Error(`API error: ${response.status}`);
537
242
  }
538
243
 
539
- // Return protected data
540
- return {
541
- resource: request.resource,
542
- data: { secret: 'Protected information' },
543
- user
544
- };
545
- }
546
-
547
- private generateToken() {
548
- return Math.random().toString(36).substring(2);
244
+ return await response.json();
549
245
  }
550
246
  }
551
247
  ```
552
248
 
553
- ### Streaming Data Request/Response
554
-
555
- ```typescript
556
- // Element side
557
- @element('data-streamer')
558
- class DataStreamer extends HTMLElement {
559
- private items: any[] = [];
560
-
561
- @request('stream-data')
562
- async *streamData() {
563
- let hasMore = true;
564
- let page = 1;
565
-
566
- while (hasMore) {
567
- // Request next page
568
- const response = await (yield {
569
- page,
570
- pageSize: 10
571
- });
572
-
573
- // Add items to list
574
- this.items.push(...response.items);
575
- this.renderItems();
576
-
577
- // Check if more pages available
578
- hasMore = response.hasMore;
579
- page++;
580
- }
581
-
582
- return {
583
- totalItems: this.items.length,
584
- complete: true
585
- };
586
- }
587
-
588
- renderItems() {
589
- const container = this.shadowRoot?.querySelector('.items');
590
- if (container) {
591
- container.innerHTML = this.items
592
- .map(item => `<div>${item.name}</div>`)
593
- .join('');
594
- }
595
- }
596
-
597
- @render()
598
- renderContent() {
599
- return html`
600
- <button @click=${this.loadAllData}>Load All Data</button>
601
- <div class="items"></div>
602
- `;
603
- }
604
-
605
- async loadAllData() {
606
- const result = await this.streamData();
607
- console.log(`Loaded ${result.totalItems} items`);
608
- }
609
- }
610
-
611
- // Controller side
612
- @controller('stream-controller')
613
- class StreamController implements IController {
614
- element: HTMLElement | null = null;
615
- private allData: any[] = Array.from({ length: 35 }, (_, i) => ({
616
- id: i + 1,
617
- name: `Item ${i + 1}`
618
- }));
619
-
620
- async attach(element: HTMLElement) {}
621
- async detach(element: HTMLElement) {}
622
-
623
- @respond('stream-data')
624
- async handleStreamData(request: { page: number; pageSize: number }) {
625
- // Simulate delay
626
- await new Promise(resolve => setTimeout(resolve, 500));
627
-
628
- // Calculate pagination
629
- const start = (request.page - 1) * request.pageSize;
630
- const end = start + request.pageSize;
631
-
632
- const items = this.allData.slice(start, end);
633
- const hasMore = end < this.allData.length;
634
-
635
- return {
636
- items,
637
- hasMore,
638
- page: request.page,
639
- totalPages: Math.ceil(this.allData.length / request.pageSize)
640
- };
641
- }
642
- }
643
- ```
249
+ ## Advanced Patterns
644
250
 
645
- ### Cached Request/Response
251
+ ### Cached Responses
646
252
 
647
253
  ```typescript
648
254
  @controller('cached-controller')
649
255
  class CachedController implements IController {
650
256
  element: HTMLElement | null = null;
651
257
  private cache = new Map<string, { data: any; timestamp: number }>();
652
- private cacheTimeout = 60000; // 1 minute
258
+ private ttl = 60000; // 1 minute
653
259
 
654
- async attach(element: HTMLElement) {}
655
- async detach(element: HTMLElement) {}
260
+ async attach() {}
261
+ async detach() {}
656
262
 
657
263
  @respond('fetch-cached')
658
- async handleFetchCached(request: { key: string; forceRefresh?: boolean }) {
659
- const cacheKey = request.key;
660
- const cached = this.cache.get(cacheKey);
661
-
662
- // Check cache validity
663
- if (!request.forceRefresh && cached) {
664
- const age = Date.now() - cached.timestamp;
665
- if (age < this.cacheTimeout) {
666
- console.log(`Returning cached data for ${cacheKey}`);
667
- return {
668
- data: cached.data,
669
- fromCache: true,
670
- age
671
- };
672
- }
264
+ async handleFetch(request: { key: string; forceRefresh?: boolean }) {
265
+ const cached = this.cache.get(request.key);
266
+ if (!request.forceRefresh && cached && Date.now() - cached.timestamp < this.ttl) {
267
+ return { data: cached.data, fromCache: true };
673
268
  }
674
269
 
675
- // Fetch fresh data
676
- console.log(`Fetching fresh data for ${cacheKey}`);
677
- const freshData = await this.fetchFreshData(cacheKey);
678
-
679
- // Update cache
680
- this.cache.set(cacheKey, {
681
- data: freshData,
682
- timestamp: Date.now()
683
- });
684
-
685
- return {
686
- data: freshData,
687
- fromCache: false,
688
- age: 0
689
- };
690
- }
691
-
692
- private async fetchFreshData(key: string) {
693
- // Simulate API call
694
- await new Promise(resolve => setTimeout(resolve, 1000));
695
- return { key, value: Math.random(), timestamp: Date.now() };
270
+ const data = await fetch(`/api/${request.key}`).then(r => r.json());
271
+ this.cache.set(request.key, { data, timestamp: Date.now() });
272
+ return { data, fromCache: false };
696
273
  }
697
274
  }
698
275
  ```
699
276
 
700
- ### Bidirectional Updates
701
-
702
- ```typescript
703
- // Element that can both request and be updated
704
- import { element, property, query, request, watch, render, html } from 'snice';
277
+ ### Subscription Pattern
705
278
 
706
- @element('live-data')
707
- class LiveData extends HTMLElement {
708
- private updateInterval?: number;
279
+ Use `@request` for one-time fetches and `@dispatch` + `@on` for ongoing updates:
709
280
 
710
- @property()
711
- status = 'Disconnected';
281
+ ```typescript
282
+ // Element: visual, subscribes to updates
283
+ @element('live-ticker')
284
+ class LiveTicker extends HTMLElement {
285
+ @property() price = '0.00';
286
+ @property() symbol = 'BTC';
712
287
 
713
- @query('.status')
714
- statusDiv?: HTMLElement;
288
+ @request('subscribe-ticker')
289
+ async *subscribe(): RequestResult<void> {
290
+ await (yield { symbol: this.symbol });
291
+ }
715
292
 
716
- @query('.data')
717
- dataDiv?: HTMLElement;
293
+ @on('ticker-update')
294
+ onUpdate(e: CustomEvent) {
295
+ this.price = e.detail.price;
296
+ }
718
297
 
719
298
  @render()
720
299
  renderContent() {
721
300
  return html`
722
- <div class="status">${this.status}</div>
723
- <div class="data"></div>
724
- <button @click=${this.connect}>Connect</button>
725
- <button @click=${this.disconnect}>Disconnect</button>
301
+ <span class="symbol">${this.symbol}</span>
302
+ <span class="price">${this.price}</span>
726
303
  `;
727
304
  }
728
-
729
- @request('subscribe')
730
- async *subscribe() {
731
- // Send subscription request
732
- const subscription = await (yield {
733
- subscribe: true,
734
- events: ['update', 'status']
735
- });
736
-
737
- if (subscription.success) {
738
- this.status = 'Connected'; // @watch will handle UI update
739
-
740
- // Start polling for updates
741
- this.startPolling();
742
- }
743
-
744
- return subscription;
745
- }
746
-
747
- @request('poll-updates')
748
- async *pollForUpdates() {
749
- const updates = await (yield { poll: true });
750
-
751
- if (updates && updates.length > 0) {
752
- this.processUpdates(updates);
753
- }
754
-
755
- return { processed: updates.length };
756
- }
757
-
758
- async connect() {
759
- await this.subscribe();
760
- }
761
-
762
- disconnect() {
763
- this.stopPolling();
764
- this.status = 'Disconnected';
765
- }
766
-
767
- startPolling() {
768
- this.updateInterval = setInterval(async () => {
769
- await this.pollForUpdates();
770
- }, 2000);
771
- }
772
-
773
- stopPolling() {
774
- if (this.updateInterval) {
775
- clearInterval(this.updateInterval);
776
- this.updateInterval = undefined;
777
- }
778
- }
779
-
780
- processUpdates(updates: any[]) {
781
- if (this.dataDiv) {
782
- updates.forEach(update => {
783
- const entry = document.createElement('div');
784
- entry.textContent = `${update.type}: ${update.value}`;
785
- this.dataDiv!.appendChild(entry);
786
- });
787
- }
788
- }
789
-
790
- @watch('status')
791
- updateStatus() {
792
- if (this.statusDiv) {
793
- this.statusDiv.textContent = this.status;
794
- }
795
- }
796
-
797
- disconnectedCallback() {
798
- super.disconnectedCallback?.();
799
- this.stopPolling();
800
- }
801
305
  }
802
306
 
803
- // Controller that manages subscriptions
804
- @controller('subscription-controller')
805
- class SubscriptionController implements IController {
307
+ // Controller: manages WebSocket, dispatches updates
308
+ @controller('ticker-controller')
309
+ class TickerController implements IController {
806
310
  element: HTMLElement | null = null;
807
- private subscribers = new Set<string>();
808
- private updates: any[] = [];
809
-
810
- async attach(element: HTMLElement) {
811
- // Generate updates periodically
812
- setInterval(() => {
813
- this.updates.push({
814
- type: 'update',
815
- value: Math.random(),
816
- timestamp: Date.now()
817
- });
818
- }, 3000);
819
- }
311
+ private ws?: WebSocket;
820
312
 
821
- async detach(element: HTMLElement) {
822
- this.subscribers.clear();
823
- }
824
-
825
- @respond('subscribe')
826
- handleSubscribe(request: any) {
827
- if (request.subscribe) {
828
- const id = Math.random().toString(36);
829
- this.subscribers.add(id);
830
-
831
- return {
832
- success: true,
833
- subscriptionId: id,
834
- events: request.events
835
- };
836
- }
313
+ async attach() {}
837
314
 
838
- return { success: false };
315
+ async detach() {
316
+ this.ws?.close();
839
317
  }
840
318
 
841
- @respond('poll-updates')
842
- handlePollUpdates(request: any) {
843
- if (!request.poll) return [];
844
-
845
- // Return and clear updates
846
- const updates = [...this.updates];
847
- this.updates = [];
848
-
849
- return updates;
319
+ @respond('subscribe-ticker')
320
+ async handleSubscribe(request: { symbol: string }) {
321
+ this.ws = new WebSocket(`wss://api.example.com/ticker/${request.symbol}`);
322
+ this.ws.onmessage = (msg) => {
323
+ this.element?.dispatchEvent(new CustomEvent('ticker-update', {
324
+ detail: JSON.parse(msg.data)
325
+ }));
326
+ };
327
+ return { subscribed: true };
850
328
  }
851
329
  }
852
330
  ```