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.
- package/dist/cdn/accordion/snice-accordion.js +1 -1
- package/dist/cdn/accordion/snice-accordion.min.js +1 -1
- package/dist/cdn/alert/snice-alert.js +1 -1
- package/dist/cdn/alert/snice-alert.min.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.min.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.min.js +1 -1
- package/dist/cdn/avatar/snice-avatar.js +1 -1
- package/dist/cdn/avatar/snice-avatar.min.js +1 -1
- package/dist/cdn/badge/snice-badge.js +1 -1
- package/dist/cdn/badge/snice-badge.min.js +1 -1
- package/dist/cdn/banner/snice-banner.js +1 -1
- package/dist/cdn/banner/snice-banner.min.js +1 -1
- package/dist/cdn/book/snice-book.js +1 -1
- package/dist/cdn/book/snice-book.min.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.min.js +1 -1
- package/dist/cdn/button/snice-button.js +1 -1
- package/dist/cdn/button/snice-button.min.js +1 -1
- package/dist/cdn/calendar/snice-calendar.js +1 -1
- package/dist/cdn/calendar/snice-calendar.min.js +1 -1
- package/dist/cdn/camera/snice-camera.js +1 -1
- package/dist/cdn/camera/snice-camera.min.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.min.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.min.js +1 -1
- package/dist/cdn/card/snice-card.js +1 -1
- package/dist/cdn/card/snice-card.min.js +1 -1
- package/dist/cdn/carousel/snice-carousel.js +1 -1
- package/dist/cdn/carousel/snice-carousel.min.js +1 -1
- package/dist/cdn/chart/snice-chart.js +1 -1
- package/dist/cdn/chart/snice-chart.min.js +1 -1
- package/dist/cdn/chat/snice-chat.js +1 -1
- package/dist/cdn/chat/snice-chat.min.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.min.js +1 -1
- package/dist/cdn/chip/snice-chip.js +1 -1
- package/dist/cdn/chip/snice-chip.min.js +1 -1
- package/dist/cdn/code-block/snice-code-block.js +2 -2
- package/dist/cdn/code-block/snice-code-block.js.map +1 -1
- package/dist/cdn/code-block/snice-code-block.min.js +2 -2
- package/dist/cdn/code-block/snice-code-block.min.js.map +1 -1
- package/dist/cdn/color-display/snice-color-display.js +1 -1
- package/dist/cdn/color-display/snice-color-display.min.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.min.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.min.js +1 -1
- package/dist/cdn/comments/snice-comments.js +1 -1
- package/dist/cdn/comments/snice-comments.min.js +1 -1
- package/dist/cdn/countdown/snice-countdown.js +1 -1
- package/dist/cdn/countdown/snice-countdown.min.js +1 -1
- package/dist/cdn/cropper/snice-cropper.js +1 -1
- package/dist/cdn/cropper/snice-cropper.min.js +1 -1
- package/dist/cdn/date-picker/snice-date-picker.js +1 -1
- package/dist/cdn/date-picker/snice-date-picker.min.js +1 -1
- package/dist/cdn/diff/snice-diff.js +1 -1
- package/dist/cdn/diff/snice-diff.min.js +1 -1
- package/dist/cdn/divider/snice-divider.js +1 -1
- package/dist/cdn/divider/snice-divider.min.js +1 -1
- package/dist/cdn/doc/snice-doc.js +1 -1
- package/dist/cdn/doc/snice-doc.min.js +1 -1
- package/dist/cdn/draw/snice-draw.js +1 -1
- package/dist/cdn/draw/snice-draw.min.js +1 -1
- package/dist/cdn/drawer/snice-drawer.js +1 -1
- package/dist/cdn/drawer/snice-drawer.min.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.min.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.min.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.min.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.min.js +1 -1
- package/dist/cdn/flow/snice-flow.js +1 -1
- package/dist/cdn/flow/snice-flow.min.js +1 -1
- package/dist/cdn/funnel/snice-funnel.js +1 -1
- package/dist/cdn/funnel/snice-funnel.min.js +1 -1
- package/dist/cdn/gantt/snice-gantt.js +1 -1
- package/dist/cdn/gantt/snice-gantt.min.js +1 -1
- package/dist/cdn/gauge/snice-gauge.js +1 -1
- package/dist/cdn/gauge/snice-gauge.min.js +1 -1
- package/dist/cdn/heatmap/snice-heatmap.js +1 -1
- package/dist/cdn/heatmap/snice-heatmap.min.js +1 -1
- package/dist/cdn/image/snice-image.js +1 -1
- package/dist/cdn/image/snice-image.min.js +1 -1
- package/dist/cdn/input/snice-input.js +1 -1
- package/dist/cdn/input/snice-input.min.js +1 -1
- package/dist/cdn/kanban/snice-kanban.js +1 -1
- package/dist/cdn/kanban/snice-kanban.min.js +1 -1
- package/dist/cdn/kpi/snice-kpi.js +1 -1
- package/dist/cdn/kpi/snice-kpi.min.js +1 -1
- package/dist/cdn/layout/snice-layout.js +1 -1
- package/dist/cdn/layout/snice-layout.min.js +1 -1
- package/dist/cdn/link/snice-link.js +1 -1
- package/dist/cdn/link/snice-link.min.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.min.js +1 -1
- package/dist/cdn/list/snice-list.js +1 -1
- package/dist/cdn/list/snice-list.min.js +1 -1
- package/dist/cdn/location/snice-location.js +1 -1
- package/dist/cdn/location/snice-location.min.js +1 -1
- package/dist/cdn/login/snice-login.js +1 -1
- package/dist/cdn/login/snice-login.min.js +1 -1
- package/dist/cdn/map/snice-map.js +1 -1
- package/dist/cdn/map/snice-map.min.js +1 -1
- package/dist/cdn/markdown/snice-markdown.js +1 -1
- package/dist/cdn/markdown/snice-markdown.min.js +1 -1
- package/dist/cdn/masonry/snice-masonry.js +1 -1
- package/dist/cdn/masonry/snice-masonry.min.js +1 -1
- package/dist/cdn/menu/snice-menu.js +1 -1
- package/dist/cdn/menu/snice-menu.min.js +1 -1
- package/dist/cdn/modal/snice-modal.js +1 -1
- package/dist/cdn/modal/snice-modal.min.js +1 -1
- package/dist/cdn/music-player/snice-music-player.js +1 -1
- package/dist/cdn/music-player/snice-music-player.min.js +1 -1
- package/dist/cdn/nav/snice-nav.js +1 -1
- package/dist/cdn/nav/snice-nav.min.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.min.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.min.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.min.js +1 -1
- package/dist/cdn/pagination/snice-pagination.js +1 -1
- package/dist/cdn/pagination/snice-pagination.min.js +1 -1
- package/dist/cdn/paint/snice-paint.js +1 -1
- package/dist/cdn/paint/snice-paint.min.js +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.min.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.min.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.min.js +1 -1
- package/dist/cdn/progress/snice-progress.js +1 -1
- package/dist/cdn/progress/snice-progress.min.js +1 -1
- package/dist/cdn/qr-code/README.md +2 -2
- package/dist/cdn/qr-code/snice-qr-code.js +149 -20
- package/dist/cdn/qr-code/snice-qr-code.js.map +1 -1
- package/dist/cdn/qr-code/snice-qr-code.min.js +3 -3
- package/dist/cdn/qr-code/snice-qr-code.min.js.map +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.min.js +1 -1
- package/dist/cdn/radio/snice-radio.js +1 -1
- package/dist/cdn/radio/snice-radio.min.js +1 -1
- package/dist/cdn/rating/snice-rating.js +1 -1
- package/dist/cdn/rating/snice-rating.min.js +1 -1
- package/dist/cdn/recipe/snice-recipe.js +1 -1
- package/dist/cdn/recipe/snice-recipe.min.js +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.js +4 -4
- package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js +3 -3
- package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.js +4 -4
- package/dist/cdn/runtime/snice-runtime.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js +3 -3
- package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
- package/dist/cdn/sankey/snice-sankey.js +1 -1
- package/dist/cdn/sankey/snice-sankey.min.js +1 -1
- package/dist/cdn/select/snice-select.js +1 -1
- package/dist/cdn/select/snice-select.min.js +1 -1
- package/dist/cdn/skeleton/snice-skeleton.js +1 -1
- package/dist/cdn/skeleton/snice-skeleton.min.js +1 -1
- package/dist/cdn/slider/snice-slider.js +2 -2
- package/dist/cdn/slider/snice-slider.js.map +1 -1
- package/dist/cdn/slider/snice-slider.min.js +5 -5
- package/dist/cdn/slider/snice-slider.min.js.map +1 -1
- package/dist/cdn/sortable/snice-sortable.js +1 -1
- package/dist/cdn/sortable/snice-sortable.min.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.min.js +1 -1
- package/dist/cdn/spinner/snice-spinner.js +1 -1
- package/dist/cdn/spinner/snice-spinner.min.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.min.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.min.js +1 -1
- package/dist/cdn/spreadsheet/snice-spreadsheet.js +1 -1
- package/dist/cdn/spreadsheet/snice-spreadsheet.min.js +1 -1
- package/dist/cdn/stepper/snice-stepper.js +1 -1
- package/dist/cdn/stepper/snice-stepper.min.js +1 -1
- package/dist/cdn/switch/snice-switch.js +1 -1
- package/dist/cdn/switch/snice-switch.min.js +1 -1
- package/dist/cdn/table/snice-table.js +1 -1
- package/dist/cdn/table/snice-table.min.js +1 -1
- package/dist/cdn/tabs/snice-tabs.js +1 -1
- package/dist/cdn/tabs/snice-tabs.min.js +1 -1
- package/dist/cdn/tag-input/snice-tag-input.js +1 -1
- package/dist/cdn/tag-input/snice-tag-input.min.js +1 -1
- package/dist/cdn/terminal/snice-terminal.js +1 -1
- package/dist/cdn/terminal/snice-terminal.min.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.min.js +1 -1
- package/dist/cdn/textarea/snice-textarea.js +1 -1
- package/dist/cdn/textarea/snice-textarea.min.js +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.min.js +1 -1
- package/dist/cdn/timeline/snice-timeline.js +1 -1
- package/dist/cdn/timeline/snice-timeline.min.js +1 -1
- package/dist/cdn/timer/snice-timer.js +1 -1
- package/dist/cdn/timer/snice-timer.min.js +1 -1
- package/dist/cdn/toast/snice-toast.js +1 -1
- package/dist/cdn/toast/snice-toast.min.js +1 -1
- package/dist/cdn/tooltip/snice-tooltip.js +1 -1
- package/dist/cdn/tooltip/snice-tooltip.min.js +1 -1
- package/dist/cdn/tree/snice-tree.js +1 -1
- package/dist/cdn/tree/snice-tree.min.js +1 -1
- package/dist/cdn/treemap/snice-treemap.js +1 -1
- package/dist/cdn/treemap/snice-treemap.min.js +1 -1
- package/dist/cdn/video-player/snice-video-player.js +1 -1
- package/dist/cdn/video-player/snice-video-player.min.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.min.js +1 -1
- package/dist/cdn/waterfall/snice-waterfall.js +1 -1
- package/dist/cdn/waterfall/snice-waterfall.min.js +1 -1
- package/dist/cdn/weather/snice-weather.js +1 -1
- package/dist/cdn/weather/snice-weather.min.js +1 -1
- package/dist/components/code-block/snice-code-block.js +1 -1
- package/dist/components/code-block/snice-code-block.js.map +1 -1
- package/dist/components/qr-code/qrcode.d.ts +1 -0
- package/dist/components/qr-code/qrcode.js +16 -8
- package/dist/components/qr-code/qrcode.js.map +1 -1
- package/dist/components/qr-code/snice-qr-code.d.ts +5 -2
- package/dist/components/qr-code/snice-qr-code.js +132 -11
- package/dist/components/qr-code/snice-qr-code.js.map +1 -1
- package/dist/components/qr-code/snice-qr-code.types.d.ts +3 -2
- package/dist/components/slider/snice-slider.js +1 -1
- package/dist/components/slider/snice-slider.js.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.esm.js +2 -2
- package/dist/index.iife.js +2 -2
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/dist/types/request-options.d.ts +1 -1
- package/docs/ai/api.md +1 -1
- package/docs/ai/components/code-block.md +1 -1
- package/docs/ai/decorators.md +2 -2
- package/docs/ai/patterns.md +17 -6
- package/docs/code-block.md +1 -3
- package/docs/controllers.md +98 -391
- package/docs/elements.md +131 -117
- package/docs/events.md +74 -83
- package/docs/fetcher.md +64 -76
- package/docs/observe.md +13 -33
- package/docs/placards.md +6 -16
- package/docs/request-response.md +171 -693
- package/docs/routing.md +67 -136
- package/package.json +1 -1
- package/docs/migration-v2-to-v3.md +0 -569
package/docs/request-response.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Request/Response API Documentation
|
|
2
2
|
|
|
3
|
-
Request/Response provides
|
|
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
|
|
29
|
+
Request/Response enables a single request/response round-trip between elements and their controllers:
|
|
17
30
|
|
|
18
|
-
1. **Element**
|
|
19
|
-
2. **Controller** receives the
|
|
20
|
-
3. **Element** receives the response and
|
|
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
|
|
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
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
64
|
+
Response handlers can be debounced or throttled:
|
|
52
65
|
|
|
53
66
|
```typescript
|
|
54
|
-
@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('
|
|
58
|
-
async
|
|
59
|
-
|
|
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('
|
|
64
|
-
async
|
|
65
|
-
|
|
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,
|
|
90
|
+
import { element, request, property, render, html } from 'snice';
|
|
91
|
+
|
|
92
|
+
type RequestResult<T> = AsyncGenerator<any, T, any> | Promise<T>;
|
|
77
93
|
|
|
78
|
-
@element('
|
|
79
|
-
class
|
|
80
|
-
|
|
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="
|
|
86
|
-
<
|
|
87
|
-
<
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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('
|
|
174
|
-
class
|
|
133
|
+
@controller('product-controller')
|
|
134
|
+
class ProductController implements IController {
|
|
175
135
|
element: HTMLElement | null = null;
|
|
136
|
+
async attach() {}
|
|
137
|
+
async detach() {}
|
|
176
138
|
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
+
### Timeout Behavior
|
|
246
152
|
|
|
247
|
-
The timeout system has **two separate timeouts
|
|
153
|
+
The timeout system has **two separate timeouts**:
|
|
248
154
|
|
|
249
|
-
- **Discovery timeout** (`discoveryTimeout`): 50ms
|
|
250
|
-
- **Response timeout** (`timeout`): 2 minutes
|
|
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('
|
|
254
|
-
discoveryTimeout: 50,
|
|
255
|
-
timeout: 30000
|
|
159
|
+
@request('heavy-computation', {
|
|
160
|
+
discoveryTimeout: 50, // 50ms to find handler
|
|
161
|
+
timeout: 30000 // 30s for actual processing
|
|
256
162
|
})
|
|
257
|
-
async *
|
|
258
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
Limits request frequency to maximum rate:
|
|
168
|
+
### Debounce/Throttle
|
|
285
169
|
|
|
286
170
|
```typescript
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
###
|
|
186
|
+
### Element-Side
|
|
349
187
|
|
|
350
188
|
```typescript
|
|
351
|
-
@element('
|
|
352
|
-
class
|
|
353
|
-
@
|
|
354
|
-
|
|
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
|
-
|
|
194
|
+
@request('load-data', { timeout: 5000 })
|
|
195
|
+
async *loadData(): RequestResult<void> {
|
|
375
196
|
try {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
### Authentication Request/Response
|
|
224
|
+
### Controller-Side
|
|
441
225
|
|
|
442
226
|
```typescript
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
error:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
258
|
+
private ttl = 60000; // 1 minute
|
|
653
259
|
|
|
654
|
-
async attach(
|
|
655
|
-
async detach(
|
|
260
|
+
async attach() {}
|
|
261
|
+
async detach() {}
|
|
656
262
|
|
|
657
263
|
@respond('fetch-cached')
|
|
658
|
-
async
|
|
659
|
-
const
|
|
660
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
711
|
-
|
|
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
|
-
@
|
|
714
|
-
|
|
288
|
+
@request('subscribe-ticker')
|
|
289
|
+
async *subscribe(): RequestResult<void> {
|
|
290
|
+
await (yield { symbol: this.symbol });
|
|
291
|
+
}
|
|
715
292
|
|
|
716
|
-
@
|
|
717
|
-
|
|
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
|
-
<
|
|
723
|
-
<
|
|
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
|
|
804
|
-
@controller('
|
|
805
|
-
class
|
|
307
|
+
// Controller: manages WebSocket, dispatches updates
|
|
308
|
+
@controller('ticker-controller')
|
|
309
|
+
class TickerController implements IController {
|
|
806
310
|
element: HTMLElement | null = null;
|
|
807
|
-
private
|
|
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
|
|
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
|
-
|
|
315
|
+
async detach() {
|
|
316
|
+
this.ws?.close();
|
|
839
317
|
}
|
|
840
318
|
|
|
841
|
-
@respond('
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
return
|
|
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
|
```
|