snice 2.5.4 → 3.1.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/README.md +501 -882
- package/bin/templates/base/src/components/counter-button.ts +13 -26
- package/bin/templates/base/src/controllers/counter-controller.ts +3 -3
- package/dist/components/accordion/snice-accordion-item.d.ts +4 -5
- package/dist/components/accordion/snice-accordion-item.js +37 -39
- package/dist/components/accordion/snice-accordion-item.js.map +1 -1
- package/dist/components/accordion/snice-accordion.d.ts +5 -11
- package/dist/components/accordion/snice-accordion.js +51 -52
- package/dist/components/accordion/snice-accordion.js.map +1 -1
- package/dist/components/alert/snice-alert.d.ts +2 -6
- package/dist/components/alert/snice-alert.js +41 -56
- package/dist/components/alert/snice-alert.js.map +1 -1
- package/dist/components/avatar/snice-avatar.d.ts +2 -6
- package/dist/components/avatar/snice-avatar.js +64 -71
- package/dist/components/avatar/snice-avatar.js.map +1 -1
- package/dist/components/badge/snice-badge.d.ts +2 -3
- package/dist/components/badge/snice-badge.js +22 -23
- package/dist/components/badge/snice-badge.js.map +1 -1
- package/dist/components/breadcrumbs/snice-breadcrumbs.d.ts +5 -12
- package/dist/components/breadcrumbs/snice-breadcrumbs.js +88 -89
- package/dist/components/breadcrumbs/snice-breadcrumbs.js.map +1 -1
- package/dist/components/button/snice-button.d.ts +3 -7
- package/dist/components/button/snice-button.js +37 -58
- package/dist/components/button/snice-button.js.map +1 -1
- package/dist/components/card/snice-card.d.ts +5 -8
- package/dist/components/card/snice-card.js +71 -56
- package/dist/components/card/snice-card.js.map +1 -1
- package/dist/components/checkbox/snice-checkbox.d.ts +4 -13
- package/dist/components/checkbox/snice-checkbox.js +66 -137
- package/dist/components/checkbox/snice-checkbox.js.map +1 -1
- package/dist/components/chip/snice-chip.d.ts +5 -11
- package/dist/components/chip/snice-chip.js +44 -47
- package/dist/components/chip/snice-chip.js.map +1 -1
- package/dist/components/date-picker/snice-date-picker.d.ts +11 -11
- package/dist/components/date-picker/snice-date-picker.js +134 -133
- package/dist/components/date-picker/snice-date-picker.js.map +1 -1
- package/dist/components/divider/snice-divider.d.ts +2 -4
- package/dist/components/divider/snice-divider.js +14 -22
- package/dist/components/divider/snice-divider.js.map +1 -1
- package/dist/components/drawer/snice-drawer.d.ts +4 -4
- package/dist/components/drawer/snice-drawer.js +25 -19
- package/dist/components/drawer/snice-drawer.js.map +1 -1
- package/dist/components/input/snice-input.d.ts +8 -6
- package/dist/components/input/snice-input.js +122 -105
- package/dist/components/input/snice-input.js.map +1 -1
- package/dist/components/layout/snice-layout-blog.d.ts +4 -4
- package/dist/components/layout/snice-layout-blog.js +21 -19
- package/dist/components/layout/snice-layout-blog.js.map +1 -1
- package/dist/components/layout/snice-layout-card.d.ts +2 -2
- package/dist/components/layout/snice-layout-card.js +16 -9
- package/dist/components/layout/snice-layout-card.js.map +1 -1
- package/dist/components/layout/snice-layout-centered.d.ts +2 -2
- package/dist/components/layout/snice-layout-centered.js +14 -7
- package/dist/components/layout/snice-layout-centered.js.map +1 -1
- package/dist/components/layout/snice-layout-dashboard.d.ts +5 -5
- package/dist/components/layout/snice-layout-dashboard.js +38 -30
- package/dist/components/layout/snice-layout-dashboard.js.map +1 -1
- package/dist/components/layout/snice-layout-fullscreen.d.ts +2 -2
- package/dist/components/layout/snice-layout-fullscreen.js +17 -10
- package/dist/components/layout/snice-layout-fullscreen.js.map +1 -1
- package/dist/components/layout/snice-layout-landing.d.ts +4 -4
- package/dist/components/layout/snice-layout-landing.js +21 -19
- package/dist/components/layout/snice-layout-landing.js.map +1 -1
- package/dist/components/layout/snice-layout-minimal.d.ts +2 -2
- package/dist/components/layout/snice-layout-minimal.js +17 -6
- package/dist/components/layout/snice-layout-minimal.js.map +1 -1
- package/dist/components/layout/snice-layout-sidebar.d.ts +5 -4
- package/dist/components/layout/snice-layout-sidebar.js +42 -20
- package/dist/components/layout/snice-layout-sidebar.js.map +1 -1
- package/dist/components/layout/snice-layout-split.d.ts +2 -2
- package/dist/components/layout/snice-layout-split.js +14 -7
- package/dist/components/layout/snice-layout-split.js.map +1 -1
- package/dist/components/layout/snice-layout.d.ts +4 -4
- package/dist/components/layout/snice-layout.js +16 -10
- package/dist/components/layout/snice-layout.js.map +1 -1
- package/dist/components/login/snice-login.d.ts +6 -11
- package/dist/components/login/snice-login.js +97 -71
- package/dist/components/login/snice-login.js.map +1 -1
- package/dist/components/modal/snice-modal.d.ts +5 -9
- package/dist/components/modal/snice-modal.js +47 -78
- package/dist/components/modal/snice-modal.js.map +1 -1
- package/dist/components/nav/snice-nav.d.ts +13 -7
- package/dist/components/nav/snice-nav.js +191 -100
- package/dist/components/nav/snice-nav.js.map +1 -1
- package/dist/components/nav/snice-nav.types.d.ts +3 -3
- package/dist/components/pagination/snice-pagination.d.ts +6 -7
- package/dist/components/pagination/snice-pagination.js +94 -81
- package/dist/components/pagination/snice-pagination.js.map +1 -1
- package/dist/components/progress/snice-progress.d.ts +2 -7
- package/dist/components/progress/snice-progress.js +41 -98
- package/dist/components/progress/snice-progress.js.map +1 -1
- package/dist/components/radio/snice-radio.d.ts +4 -4
- package/dist/components/radio/snice-radio.js +52 -44
- package/dist/components/radio/snice-radio.js.map +1 -1
- package/dist/components/select/snice-option.d.ts +2 -1
- package/dist/components/select/snice-option.js +12 -5
- package/dist/components/select/snice-option.js.map +1 -1
- package/dist/components/select/snice-select.d.ts +9 -21
- package/dist/components/select/snice-select.js +98 -170
- package/dist/components/select/snice-select.js.map +1 -1
- package/dist/components/skeleton/snice-skeleton.d.ts +2 -6
- package/dist/components/skeleton/snice-skeleton.js +18 -49
- package/dist/components/skeleton/snice-skeleton.js.map +1 -1
- package/dist/components/snice-cell-BLFVdxPp.js +4 -0
- package/dist/components/snice-cell-BLFVdxPp.js.map +1 -0
- package/dist/components/switch/snice-switch.d.ts +2 -2
- package/dist/components/switch/snice-switch.js +38 -26
- package/dist/components/switch/snice-switch.js.map +1 -1
- package/dist/components/table/snice-cell-actions.d.ts +24 -0
- package/dist/components/table/snice-cell-actions.js +149 -0
- package/dist/components/table/snice-cell-actions.js.map +1 -0
- package/dist/components/table/snice-cell-boolean.d.ts +2 -2
- package/dist/components/table/snice-cell-boolean.js +13 -7
- package/dist/components/table/snice-cell-boolean.js.map +1 -1
- package/dist/components/table/snice-cell-color.d.ts +18 -0
- package/dist/components/table/snice-cell-color.js +149 -0
- package/dist/components/table/snice-cell-color.js.map +1 -0
- package/dist/components/table/snice-cell-currency.d.ts +24 -0
- package/dist/components/table/snice-cell-currency.js +235 -0
- package/dist/components/table/snice-cell-currency.js.map +1 -0
- package/dist/components/table/snice-cell-date.d.ts +2 -2
- package/dist/components/table/snice-cell-date.js +14 -8
- package/dist/components/table/snice-cell-date.js.map +1 -1
- package/dist/components/table/snice-cell-duration.d.ts +2 -2
- package/dist/components/table/snice-cell-duration.js +12 -6
- package/dist/components/table/snice-cell-duration.js.map +1 -1
- package/dist/components/table/snice-cell-email.d.ts +15 -0
- package/dist/components/table/snice-cell-email.js +125 -0
- package/dist/components/table/snice-cell-email.js.map +1 -0
- package/dist/components/table/snice-cell-filesize.d.ts +2 -2
- package/dist/components/table/snice-cell-filesize.js +12 -6
- package/dist/components/table/snice-cell-filesize.js.map +1 -1
- package/dist/components/table/snice-cell-image.d.ts +20 -0
- package/dist/components/table/snice-cell-image.js +162 -0
- package/dist/components/table/snice-cell-image.js.map +1 -0
- package/dist/components/table/snice-cell-json.d.ts +20 -0
- package/dist/components/table/snice-cell-json.js +186 -0
- package/dist/components/table/snice-cell-json.js.map +1 -0
- package/dist/components/table/snice-cell-link.d.ts +17 -0
- package/dist/components/table/snice-cell-link.js +142 -0
- package/dist/components/table/snice-cell-link.js.map +1 -0
- package/dist/components/table/snice-cell-location.d.ts +19 -0
- package/dist/components/table/snice-cell-location.js +185 -0
- package/dist/components/table/snice-cell-location.js.map +1 -0
- package/dist/components/table/snice-cell-number.d.ts +2 -2
- package/dist/components/table/snice-cell-number.js +12 -6
- package/dist/components/table/snice-cell-number.js.map +1 -1
- package/dist/components/table/snice-cell-percentage.d.ts +22 -0
- package/dist/components/table/snice-cell-percentage.js +208 -0
- package/dist/components/table/snice-cell-percentage.js.map +1 -0
- package/dist/components/table/snice-cell-phone.d.ts +18 -0
- package/dist/components/table/snice-cell-phone.js +153 -0
- package/dist/components/table/snice-cell-phone.js.map +1 -0
- package/dist/components/table/snice-cell-progress.d.ts +2 -2
- package/dist/components/table/snice-cell-progress.js +12 -6
- package/dist/components/table/snice-cell-progress.js.map +1 -1
- package/dist/components/table/snice-cell-rating.d.ts +2 -2
- package/dist/components/table/snice-cell-rating.js +12 -6
- package/dist/components/table/snice-cell-rating.js.map +1 -1
- package/dist/components/table/snice-cell-sparkline.d.ts +2 -2
- package/dist/components/table/snice-cell-sparkline.js +13 -7
- package/dist/components/table/snice-cell-sparkline.js.map +1 -1
- package/dist/components/table/snice-cell-status.d.ts +17 -0
- package/dist/components/table/snice-cell-status.js +144 -0
- package/dist/components/table/snice-cell-status.js.map +1 -0
- package/dist/components/table/snice-cell-tag.d.ts +16 -0
- package/dist/components/table/snice-cell-tag.js +131 -0
- package/dist/components/table/snice-cell-tag.js.map +1 -0
- package/dist/components/table/snice-cell-text.d.ts +2 -2
- package/dist/components/table/snice-cell-text.js +14 -8
- package/dist/components/table/snice-cell-text.js.map +1 -1
- package/dist/components/table/snice-cell.d.ts +2 -2
- package/dist/components/table/snice-cell.js +12 -6
- package/dist/components/table/snice-cell.js.map +1 -1
- package/dist/components/table/snice-column.d.ts +1 -1
- package/dist/components/table/snice-column.js +6 -3
- package/dist/components/table/snice-column.js.map +1 -1
- package/dist/components/table/snice-header.d.ts +5 -5
- package/dist/components/table/snice-header.js +60 -50
- package/dist/components/table/snice-header.js.map +1 -1
- package/dist/components/table/snice-progress.d.ts +2 -2
- package/dist/components/table/snice-progress.js +18 -11
- package/dist/components/table/snice-progress.js.map +1 -1
- package/dist/components/table/snice-rating.d.ts +2 -2
- package/dist/components/table/snice-rating.js +15 -8
- package/dist/components/table/snice-rating.js.map +1 -1
- package/dist/components/table/snice-row.d.ts +17 -6
- package/dist/components/table/snice-row.js +95 -44
- package/dist/components/table/snice-row.js.map +1 -1
- package/dist/components/table/snice-table.d.ts +18 -10
- package/dist/components/table/snice-table.js +355 -173
- package/dist/components/table/snice-table.js.map +1 -1
- package/dist/components/table/snice-table.types.d.ts +101 -2
- package/dist/components/tabs/snice-tab-panel.d.ts +2 -2
- package/dist/components/tabs/snice-tab-panel.js +12 -6
- package/dist/components/tabs/snice-tab-panel.js.map +1 -1
- package/dist/components/tabs/snice-tab.d.ts +6 -5
- package/dist/components/tabs/snice-tab.js +36 -19
- package/dist/components/tabs/snice-tab.js.map +1 -1
- package/dist/components/tabs/snice-tabs.d.ts +5 -5
- package/dist/components/tabs/snice-tabs.js +38 -28
- package/dist/components/tabs/snice-tabs.js.map +1 -1
- package/dist/components/toast/snice-toast-container.d.ts +7 -7
- package/dist/components/toast/snice-toast-container.js +19 -12
- package/dist/components/toast/snice-toast-container.js.map +1 -1
- package/dist/components/toast/snice-toast.d.ts +3 -15
- package/dist/components/toast/snice-toast.js +49 -108
- package/dist/components/toast/snice-toast.js.map +1 -1
- package/dist/components/tooltip/snice-tooltip.d.ts +2 -2
- package/dist/components/tooltip/snice-tooltip.js +14 -7
- package/dist/components/tooltip/snice-tooltip.js.map +1 -1
- package/dist/context.d.ts +44 -0
- package/dist/element-ready.d.ts +40 -0
- package/dist/{types/element.d.ts → element.d.ts} +2 -8
- package/dist/{types/events.d.ts → events.d.ts} +0 -4
- package/dist/index.cjs +2589 -605
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.esm.js +2568 -604
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +2589 -605
- package/dist/index.iife.js.map +1 -1
- package/dist/method-decorators.d.ts +121 -0
- package/dist/on.d.ts +59 -0
- package/dist/parts.d.ts +159 -0
- package/dist/render-debug.d.ts +27 -0
- package/dist/render-tracker.d.ts +14 -0
- package/dist/render.d.ts +96 -0
- package/dist/symbols.cjs +163 -0
- package/dist/symbols.cjs.map +1 -1
- package/dist/{types/symbols.d.ts → symbols.d.ts} +22 -0
- package/dist/symbols.esm.js +27 -3
- package/dist/symbols.esm.js.map +1 -1
- package/dist/template.d.ts +100 -0
- package/dist/transitions.cjs +219 -0
- package/dist/transitions.esm.js +2 -2
- package/dist/types/context.d.ts +48 -0
- package/dist/types/element-options.d.ts +26 -0
- package/dist/types/index.d.ts +25 -9
- package/dist/types/nav-context.d.ts +19 -0
- package/dist/types/{types/on-options.d.ts → on-options.d.ts} +2 -0
- package/dist/types/{types/placard.d.ts → placard.d.ts} +0 -1
- package/docs/ai/README.md +17 -0
- package/docs/ai/api.md +175 -0
- package/docs/ai/architecture.md +160 -0
- package/docs/ai/components/accordion.md +174 -0
- package/docs/ai/components/alert.md +77 -0
- package/docs/ai/components/avatar.md +61 -0
- package/docs/ai/components/badge.md +69 -0
- package/docs/ai/components/breadcrumbs.md +74 -0
- package/docs/ai/components/button.md +75 -0
- package/docs/ai/components/card.md +61 -0
- package/docs/ai/components/checkbox.md +74 -0
- package/docs/ai/components/chip.md +73 -0
- package/docs/ai/components/date-picker.md +75 -0
- package/docs/ai/components/divider.md +66 -0
- package/docs/ai/components/drawer.md +80 -0
- package/docs/ai/components/input.md +111 -0
- package/docs/ai/components/login.md +109 -0
- package/docs/ai/components/modal.md +67 -0
- package/docs/ai/components/nav.md +76 -0
- package/docs/ai/components/pagination.md +55 -0
- package/docs/ai/components/progress.md +72 -0
- package/docs/ai/components/radio.md +79 -0
- package/docs/ai/components/select.md +92 -0
- package/docs/ai/components/skeleton.md +57 -0
- package/docs/ai/components/switch.md +53 -0
- package/docs/ai/components/table.md +227 -0
- package/docs/ai/components/tabs.md +83 -0
- package/docs/ai/components/toast.md +140 -0
- package/docs/ai/components/tooltip.md +146 -0
- package/docs/ai/patterns.md +244 -0
- package/docs/components/accordion.md +558 -0
- package/docs/components/drawer.md +602 -0
- package/docs/components/modal.md +558 -0
- package/docs/components/nav.md +239 -0
- package/docs/components/pagination.md +289 -0
- package/docs/components/select.md +599 -0
- package/docs/components/switch.md +354 -0
- package/docs/components/tabs.md +546 -0
- package/docs/components/toast.md +506 -0
- package/docs/components/tooltip.md +523 -0
- package/docs/controllers.md +744 -0
- package/docs/elements.md +855 -0
- package/docs/events.md +807 -0
- package/docs/migration-v2-to-v3.md +569 -0
- package/docs/observe.md +588 -0
- package/docs/placards.md +401 -0
- package/docs/request-response.md +852 -0
- package/docs/routing.md +1186 -0
- package/package.json +10 -11
- package/dist/components/snice-cell-C9N6yGxQ.js +0 -4
- package/dist/components/snice-cell-C9N6yGxQ.js.map +0 -1
- package/dist/types/types/index.d.ts +0 -23
- /package/dist/{types/controller.d.ts → controller.d.ts} +0 -0
- /package/dist/{types/global.d.ts → global.d.ts} +0 -0
- /package/dist/{types/observe.d.ts → observe.d.ts} +0 -0
- /package/dist/{types/request-response.d.ts → request-response.d.ts} +0 -0
- /package/dist/{types/router.d.ts → router.d.ts} +0 -0
- /package/dist/{types/testing.d.ts → testing.d.ts} +0 -0
- /package/dist/{types/transitions.d.ts → transitions.d.ts} +0 -0
- /package/dist/types/{types/adopted-options.d.ts → adopted-options.d.ts} +0 -0
- /package/dist/types/{types/app-context.d.ts → app-context.d.ts} +0 -0
- /package/dist/types/{types/dispatch-options.d.ts → dispatch-options.d.ts} +0 -0
- /package/dist/types/{types/guard.d.ts → guard.d.ts} +0 -0
- /package/dist/types/{types/i-controller.d.ts → i-controller.d.ts} +0 -0
- /package/dist/types/{types/moved-options.d.ts → moved-options.d.ts} +0 -0
- /package/dist/types/{types/observe-options.d.ts → observe-options.d.ts} +0 -0
- /package/dist/types/{types/page-options.d.ts → page-options.d.ts} +0 -0
- /package/dist/types/{types/part-options.d.ts → part-options.d.ts} +0 -0
- /package/dist/types/{types/property-converter.d.ts → property-converter.d.ts} +0 -0
- /package/dist/types/{types/property-options.d.ts → property-options.d.ts} +0 -0
- /package/dist/types/{types/query-options.d.ts → query-options.d.ts} +0 -0
- /package/dist/types/{types/request-options.d.ts → request-options.d.ts} +0 -0
- /package/dist/types/{types/respond-options.d.ts → respond-options.d.ts} +0 -0
- /package/dist/types/{types/route-params.d.ts → route-params.d.ts} +0 -0
- /package/dist/types/{types/router-instance.d.ts → router-instance.d.ts} +0 -0
- /package/dist/types/{types/router-options.d.ts → router-options.d.ts} +0 -0
- /package/dist/types/{types/simple-array.d.ts → simple-array.d.ts} +0 -0
- /package/dist/types/{types/snice-element.d.ts → snice-element.d.ts} +0 -0
- /package/dist/types/{types/snice-global.d.ts → snice-global.d.ts} +0 -0
- /package/dist/types/{types/transition.d.ts → transition.d.ts} +0 -0
- /package/dist/{types/utils.d.ts → utils.d.ts} +0 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
# Request/Response API Documentation
|
|
2
|
+
|
|
3
|
+
Request/Response provides bidirectional request/response communication between elements and controllers using async generators.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Basic Concept](#basic-concept)
|
|
7
|
+
- [Request/Response Decorators](#requestresponse-decorators)
|
|
8
|
+
- [Element-Side Requests](#element-side-requests)
|
|
9
|
+
- [Controller-Side Responses](#controller-side-responses)
|
|
10
|
+
- [Request/Response Options](#requestresponse-options)
|
|
11
|
+
- [Error Handling](#error-handling)
|
|
12
|
+
- [Advanced Patterns](#advanced-patterns)
|
|
13
|
+
|
|
14
|
+
## Basic Concept
|
|
15
|
+
|
|
16
|
+
Request/Response enables a request/response pattern between elements and their controllers:
|
|
17
|
+
|
|
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
|
|
21
|
+
|
|
22
|
+
This pattern is implemented using async generators and custom events.
|
|
23
|
+
|
|
24
|
+
## Request/Response Decorators
|
|
25
|
+
|
|
26
|
+
### Signature
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
function request(requestName: string, options?: RequestOptions): MethodDecorator
|
|
30
|
+
function response(responseName: string, options?: RespondOptions): MethodDecorator
|
|
31
|
+
|
|
32
|
+
interface RequestOptions extends EventInit {
|
|
33
|
+
timeout?: number; // Response timeout in ms (default: 120000ms = 2 minutes)
|
|
34
|
+
discoveryTimeout?: number; // Handler discovery timeout in ms (default: 50ms)
|
|
35
|
+
debounce?: number; // Debounce requests by specified ms
|
|
36
|
+
throttle?: number; // Throttle requests by specified ms
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface RespondOptions {
|
|
40
|
+
debounce?: number; // Debounce responses by specified ms
|
|
41
|
+
throttle?: number; // Throttle responses by specified ms
|
|
42
|
+
}
|
|
43
|
+
|
|
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>;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### Response Debounce/Throttle
|
|
50
|
+
|
|
51
|
+
Response handlers can also be debounced or throttled to prevent excessive processing:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
@controller('heavy-processing-controller')
|
|
55
|
+
class ProcessingController implements IController {
|
|
56
|
+
|
|
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);
|
|
61
|
+
}
|
|
62
|
+
|
|
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);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Element-Side Requests
|
|
72
|
+
|
|
73
|
+
Elements use async generators to make requests:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { element, request, Response, render, html } from 'snice';
|
|
77
|
+
|
|
78
|
+
@element('user-card')
|
|
79
|
+
class UserCard extends HTMLElement {
|
|
80
|
+
userId = 123;
|
|
81
|
+
|
|
82
|
+
@render()
|
|
83
|
+
renderContent() {
|
|
84
|
+
return html`
|
|
85
|
+
<div class="user-info">
|
|
86
|
+
<button @click=${this.loadUser}>Load User</button>
|
|
87
|
+
<div class="content"></div>
|
|
88
|
+
</div>
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
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
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
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
|
+
```
|
|
165
|
+
|
|
166
|
+
## Controller-Side Responses
|
|
167
|
+
|
|
168
|
+
Controllers handle requests and provide responses:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { controller, respond, IController } from 'snice';
|
|
172
|
+
|
|
173
|
+
@controller('user-controller')
|
|
174
|
+
class UserController implements IController {
|
|
175
|
+
element: HTMLElement | null = null;
|
|
176
|
+
|
|
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
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Request/Response Options
|
|
233
|
+
|
|
234
|
+
### RequestOptions
|
|
235
|
+
|
|
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
|
+
```
|
|
244
|
+
|
|
245
|
+
#### Timeout Behavior (IMPORTANT)
|
|
246
|
+
|
|
247
|
+
The timeout system has **two separate timeouts** for different phases:
|
|
248
|
+
|
|
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
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
@request('@api/heavy-processing', {
|
|
254
|
+
discoveryTimeout: 50, // 50ms to find handler (fast)
|
|
255
|
+
timeout: 30000 // 30s total timeout for processing
|
|
256
|
+
})
|
|
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;
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### Throttle Support
|
|
283
|
+
|
|
284
|
+
Limits request frequency to maximum rate:
|
|
285
|
+
|
|
286
|
+
```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;
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
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
|
+
}
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Error Handling
|
|
347
|
+
|
|
348
|
+
### Handling Timeouts
|
|
349
|
+
|
|
350
|
+
```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
|
+
}
|
|
373
|
+
|
|
374
|
+
async loadData() {
|
|
375
|
+
try {
|
|
376
|
+
const result = await this.fetchData();
|
|
377
|
+
if (!result.success) {
|
|
378
|
+
this.showError('Failed to load data');
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
this.showError('Unexpected error');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
showError(message: string) {
|
|
386
|
+
console.error(message);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
@render()
|
|
390
|
+
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 };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Advanced Patterns
|
|
439
|
+
|
|
440
|
+
### Authentication Request/Response
|
|
441
|
+
|
|
442
|
+
```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 {
|
|
500
|
+
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);
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
success: true,
|
|
519
|
+
token,
|
|
520
|
+
user
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
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');
|
|
537
|
+
}
|
|
538
|
+
|
|
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);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
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
|
+
```
|
|
644
|
+
|
|
645
|
+
### Cached Request/Response
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
@controller('cached-controller')
|
|
649
|
+
class CachedController implements IController {
|
|
650
|
+
element: HTMLElement | null = null;
|
|
651
|
+
private cache = new Map<string, { data: any; timestamp: number }>();
|
|
652
|
+
private cacheTimeout = 60000; // 1 minute
|
|
653
|
+
|
|
654
|
+
async attach(element: HTMLElement) {}
|
|
655
|
+
async detach(element: HTMLElement) {}
|
|
656
|
+
|
|
657
|
+
@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
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
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() };
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
```
|
|
699
|
+
|
|
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';
|
|
705
|
+
|
|
706
|
+
@element('live-data')
|
|
707
|
+
class LiveData extends HTMLElement {
|
|
708
|
+
private updateInterval?: number;
|
|
709
|
+
|
|
710
|
+
@property()
|
|
711
|
+
status = 'Disconnected';
|
|
712
|
+
|
|
713
|
+
@query('.status')
|
|
714
|
+
statusDiv?: HTMLElement;
|
|
715
|
+
|
|
716
|
+
@query('.data')
|
|
717
|
+
dataDiv?: HTMLElement;
|
|
718
|
+
|
|
719
|
+
@render()
|
|
720
|
+
renderContent() {
|
|
721
|
+
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>
|
|
726
|
+
`;
|
|
727
|
+
}
|
|
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
|
+
}
|
|
802
|
+
|
|
803
|
+
// Controller that manages subscriptions
|
|
804
|
+
@controller('subscription-controller')
|
|
805
|
+
class SubscriptionController implements IController {
|
|
806
|
+
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
|
+
}
|
|
820
|
+
|
|
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
|
+
}
|
|
837
|
+
|
|
838
|
+
return { success: false };
|
|
839
|
+
}
|
|
840
|
+
|
|
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;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
```
|