@wcstack/websocket 1.8.1 → 1.8.5
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.ja.md +546 -546
- package/README.md +546 -546
- package/dist/auto.js +3 -3
- package/dist/auto.min.js +3 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +70 -70
package/README.md
CHANGED
|
@@ -1,546 +1,546 @@
|
|
|
1
|
-
# @wcstack/websocket
|
|
2
|
-
|
|
3
|
-
`@wcstack/websocket` is a headless WebSocket component for the wcstack ecosystem.
|
|
4
|
-
|
|
5
|
-
It is not a visual UI widget.
|
|
6
|
-
It is an **I/O node** that connects WebSocket communication to reactive state.
|
|
7
|
-
|
|
8
|
-
With `@wcstack/state`, `<wcs-ws>` can be bound directly through path contracts:
|
|
9
|
-
|
|
10
|
-
- **input / command surface**: `url`, `trigger`, `send`
|
|
11
|
-
- **output state surface**: `message`, `connected`, `loading`, `error`, `readyState`
|
|
12
|
-
|
|
13
|
-
This means real-time communication can be expressed declaratively in HTML, without writing `new WebSocket()`, `onmessage`, or connection glue code in your UI layer.
|
|
14
|
-
|
|
15
|
-
`@wcstack/websocket` follows the [HAWC](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/docs/articles/HAWC.md) architecture:
|
|
16
|
-
|
|
17
|
-
- **Core** (`WebSocketCore`) handles connection, messaging, reconnection, and async state
|
|
18
|
-
- **Shell** (`<wcs-ws>`) connects that state to the DOM
|
|
19
|
-
- frameworks and binding systems consume it through [wc-bindable-protocol](https://github.com/wc-bindable-protocol/wc-bindable-protocol)
|
|
20
|
-
|
|
21
|
-
## Why this exists
|
|
22
|
-
|
|
23
|
-
Real-time features typically require imperative WebSocket management: connection lifecycle, reconnection logic, message parsing, error handling, and cleanup on disconnect.
|
|
24
|
-
|
|
25
|
-
`@wcstack/websocket` moves that logic into a reusable component and exposes the result as bindable state.
|
|
26
|
-
|
|
27
|
-
With `@wcstack/state`, the flow becomes:
|
|
28
|
-
|
|
29
|
-
1. state determines the `url` (or `trigger` fires)
|
|
30
|
-
2. `<wcs-ws>` opens the connection
|
|
31
|
-
3. incoming messages arrive as `message`, connection status as `connected`, `loading`, `error`
|
|
32
|
-
4. UI binds to those paths with `data-wcs`
|
|
33
|
-
|
|
34
|
-
This turns real-time communication into **state transitions**, not imperative event wiring.
|
|
35
|
-
|
|
36
|
-
## Install
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
npm install @wcstack/websocket
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Quick Start
|
|
43
|
-
|
|
44
|
-
### 1. Reactive WebSocket from state
|
|
45
|
-
|
|
46
|
-
When `<wcs-ws>` is connected to the DOM with a `url`, it automatically opens a WebSocket connection. JSON messages are automatically parsed.
|
|
47
|
-
|
|
48
|
-
```html
|
|
49
|
-
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
|
|
50
|
-
<script type="module" src="https://esm.run/@wcstack/websocket/auto"></script>
|
|
51
|
-
|
|
52
|
-
<wcs-state>
|
|
53
|
-
<script type="application/json">
|
|
54
|
-
{
|
|
55
|
-
"lastMessage": null,
|
|
56
|
-
"isConnected": false,
|
|
57
|
-
"isLoading": false
|
|
58
|
-
}
|
|
59
|
-
</script>
|
|
60
|
-
|
|
61
|
-
<wcs-ws
|
|
62
|
-
url="wss://example.com/ws"
|
|
63
|
-
data-wcs="message: lastMessage; connected: isConnected; loading: isLoading">
|
|
64
|
-
</wcs-ws>
|
|
65
|
-
|
|
66
|
-
<p data-wcs="textContent: isConnected|then('Connected','Disconnected')"></p>
|
|
67
|
-
<pre data-wcs="textContent: lastMessage|json"></pre>
|
|
68
|
-
</wcs-state>
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
This is the default mode:
|
|
72
|
-
|
|
73
|
-
- set `url`
|
|
74
|
-
- receive `message`
|
|
75
|
-
- optionally bind `connected`, `loading`, `error`, and `readyState`
|
|
76
|
-
|
|
77
|
-
### 2. Sending messages from state
|
|
78
|
-
|
|
79
|
-
Use the `send` property to push data to the server. Setting `send` transmits the value immediately; objects are automatically JSON-stringified.
|
|
80
|
-
|
|
81
|
-
```html
|
|
82
|
-
<wcs-state>
|
|
83
|
-
<script type="module">
|
|
84
|
-
export default {
|
|
85
|
-
chatInput: "",
|
|
86
|
-
lastMessage: null,
|
|
87
|
-
outgoing: null,
|
|
88
|
-
|
|
89
|
-
sendChat() {
|
|
90
|
-
this.outgoing = { type: "chat", content: this.chatInput };
|
|
91
|
-
this.chatInput = "";
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
</script>
|
|
95
|
-
|
|
96
|
-
<wcs-ws
|
|
97
|
-
url="wss://example.com/ws"
|
|
98
|
-
data-wcs="message: lastMessage; send: outgoing">
|
|
99
|
-
</wcs-ws>
|
|
100
|
-
|
|
101
|
-
<input data-wcs="value: chatInput" placeholder="Type a message">
|
|
102
|
-
<button data-wcs="onclick: sendChat">Send</button>
|
|
103
|
-
|
|
104
|
-
<pre data-wcs="textContent: lastMessage|json"></pre>
|
|
105
|
-
</wcs-state>
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### 3. Manual connection with `trigger`
|
|
109
|
-
|
|
110
|
-
Use `manual` when you want to control when the connection opens.
|
|
111
|
-
|
|
112
|
-
```html
|
|
113
|
-
<wcs-state>
|
|
114
|
-
<script type="module">
|
|
115
|
-
export default {
|
|
116
|
-
shouldConnect: false,
|
|
117
|
-
lastMessage: null,
|
|
118
|
-
isConnected: false,
|
|
119
|
-
|
|
120
|
-
openConnection() {
|
|
121
|
-
this.shouldConnect = true;
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
</script>
|
|
125
|
-
|
|
126
|
-
<wcs-ws
|
|
127
|
-
url="wss://example.com/ws"
|
|
128
|
-
manual
|
|
129
|
-
data-wcs="trigger: shouldConnect; message: lastMessage; connected: isConnected">
|
|
130
|
-
</wcs-ws>
|
|
131
|
-
|
|
132
|
-
<button data-wcs="onclick: openConnection">Connect</button>
|
|
133
|
-
<p data-wcs="textContent: isConnected|then('Connected','Disconnected')"></p>
|
|
134
|
-
</wcs-state>
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
`trigger` is a **one-way command surface**:
|
|
138
|
-
|
|
139
|
-
- writing `true` opens the connection
|
|
140
|
-
- it resets itself to `false` after the connection is initiated
|
|
141
|
-
- the reset emits `wcs-ws:trigger-changed`
|
|
142
|
-
|
|
143
|
-
```
|
|
144
|
-
external write: false → true No event (triggers connect)
|
|
145
|
-
auto-reset: true → false Dispatches wcs-ws:trigger-changed
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### 4. Auto-reconnect
|
|
149
|
-
|
|
150
|
-
```html
|
|
151
|
-
<wcs-ws
|
|
152
|
-
url="wss://example.com/ws"
|
|
153
|
-
auto-reconnect
|
|
154
|
-
reconnect-interval="5000"
|
|
155
|
-
max-reconnects="10"
|
|
156
|
-
data-wcs="message: lastMessage; connected: isConnected; error: wsError">
|
|
157
|
-
</wcs-ws>
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
When the connection drops unexpectedly (close code other than 1000), `<wcs-ws>` automatically reconnects:
|
|
161
|
-
|
|
162
|
-
- waits `reconnect-interval` ms (default: 3000)
|
|
163
|
-
- retries up to `max-reconnects` times (default: Infinity)
|
|
164
|
-
- resets the retry count on successful reconnection
|
|
165
|
-
|
|
166
|
-
## State Surface vs Command Surface
|
|
167
|
-
|
|
168
|
-
`<wcs-ws>` exposes two different kinds of properties.
|
|
169
|
-
|
|
170
|
-
### Output state (bindable async state)
|
|
171
|
-
|
|
172
|
-
These properties represent the current connection state and are the main HAWC surface:
|
|
173
|
-
|
|
174
|
-
| Property | Type | Description |
|
|
175
|
-
|----------|------|-------------|
|
|
176
|
-
| `message` | `any` | Latest received message (JSON auto-parsed) |
|
|
177
|
-
| `connected` | `boolean` | `true` while WebSocket is open |
|
|
178
|
-
| `loading` | `boolean` | `true` while connecting |
|
|
179
|
-
| `error` | `WcsWsError \| Event \| null` | Connection or close error |
|
|
180
|
-
| `readyState` | `number` | WebSocket readyState constant |
|
|
181
|
-
|
|
182
|
-
### Input / command surface
|
|
183
|
-
|
|
184
|
-
These properties control connection and messaging from HTML, JS, or `@wcstack/state` bindings:
|
|
185
|
-
|
|
186
|
-
| Property | Type | Description |
|
|
187
|
-
|----------|------|-------------|
|
|
188
|
-
| `url` | `string` | WebSocket endpoint URL |
|
|
189
|
-
| `trigger` | `boolean` | One-way connection trigger |
|
|
190
|
-
| `send` | `any` | Set to transmit data (auto-stringifies objects) |
|
|
191
|
-
| `manual` | `boolean` | Disables auto-connect on DOM attach |
|
|
192
|
-
|
|
193
|
-
## Architecture
|
|
194
|
-
|
|
195
|
-
`@wcstack/websocket` follows the HAWC architecture.
|
|
196
|
-
|
|
197
|
-
### Core: `WebSocketCore`
|
|
198
|
-
|
|
199
|
-
`WebSocketCore` is a pure `EventTarget` class.
|
|
200
|
-
It contains:
|
|
201
|
-
|
|
202
|
-
- WebSocket connection management
|
|
203
|
-
- automatic reconnection logic
|
|
204
|
-
- JSON message parsing
|
|
205
|
-
- async state transitions
|
|
206
|
-
- `wc-bindable-protocol` declaration
|
|
207
|
-
|
|
208
|
-
It can run headlessly in any runtime that supports `EventTarget` and `WebSocket`.
|
|
209
|
-
|
|
210
|
-
### Shell: `<wcs-ws>`
|
|
211
|
-
|
|
212
|
-
`<wcs-ws>` is a thin `HTMLElement` wrapper around `WebSocketCore`.
|
|
213
|
-
It adds:
|
|
214
|
-
|
|
215
|
-
- attribute / property mapping
|
|
216
|
-
- DOM lifecycle integration
|
|
217
|
-
- declarative helpers: `trigger`, `send`
|
|
218
|
-
|
|
219
|
-
This split keeps the connection logic portable while allowing DOM-based binding systems such as `@wcstack/state` to interact with it naturally.
|
|
220
|
-
|
|
221
|
-
### Target injection
|
|
222
|
-
|
|
223
|
-
The Core dispatches events directly on the Shell via **target injection**, so no event re-dispatch is needed.
|
|
224
|
-
|
|
225
|
-
## Headless Usage (Core only)
|
|
226
|
-
|
|
227
|
-
`WebSocketCore` can be used standalone without the DOM. Since it declares `static wcBindable`, you can use `@wc-bindable/core`'s `bind()` to subscribe to its state:
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
import { WebSocketCore } from "@wcstack/websocket";
|
|
231
|
-
import { bind } from "@wc-bindable/core";
|
|
232
|
-
|
|
233
|
-
const core = new WebSocketCore();
|
|
234
|
-
|
|
235
|
-
const unbind = bind(core, (name, value) => {
|
|
236
|
-
console.log(`${name}:`, value);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
core.connect("wss://example.com/ws", {
|
|
240
|
-
autoReconnect: true,
|
|
241
|
-
reconnectInterval: 5000,
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Send a message
|
|
245
|
-
core.send(JSON.stringify({ type: "ping" }));
|
|
246
|
-
|
|
247
|
-
// Clean up
|
|
248
|
-
core.close();
|
|
249
|
-
unbind();
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
This works in Node.js, Deno, Cloudflare Workers — anywhere `EventTarget` and `WebSocket` are available.
|
|
253
|
-
|
|
254
|
-
## URL Observation
|
|
255
|
-
|
|
256
|
-
By default, `<wcs-ws>` automatically opens a connection when:
|
|
257
|
-
|
|
258
|
-
1. it is connected to the DOM and `url` is set
|
|
259
|
-
2. the `url` attribute changes while connected to the DOM
|
|
260
|
-
|
|
261
|
-
Set the `manual` attribute to disable auto-connect and control the connection explicitly via `connect()` or `trigger`.
|
|
262
|
-
|
|
263
|
-
## Programmatic Usage
|
|
264
|
-
|
|
265
|
-
```javascript
|
|
266
|
-
const wsEl = document.querySelector("wcs-ws");
|
|
267
|
-
|
|
268
|
-
// Connect manually
|
|
269
|
-
wsEl.connect();
|
|
270
|
-
|
|
271
|
-
// Send data
|
|
272
|
-
wsEl.sendMessage(JSON.stringify({ type: "chat", content: "hello" }));
|
|
273
|
-
|
|
274
|
-
console.log(wsEl.message); // latest message
|
|
275
|
-
console.log(wsEl.connected); // boolean
|
|
276
|
-
console.log(wsEl.loading); // boolean
|
|
277
|
-
console.log(wsEl.error); // error info or null
|
|
278
|
-
console.log(wsEl.readyState); // WebSocket readyState
|
|
279
|
-
|
|
280
|
-
// Close
|
|
281
|
-
wsEl.close();
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
## Optional DOM Triggering
|
|
285
|
-
|
|
286
|
-
If `autoTrigger` is enabled (default), clicking an element with `data-wstarget` triggers the corresponding `<wcs-ws>` element:
|
|
287
|
-
|
|
288
|
-
```html
|
|
289
|
-
<button data-wstarget="my-ws">Connect</button>
|
|
290
|
-
<wcs-ws id="my-ws" url="wss://example.com/ws" manual></wcs-ws>
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
Event delegation is used — works with dynamically added elements. The `closest()` API handles nested elements (e.g., icon inside a button).
|
|
294
|
-
|
|
295
|
-
If the target id does not match any element, or the matched element is not a `<wcs-ws>`, the click is silently ignored.
|
|
296
|
-
|
|
297
|
-
This is a convenience feature.
|
|
298
|
-
In wcstack applications, **state-driven triggering via `trigger`** is usually the primary pattern.
|
|
299
|
-
|
|
300
|
-
## Elements
|
|
301
|
-
|
|
302
|
-
### `<wcs-ws>`
|
|
303
|
-
|
|
304
|
-
| Attribute | Type | Default | Description |
|
|
305
|
-
|-----------|------|---------|-------------|
|
|
306
|
-
| `url` | `string` | — | WebSocket endpoint URL |
|
|
307
|
-
| `protocols` | `string` | — | Comma-separated subprotocol list |
|
|
308
|
-
| `manual` | `boolean` | `false` | Disable auto-connect |
|
|
309
|
-
| `auto-reconnect` | `boolean` | `false` | Enable automatic reconnection |
|
|
310
|
-
| `reconnect-interval` | `number` | `3000` | Reconnection delay in ms |
|
|
311
|
-
| `max-reconnects` | `number` | `Infinity` | Maximum reconnection attempts |
|
|
312
|
-
|
|
313
|
-
| Property | Type | Description |
|
|
314
|
-
|----------|------|-------------|
|
|
315
|
-
| `message` | `any` | Latest received message (JSON auto-parsed) |
|
|
316
|
-
| `connected` | `boolean` | `true` while WebSocket is open |
|
|
317
|
-
| `loading` | `boolean` | `true` while connecting |
|
|
318
|
-
| `error` | `WcsWsError \| Event \| null` | Error info |
|
|
319
|
-
| `readyState` | `number` | WebSocket readyState constant |
|
|
320
|
-
| `trigger` | `boolean` | Set to `true` to open connection |
|
|
321
|
-
| `send` | `any` | Set to transmit data |
|
|
322
|
-
|
|
323
|
-
| Method | Description |
|
|
324
|
-
|--------|-------------|
|
|
325
|
-
| `connect()` | Open the WebSocket connection |
|
|
326
|
-
| `sendMessage(data)` | Send data over the connection |
|
|
327
|
-
| `close(code?, reason?)` | Close the connection |
|
|
328
|
-
|
|
329
|
-
## wc-bindable-protocol
|
|
330
|
-
|
|
331
|
-
Both `WebSocketCore` and `<wcs-ws>` declare `wc-bindable-protocol` compliance, making them interoperable with any framework or component that supports the protocol.
|
|
332
|
-
|
|
333
|
-
### Core (`WebSocketCore`)
|
|
334
|
-
|
|
335
|
-
`WebSocketCore` declares the bindable async state that any runtime can subscribe to:
|
|
336
|
-
|
|
337
|
-
```typescript
|
|
338
|
-
static wcBindable = {
|
|
339
|
-
protocol: "wc-bindable",
|
|
340
|
-
version: 1,
|
|
341
|
-
properties: [
|
|
342
|
-
{ name: "message", event: "wcs-ws:message" },
|
|
343
|
-
{ name: "connected", event: "wcs-ws:connected-changed" },
|
|
344
|
-
{ name: "loading", event: "wcs-ws:loading-changed" },
|
|
345
|
-
{ name: "error", event: "wcs-ws:error" },
|
|
346
|
-
{ name: "readyState", event: "wcs-ws:readystate-changed" },
|
|
347
|
-
],
|
|
348
|
-
};
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
Headless consumers call `core.connect(url)` directly — no `trigger` needed.
|
|
352
|
-
|
|
353
|
-
### Shell (`<wcs-ws>`)
|
|
354
|
-
|
|
355
|
-
The Shell extends the Core declaration with `trigger` and `send` so binding systems can control the connection declaratively:
|
|
356
|
-
|
|
357
|
-
```typescript
|
|
358
|
-
static wcBindable = {
|
|
359
|
-
...WebSocketCore.wcBindable,
|
|
360
|
-
properties: [
|
|
361
|
-
...WebSocketCore.wcBindable.properties,
|
|
362
|
-
{ name: "trigger", event: "wcs-ws:trigger-changed" },
|
|
363
|
-
{ name: "send", event: "wcs-ws:send-changed" },
|
|
364
|
-
],
|
|
365
|
-
};
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
## TypeScript Types
|
|
369
|
-
|
|
370
|
-
```typescript
|
|
371
|
-
import type {
|
|
372
|
-
WcsWsError, WcsWsCoreValues, WcsWsValues
|
|
373
|
-
} from "@wcstack/websocket";
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
```typescript
|
|
377
|
-
// WebSocket error
|
|
378
|
-
interface WcsWsError {
|
|
379
|
-
code?: number;
|
|
380
|
-
reason?: string;
|
|
381
|
-
message?: string;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Core (headless) — 5 async state properties
|
|
385
|
-
// T defaults to unknown; pass a type argument for typed `message`
|
|
386
|
-
interface WcsWsCoreValues<T = unknown> {
|
|
387
|
-
message: T;
|
|
388
|
-
connected: boolean;
|
|
389
|
-
loading: boolean;
|
|
390
|
-
error: WcsWsError | Event | null;
|
|
391
|
-
readyState: number;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Shell (<wcs-ws>) — extends Core with trigger and send
|
|
395
|
-
interface WcsWsValues<T = unknown> extends WcsWsCoreValues<T> {
|
|
396
|
-
trigger: boolean;
|
|
397
|
-
send: unknown;
|
|
398
|
-
}
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
## Why this works well with `@wcstack/state`
|
|
402
|
-
|
|
403
|
-
`@wcstack/state` uses path strings as the only contract between UI and state.
|
|
404
|
-
`<wcs-ws>` fits this model naturally:
|
|
405
|
-
|
|
406
|
-
- state determines the `url` or fires `trigger`
|
|
407
|
-
- `<wcs-ws>` opens and manages the connection
|
|
408
|
-
- incoming data arrives as `message`; status as `connected`, `loading`, `error`
|
|
409
|
-
- UI binds to those paths without writing WebSocket glue code
|
|
410
|
-
- outgoing data flows via the `send` property
|
|
411
|
-
|
|
412
|
-
This makes real-time communication look like ordinary state updates.
|
|
413
|
-
|
|
414
|
-
## Framework Integration
|
|
415
|
-
|
|
416
|
-
Since `<wcs-ws>` is HAWC + `wc-bindable-protocol`, it works with any framework through thin adapters from `@wc-bindable/*`.
|
|
417
|
-
|
|
418
|
-
### React
|
|
419
|
-
|
|
420
|
-
```tsx
|
|
421
|
-
import { useWcBindable } from "@wc-bindable/react";
|
|
422
|
-
import type { WcsWsValues } from "@wcstack/websocket";
|
|
423
|
-
|
|
424
|
-
interface ChatMessage { type: string; content: string; }
|
|
425
|
-
|
|
426
|
-
function Chat() {
|
|
427
|
-
const [ref, { message, connected, loading }] =
|
|
428
|
-
useWcBindable<HTMLElement, WcsWsValues<ChatMessage>>();
|
|
429
|
-
|
|
430
|
-
return (
|
|
431
|
-
<>
|
|
432
|
-
<wcs-ws ref={ref} url="wss://example.com/ws" auto-reconnect />
|
|
433
|
-
{loading && <p>Connecting...</p>}
|
|
434
|
-
{connected && <p>Connected</p>}
|
|
435
|
-
{message && <pre>{JSON.stringify(message)}</pre>}
|
|
436
|
-
</>
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
### Vue
|
|
442
|
-
|
|
443
|
-
```vue
|
|
444
|
-
<script setup lang="ts">
|
|
445
|
-
import { useWcBindable } from "@wc-bindable/vue";
|
|
446
|
-
import type { WcsWsValues } from "@wcstack/websocket";
|
|
447
|
-
|
|
448
|
-
interface ChatMessage { type: string; content: string; }
|
|
449
|
-
|
|
450
|
-
const { ref, values } = useWcBindable<HTMLElement, WcsWsValues<ChatMessage>>();
|
|
451
|
-
</script>
|
|
452
|
-
|
|
453
|
-
<template>
|
|
454
|
-
<wcs-ws :ref="ref" url="wss://example.com/ws" auto-reconnect />
|
|
455
|
-
<p v-if="values.loading">Connecting...</p>
|
|
456
|
-
<p v-else-if="values.connected">Connected</p>
|
|
457
|
-
<pre v-if="values.message">{{ values.message }}</pre>
|
|
458
|
-
</template>
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
### Svelte
|
|
462
|
-
|
|
463
|
-
```svelte
|
|
464
|
-
<script>
|
|
465
|
-
import { wcBindable } from "@wc-bindable/svelte";
|
|
466
|
-
|
|
467
|
-
let message = $state(null);
|
|
468
|
-
let connected = $state(false);
|
|
469
|
-
</script>
|
|
470
|
-
|
|
471
|
-
<wcs-ws url="wss://example.com/ws" auto-reconnect
|
|
472
|
-
use:wcBindable={{ onUpdate: (name, v) => {
|
|
473
|
-
if (name === "message") message = v;
|
|
474
|
-
if (name === "connected") connected = v;
|
|
475
|
-
}}} />
|
|
476
|
-
|
|
477
|
-
<p>{connected ? "Connected" : "Disconnected"}</p>
|
|
478
|
-
{#if message}
|
|
479
|
-
<pre>{JSON.stringify(message)}</pre>
|
|
480
|
-
{/if}
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
### Solid
|
|
484
|
-
|
|
485
|
-
```tsx
|
|
486
|
-
import { createWcBindable } from "@wc-bindable/solid";
|
|
487
|
-
import type { WcsWsValues } from "@wcstack/websocket";
|
|
488
|
-
|
|
489
|
-
interface ChatMessage { type: string; content: string; }
|
|
490
|
-
|
|
491
|
-
function Chat() {
|
|
492
|
-
const [values, directive] = createWcBindable<WcsWsValues<ChatMessage>>();
|
|
493
|
-
|
|
494
|
-
return (
|
|
495
|
-
<>
|
|
496
|
-
<wcs-ws ref={directive} url="wss://example.com/ws" auto-reconnect />
|
|
497
|
-
<Show when={values.connected} fallback={<p>Disconnected</p>}>
|
|
498
|
-
<p>Connected</p>
|
|
499
|
-
</Show>
|
|
500
|
-
<Show when={values.message}>
|
|
501
|
-
<pre>{JSON.stringify(values.message)}</pre>
|
|
502
|
-
</Show>
|
|
503
|
-
</>
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
### Vanilla — `bind()` directly
|
|
509
|
-
|
|
510
|
-
```javascript
|
|
511
|
-
import { bind } from "@wc-bindable/core";
|
|
512
|
-
|
|
513
|
-
const wsEl = document.querySelector("wcs-ws");
|
|
514
|
-
|
|
515
|
-
bind(wsEl, (name, value) => {
|
|
516
|
-
console.log(`${name} changed:`, value);
|
|
517
|
-
});
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
## Configuration
|
|
521
|
-
|
|
522
|
-
```javascript
|
|
523
|
-
import { bootstrapWebSocket } from "@wcstack/websocket";
|
|
524
|
-
|
|
525
|
-
bootstrapWebSocket({
|
|
526
|
-
autoTrigger: true,
|
|
527
|
-
triggerAttribute: "data-wstarget",
|
|
528
|
-
tagNames: {
|
|
529
|
-
ws: "wcs-ws",
|
|
530
|
-
},
|
|
531
|
-
});
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
## Design Notes
|
|
535
|
-
|
|
536
|
-
- `message`, `connected`, `loading`, `error`, and `readyState` are **output state**
|
|
537
|
-
- `url`, `trigger`, and `send` are **input / command surface**
|
|
538
|
-
- `trigger` is intentionally one-way: writing `true` connects, reset emits completion
|
|
539
|
-
- `send` transmits immediately and resets to `null` — set it each time you want to send
|
|
540
|
-
- JSON messages are automatically parsed on receive; objects are auto-stringified on send
|
|
541
|
-
- `manual` is useful when connection timing should be controlled explicitly
|
|
542
|
-
- Auto-reconnect only fires on abnormal close (code other than 1000)
|
|
543
|
-
|
|
544
|
-
## License
|
|
545
|
-
|
|
546
|
-
MIT
|
|
1
|
+
# @wcstack/websocket
|
|
2
|
+
|
|
3
|
+
`@wcstack/websocket` is a headless WebSocket component for the wcstack ecosystem.
|
|
4
|
+
|
|
5
|
+
It is not a visual UI widget.
|
|
6
|
+
It is an **I/O node** that connects WebSocket communication to reactive state.
|
|
7
|
+
|
|
8
|
+
With `@wcstack/state`, `<wcs-ws>` can be bound directly through path contracts:
|
|
9
|
+
|
|
10
|
+
- **input / command surface**: `url`, `trigger`, `send`
|
|
11
|
+
- **output state surface**: `message`, `connected`, `loading`, `error`, `readyState`
|
|
12
|
+
|
|
13
|
+
This means real-time communication can be expressed declaratively in HTML, without writing `new WebSocket()`, `onmessage`, or connection glue code in your UI layer.
|
|
14
|
+
|
|
15
|
+
`@wcstack/websocket` follows the [HAWC](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/docs/articles/HAWC.md) architecture:
|
|
16
|
+
|
|
17
|
+
- **Core** (`WebSocketCore`) handles connection, messaging, reconnection, and async state
|
|
18
|
+
- **Shell** (`<wcs-ws>`) connects that state to the DOM
|
|
19
|
+
- frameworks and binding systems consume it through [wc-bindable-protocol](https://github.com/wc-bindable-protocol/wc-bindable-protocol)
|
|
20
|
+
|
|
21
|
+
## Why this exists
|
|
22
|
+
|
|
23
|
+
Real-time features typically require imperative WebSocket management: connection lifecycle, reconnection logic, message parsing, error handling, and cleanup on disconnect.
|
|
24
|
+
|
|
25
|
+
`@wcstack/websocket` moves that logic into a reusable component and exposes the result as bindable state.
|
|
26
|
+
|
|
27
|
+
With `@wcstack/state`, the flow becomes:
|
|
28
|
+
|
|
29
|
+
1. state determines the `url` (or `trigger` fires)
|
|
30
|
+
2. `<wcs-ws>` opens the connection
|
|
31
|
+
3. incoming messages arrive as `message`, connection status as `connected`, `loading`, `error`
|
|
32
|
+
4. UI binds to those paths with `data-wcs`
|
|
33
|
+
|
|
34
|
+
This turns real-time communication into **state transitions**, not imperative event wiring.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @wcstack/websocket
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
### 1. Reactive WebSocket from state
|
|
45
|
+
|
|
46
|
+
When `<wcs-ws>` is connected to the DOM with a `url`, it automatically opens a WebSocket connection. JSON messages are automatically parsed.
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
|
|
50
|
+
<script type="module" src="https://esm.run/@wcstack/websocket/auto"></script>
|
|
51
|
+
|
|
52
|
+
<wcs-state>
|
|
53
|
+
<script type="application/json">
|
|
54
|
+
{
|
|
55
|
+
"lastMessage": null,
|
|
56
|
+
"isConnected": false,
|
|
57
|
+
"isLoading": false
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<wcs-ws
|
|
62
|
+
url="wss://example.com/ws"
|
|
63
|
+
data-wcs="message: lastMessage; connected: isConnected; loading: isLoading">
|
|
64
|
+
</wcs-ws>
|
|
65
|
+
|
|
66
|
+
<p data-wcs="textContent: isConnected|then('Connected','Disconnected')"></p>
|
|
67
|
+
<pre data-wcs="textContent: lastMessage|json"></pre>
|
|
68
|
+
</wcs-state>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This is the default mode:
|
|
72
|
+
|
|
73
|
+
- set `url`
|
|
74
|
+
- receive `message`
|
|
75
|
+
- optionally bind `connected`, `loading`, `error`, and `readyState`
|
|
76
|
+
|
|
77
|
+
### 2. Sending messages from state
|
|
78
|
+
|
|
79
|
+
Use the `send` property to push data to the server. Setting `send` transmits the value immediately; objects are automatically JSON-stringified.
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<wcs-state>
|
|
83
|
+
<script type="module">
|
|
84
|
+
export default {
|
|
85
|
+
chatInput: "",
|
|
86
|
+
lastMessage: null,
|
|
87
|
+
outgoing: null,
|
|
88
|
+
|
|
89
|
+
sendChat() {
|
|
90
|
+
this.outgoing = { type: "chat", content: this.chatInput };
|
|
91
|
+
this.chatInput = "";
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<wcs-ws
|
|
97
|
+
url="wss://example.com/ws"
|
|
98
|
+
data-wcs="message: lastMessage; send: outgoing">
|
|
99
|
+
</wcs-ws>
|
|
100
|
+
|
|
101
|
+
<input data-wcs="value: chatInput" placeholder="Type a message">
|
|
102
|
+
<button data-wcs="onclick: sendChat">Send</button>
|
|
103
|
+
|
|
104
|
+
<pre data-wcs="textContent: lastMessage|json"></pre>
|
|
105
|
+
</wcs-state>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Manual connection with `trigger`
|
|
109
|
+
|
|
110
|
+
Use `manual` when you want to control when the connection opens.
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<wcs-state>
|
|
114
|
+
<script type="module">
|
|
115
|
+
export default {
|
|
116
|
+
shouldConnect: false,
|
|
117
|
+
lastMessage: null,
|
|
118
|
+
isConnected: false,
|
|
119
|
+
|
|
120
|
+
openConnection() {
|
|
121
|
+
this.shouldConnect = true;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<wcs-ws
|
|
127
|
+
url="wss://example.com/ws"
|
|
128
|
+
manual
|
|
129
|
+
data-wcs="trigger: shouldConnect; message: lastMessage; connected: isConnected">
|
|
130
|
+
</wcs-ws>
|
|
131
|
+
|
|
132
|
+
<button data-wcs="onclick: openConnection">Connect</button>
|
|
133
|
+
<p data-wcs="textContent: isConnected|then('Connected','Disconnected')"></p>
|
|
134
|
+
</wcs-state>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`trigger` is a **one-way command surface**:
|
|
138
|
+
|
|
139
|
+
- writing `true` opens the connection
|
|
140
|
+
- it resets itself to `false` after the connection is initiated
|
|
141
|
+
- the reset emits `wcs-ws:trigger-changed`
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
external write: false → true No event (triggers connect)
|
|
145
|
+
auto-reset: true → false Dispatches wcs-ws:trigger-changed
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 4. Auto-reconnect
|
|
149
|
+
|
|
150
|
+
```html
|
|
151
|
+
<wcs-ws
|
|
152
|
+
url="wss://example.com/ws"
|
|
153
|
+
auto-reconnect
|
|
154
|
+
reconnect-interval="5000"
|
|
155
|
+
max-reconnects="10"
|
|
156
|
+
data-wcs="message: lastMessage; connected: isConnected; error: wsError">
|
|
157
|
+
</wcs-ws>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
When the connection drops unexpectedly (close code other than 1000), `<wcs-ws>` automatically reconnects:
|
|
161
|
+
|
|
162
|
+
- waits `reconnect-interval` ms (default: 3000)
|
|
163
|
+
- retries up to `max-reconnects` times (default: Infinity)
|
|
164
|
+
- resets the retry count on successful reconnection
|
|
165
|
+
|
|
166
|
+
## State Surface vs Command Surface
|
|
167
|
+
|
|
168
|
+
`<wcs-ws>` exposes two different kinds of properties.
|
|
169
|
+
|
|
170
|
+
### Output state (bindable async state)
|
|
171
|
+
|
|
172
|
+
These properties represent the current connection state and are the main HAWC surface:
|
|
173
|
+
|
|
174
|
+
| Property | Type | Description |
|
|
175
|
+
|----------|------|-------------|
|
|
176
|
+
| `message` | `any` | Latest received message (JSON auto-parsed) |
|
|
177
|
+
| `connected` | `boolean` | `true` while WebSocket is open |
|
|
178
|
+
| `loading` | `boolean` | `true` while connecting |
|
|
179
|
+
| `error` | `WcsWsError \| Event \| null` | Connection or close error |
|
|
180
|
+
| `readyState` | `number` | WebSocket readyState constant |
|
|
181
|
+
|
|
182
|
+
### Input / command surface
|
|
183
|
+
|
|
184
|
+
These properties control connection and messaging from HTML, JS, or `@wcstack/state` bindings:
|
|
185
|
+
|
|
186
|
+
| Property | Type | Description |
|
|
187
|
+
|----------|------|-------------|
|
|
188
|
+
| `url` | `string` | WebSocket endpoint URL |
|
|
189
|
+
| `trigger` | `boolean` | One-way connection trigger |
|
|
190
|
+
| `send` | `any` | Set to transmit data (auto-stringifies objects) |
|
|
191
|
+
| `manual` | `boolean` | Disables auto-connect on DOM attach |
|
|
192
|
+
|
|
193
|
+
## Architecture
|
|
194
|
+
|
|
195
|
+
`@wcstack/websocket` follows the HAWC architecture.
|
|
196
|
+
|
|
197
|
+
### Core: `WebSocketCore`
|
|
198
|
+
|
|
199
|
+
`WebSocketCore` is a pure `EventTarget` class.
|
|
200
|
+
It contains:
|
|
201
|
+
|
|
202
|
+
- WebSocket connection management
|
|
203
|
+
- automatic reconnection logic
|
|
204
|
+
- JSON message parsing
|
|
205
|
+
- async state transitions
|
|
206
|
+
- `wc-bindable-protocol` declaration
|
|
207
|
+
|
|
208
|
+
It can run headlessly in any runtime that supports `EventTarget` and `WebSocket`.
|
|
209
|
+
|
|
210
|
+
### Shell: `<wcs-ws>`
|
|
211
|
+
|
|
212
|
+
`<wcs-ws>` is a thin `HTMLElement` wrapper around `WebSocketCore`.
|
|
213
|
+
It adds:
|
|
214
|
+
|
|
215
|
+
- attribute / property mapping
|
|
216
|
+
- DOM lifecycle integration
|
|
217
|
+
- declarative helpers: `trigger`, `send`
|
|
218
|
+
|
|
219
|
+
This split keeps the connection logic portable while allowing DOM-based binding systems such as `@wcstack/state` to interact with it naturally.
|
|
220
|
+
|
|
221
|
+
### Target injection
|
|
222
|
+
|
|
223
|
+
The Core dispatches events directly on the Shell via **target injection**, so no event re-dispatch is needed.
|
|
224
|
+
|
|
225
|
+
## Headless Usage (Core only)
|
|
226
|
+
|
|
227
|
+
`WebSocketCore` can be used standalone without the DOM. Since it declares `static wcBindable`, you can use `@wc-bindable/core`'s `bind()` to subscribe to its state:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { WebSocketCore } from "@wcstack/websocket";
|
|
231
|
+
import { bind } from "@wc-bindable/core";
|
|
232
|
+
|
|
233
|
+
const core = new WebSocketCore();
|
|
234
|
+
|
|
235
|
+
const unbind = bind(core, (name, value) => {
|
|
236
|
+
console.log(`${name}:`, value);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
core.connect("wss://example.com/ws", {
|
|
240
|
+
autoReconnect: true,
|
|
241
|
+
reconnectInterval: 5000,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Send a message
|
|
245
|
+
core.send(JSON.stringify({ type: "ping" }));
|
|
246
|
+
|
|
247
|
+
// Clean up
|
|
248
|
+
core.close();
|
|
249
|
+
unbind();
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
This works in Node.js, Deno, Cloudflare Workers — anywhere `EventTarget` and `WebSocket` are available.
|
|
253
|
+
|
|
254
|
+
## URL Observation
|
|
255
|
+
|
|
256
|
+
By default, `<wcs-ws>` automatically opens a connection when:
|
|
257
|
+
|
|
258
|
+
1. it is connected to the DOM and `url` is set
|
|
259
|
+
2. the `url` attribute changes while connected to the DOM
|
|
260
|
+
|
|
261
|
+
Set the `manual` attribute to disable auto-connect and control the connection explicitly via `connect()` or `trigger`.
|
|
262
|
+
|
|
263
|
+
## Programmatic Usage
|
|
264
|
+
|
|
265
|
+
```javascript
|
|
266
|
+
const wsEl = document.querySelector("wcs-ws");
|
|
267
|
+
|
|
268
|
+
// Connect manually
|
|
269
|
+
wsEl.connect();
|
|
270
|
+
|
|
271
|
+
// Send data
|
|
272
|
+
wsEl.sendMessage(JSON.stringify({ type: "chat", content: "hello" }));
|
|
273
|
+
|
|
274
|
+
console.log(wsEl.message); // latest message
|
|
275
|
+
console.log(wsEl.connected); // boolean
|
|
276
|
+
console.log(wsEl.loading); // boolean
|
|
277
|
+
console.log(wsEl.error); // error info or null
|
|
278
|
+
console.log(wsEl.readyState); // WebSocket readyState
|
|
279
|
+
|
|
280
|
+
// Close
|
|
281
|
+
wsEl.close();
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Optional DOM Triggering
|
|
285
|
+
|
|
286
|
+
If `autoTrigger` is enabled (default), clicking an element with `data-wstarget` triggers the corresponding `<wcs-ws>` element:
|
|
287
|
+
|
|
288
|
+
```html
|
|
289
|
+
<button data-wstarget="my-ws">Connect</button>
|
|
290
|
+
<wcs-ws id="my-ws" url="wss://example.com/ws" manual></wcs-ws>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Event delegation is used — works with dynamically added elements. The `closest()` API handles nested elements (e.g., icon inside a button).
|
|
294
|
+
|
|
295
|
+
If the target id does not match any element, or the matched element is not a `<wcs-ws>`, the click is silently ignored.
|
|
296
|
+
|
|
297
|
+
This is a convenience feature.
|
|
298
|
+
In wcstack applications, **state-driven triggering via `trigger`** is usually the primary pattern.
|
|
299
|
+
|
|
300
|
+
## Elements
|
|
301
|
+
|
|
302
|
+
### `<wcs-ws>`
|
|
303
|
+
|
|
304
|
+
| Attribute | Type | Default | Description |
|
|
305
|
+
|-----------|------|---------|-------------|
|
|
306
|
+
| `url` | `string` | — | WebSocket endpoint URL |
|
|
307
|
+
| `protocols` | `string` | — | Comma-separated subprotocol list |
|
|
308
|
+
| `manual` | `boolean` | `false` | Disable auto-connect |
|
|
309
|
+
| `auto-reconnect` | `boolean` | `false` | Enable automatic reconnection |
|
|
310
|
+
| `reconnect-interval` | `number` | `3000` | Reconnection delay in ms |
|
|
311
|
+
| `max-reconnects` | `number` | `Infinity` | Maximum reconnection attempts |
|
|
312
|
+
|
|
313
|
+
| Property | Type | Description |
|
|
314
|
+
|----------|------|-------------|
|
|
315
|
+
| `message` | `any` | Latest received message (JSON auto-parsed) |
|
|
316
|
+
| `connected` | `boolean` | `true` while WebSocket is open |
|
|
317
|
+
| `loading` | `boolean` | `true` while connecting |
|
|
318
|
+
| `error` | `WcsWsError \| Event \| null` | Error info |
|
|
319
|
+
| `readyState` | `number` | WebSocket readyState constant |
|
|
320
|
+
| `trigger` | `boolean` | Set to `true` to open connection |
|
|
321
|
+
| `send` | `any` | Set to transmit data |
|
|
322
|
+
|
|
323
|
+
| Method | Description |
|
|
324
|
+
|--------|-------------|
|
|
325
|
+
| `connect()` | Open the WebSocket connection |
|
|
326
|
+
| `sendMessage(data)` | Send data over the connection |
|
|
327
|
+
| `close(code?, reason?)` | Close the connection |
|
|
328
|
+
|
|
329
|
+
## wc-bindable-protocol
|
|
330
|
+
|
|
331
|
+
Both `WebSocketCore` and `<wcs-ws>` declare `wc-bindable-protocol` compliance, making them interoperable with any framework or component that supports the protocol.
|
|
332
|
+
|
|
333
|
+
### Core (`WebSocketCore`)
|
|
334
|
+
|
|
335
|
+
`WebSocketCore` declares the bindable async state that any runtime can subscribe to:
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
static wcBindable = {
|
|
339
|
+
protocol: "wc-bindable",
|
|
340
|
+
version: 1,
|
|
341
|
+
properties: [
|
|
342
|
+
{ name: "message", event: "wcs-ws:message" },
|
|
343
|
+
{ name: "connected", event: "wcs-ws:connected-changed" },
|
|
344
|
+
{ name: "loading", event: "wcs-ws:loading-changed" },
|
|
345
|
+
{ name: "error", event: "wcs-ws:error" },
|
|
346
|
+
{ name: "readyState", event: "wcs-ws:readystate-changed" },
|
|
347
|
+
],
|
|
348
|
+
};
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Headless consumers call `core.connect(url)` directly — no `trigger` needed.
|
|
352
|
+
|
|
353
|
+
### Shell (`<wcs-ws>`)
|
|
354
|
+
|
|
355
|
+
The Shell extends the Core declaration with `trigger` and `send` so binding systems can control the connection declaratively:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
static wcBindable = {
|
|
359
|
+
...WebSocketCore.wcBindable,
|
|
360
|
+
properties: [
|
|
361
|
+
...WebSocketCore.wcBindable.properties,
|
|
362
|
+
{ name: "trigger", event: "wcs-ws:trigger-changed" },
|
|
363
|
+
{ name: "send", event: "wcs-ws:send-changed" },
|
|
364
|
+
],
|
|
365
|
+
};
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## TypeScript Types
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import type {
|
|
372
|
+
WcsWsError, WcsWsCoreValues, WcsWsValues
|
|
373
|
+
} from "@wcstack/websocket";
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// WebSocket error
|
|
378
|
+
interface WcsWsError {
|
|
379
|
+
code?: number;
|
|
380
|
+
reason?: string;
|
|
381
|
+
message?: string;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Core (headless) — 5 async state properties
|
|
385
|
+
// T defaults to unknown; pass a type argument for typed `message`
|
|
386
|
+
interface WcsWsCoreValues<T = unknown> {
|
|
387
|
+
message: T;
|
|
388
|
+
connected: boolean;
|
|
389
|
+
loading: boolean;
|
|
390
|
+
error: WcsWsError | Event | null;
|
|
391
|
+
readyState: number;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Shell (<wcs-ws>) — extends Core with trigger and send
|
|
395
|
+
interface WcsWsValues<T = unknown> extends WcsWsCoreValues<T> {
|
|
396
|
+
trigger: boolean;
|
|
397
|
+
send: unknown;
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Why this works well with `@wcstack/state`
|
|
402
|
+
|
|
403
|
+
`@wcstack/state` uses path strings as the only contract between UI and state.
|
|
404
|
+
`<wcs-ws>` fits this model naturally:
|
|
405
|
+
|
|
406
|
+
- state determines the `url` or fires `trigger`
|
|
407
|
+
- `<wcs-ws>` opens and manages the connection
|
|
408
|
+
- incoming data arrives as `message`; status as `connected`, `loading`, `error`
|
|
409
|
+
- UI binds to those paths without writing WebSocket glue code
|
|
410
|
+
- outgoing data flows via the `send` property
|
|
411
|
+
|
|
412
|
+
This makes real-time communication look like ordinary state updates.
|
|
413
|
+
|
|
414
|
+
## Framework Integration
|
|
415
|
+
|
|
416
|
+
Since `<wcs-ws>` is HAWC + `wc-bindable-protocol`, it works with any framework through thin adapters from `@wc-bindable/*`.
|
|
417
|
+
|
|
418
|
+
### React
|
|
419
|
+
|
|
420
|
+
```tsx
|
|
421
|
+
import { useWcBindable } from "@wc-bindable/react";
|
|
422
|
+
import type { WcsWsValues } from "@wcstack/websocket";
|
|
423
|
+
|
|
424
|
+
interface ChatMessage { type: string; content: string; }
|
|
425
|
+
|
|
426
|
+
function Chat() {
|
|
427
|
+
const [ref, { message, connected, loading }] =
|
|
428
|
+
useWcBindable<HTMLElement, WcsWsValues<ChatMessage>>();
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<>
|
|
432
|
+
<wcs-ws ref={ref} url="wss://example.com/ws" auto-reconnect />
|
|
433
|
+
{loading && <p>Connecting...</p>}
|
|
434
|
+
{connected && <p>Connected</p>}
|
|
435
|
+
{message && <pre>{JSON.stringify(message)}</pre>}
|
|
436
|
+
</>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Vue
|
|
442
|
+
|
|
443
|
+
```vue
|
|
444
|
+
<script setup lang="ts">
|
|
445
|
+
import { useWcBindable } from "@wc-bindable/vue";
|
|
446
|
+
import type { WcsWsValues } from "@wcstack/websocket";
|
|
447
|
+
|
|
448
|
+
interface ChatMessage { type: string; content: string; }
|
|
449
|
+
|
|
450
|
+
const { ref, values } = useWcBindable<HTMLElement, WcsWsValues<ChatMessage>>();
|
|
451
|
+
</script>
|
|
452
|
+
|
|
453
|
+
<template>
|
|
454
|
+
<wcs-ws :ref="ref" url="wss://example.com/ws" auto-reconnect />
|
|
455
|
+
<p v-if="values.loading">Connecting...</p>
|
|
456
|
+
<p v-else-if="values.connected">Connected</p>
|
|
457
|
+
<pre v-if="values.message">{{ values.message }}</pre>
|
|
458
|
+
</template>
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Svelte
|
|
462
|
+
|
|
463
|
+
```svelte
|
|
464
|
+
<script>
|
|
465
|
+
import { wcBindable } from "@wc-bindable/svelte";
|
|
466
|
+
|
|
467
|
+
let message = $state(null);
|
|
468
|
+
let connected = $state(false);
|
|
469
|
+
</script>
|
|
470
|
+
|
|
471
|
+
<wcs-ws url="wss://example.com/ws" auto-reconnect
|
|
472
|
+
use:wcBindable={{ onUpdate: (name, v) => {
|
|
473
|
+
if (name === "message") message = v;
|
|
474
|
+
if (name === "connected") connected = v;
|
|
475
|
+
}}} />
|
|
476
|
+
|
|
477
|
+
<p>{connected ? "Connected" : "Disconnected"}</p>
|
|
478
|
+
{#if message}
|
|
479
|
+
<pre>{JSON.stringify(message)}</pre>
|
|
480
|
+
{/if}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Solid
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
import { createWcBindable } from "@wc-bindable/solid";
|
|
487
|
+
import type { WcsWsValues } from "@wcstack/websocket";
|
|
488
|
+
|
|
489
|
+
interface ChatMessage { type: string; content: string; }
|
|
490
|
+
|
|
491
|
+
function Chat() {
|
|
492
|
+
const [values, directive] = createWcBindable<WcsWsValues<ChatMessage>>();
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
<>
|
|
496
|
+
<wcs-ws ref={directive} url="wss://example.com/ws" auto-reconnect />
|
|
497
|
+
<Show when={values.connected} fallback={<p>Disconnected</p>}>
|
|
498
|
+
<p>Connected</p>
|
|
499
|
+
</Show>
|
|
500
|
+
<Show when={values.message}>
|
|
501
|
+
<pre>{JSON.stringify(values.message)}</pre>
|
|
502
|
+
</Show>
|
|
503
|
+
</>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Vanilla — `bind()` directly
|
|
509
|
+
|
|
510
|
+
```javascript
|
|
511
|
+
import { bind } from "@wc-bindable/core";
|
|
512
|
+
|
|
513
|
+
const wsEl = document.querySelector("wcs-ws");
|
|
514
|
+
|
|
515
|
+
bind(wsEl, (name, value) => {
|
|
516
|
+
console.log(`${name} changed:`, value);
|
|
517
|
+
});
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
## Configuration
|
|
521
|
+
|
|
522
|
+
```javascript
|
|
523
|
+
import { bootstrapWebSocket } from "@wcstack/websocket";
|
|
524
|
+
|
|
525
|
+
bootstrapWebSocket({
|
|
526
|
+
autoTrigger: true,
|
|
527
|
+
triggerAttribute: "data-wstarget",
|
|
528
|
+
tagNames: {
|
|
529
|
+
ws: "wcs-ws",
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
## Design Notes
|
|
535
|
+
|
|
536
|
+
- `message`, `connected`, `loading`, `error`, and `readyState` are **output state**
|
|
537
|
+
- `url`, `trigger`, and `send` are **input / command surface**
|
|
538
|
+
- `trigger` is intentionally one-way: writing `true` connects, reset emits completion
|
|
539
|
+
- `send` transmits immediately and resets to `null` — set it each time you want to send
|
|
540
|
+
- JSON messages are automatically parsed on receive; objects are auto-stringified on send
|
|
541
|
+
- `manual` is useful when connection timing should be controlled explicitly
|
|
542
|
+
- Auto-reconnect only fires on abnormal close (code other than 1000)
|
|
543
|
+
|
|
544
|
+
## License
|
|
545
|
+
|
|
546
|
+
MIT
|