@wcstack/websocket 1.9.1 → 1.10.4

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 CHANGED
@@ -1,546 +1,602 @@
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 [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md) (Core / Shell / Binding Contract) architecture:
16
+
17
+ - **Core** (`WebSocketCore`) handles connection, messaging, reconnection, and async state
18
+ - **Shell** (`<wcs-ws>`) connects that state to DOM attributes, lifecycle, and declarative commands
19
+ - **Binding Contract** (`static wcBindable`) declares observable `properties`, writable `inputs`, and callable `commands`
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 observable 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 CSBC 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 for observable state and callable commands
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
+ - `wc-bindable-protocol` inputs for DOM-facing configuration and command properties
219
+
220
+ This split keeps the connection logic portable while allowing DOM-based binding systems such as `@wcstack/state` to interact with it naturally.
221
+
222
+ ### Target injection
223
+
224
+ The Core dispatches events directly on the Shell via **target injection**, so no event re-dispatch is needed.
225
+
226
+ ## Headless Usage (Core only)
227
+
228
+ `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:
229
+
230
+ ```typescript
231
+ import { WebSocketCore } from "@wcstack/websocket";
232
+ import { bind } from "@wc-bindable/core";
233
+
234
+ const core = new WebSocketCore();
235
+
236
+ const unbind = bind(core, (name, value) => {
237
+ console.log(`${name}:`, value);
238
+ });
239
+
240
+ core.connect("wss://example.com/ws", {
241
+ autoReconnect: true,
242
+ reconnectInterval: 5000,
243
+ });
244
+
245
+ // Send a message
246
+ core.send(JSON.stringify({ type: "ping" }));
247
+
248
+ // Clean up
249
+ core.close();
250
+ unbind();
251
+ ```
252
+
253
+ This works in Node.js, Deno, Cloudflare Workers — anywhere `EventTarget` and `WebSocket` are available.
254
+
255
+ ## URL Observation
256
+
257
+ By default, `<wcs-ws>` automatically opens a connection when:
258
+
259
+ 1. it is connected to the DOM and `url` is set
260
+ 2. the `url` attribute changes while connected to the DOM
261
+
262
+ Set the `manual` attribute to disable auto-connect and control the connection explicitly via `connect()` or `trigger`.
263
+
264
+ ## Programmatic Usage
265
+
266
+ ```javascript
267
+ const wsEl = document.querySelector("wcs-ws");
268
+
269
+ // Connect manually
270
+ wsEl.connect();
271
+
272
+ // Send data
273
+ wsEl.sendMessage(JSON.stringify({ type: "chat", content: "hello" }));
274
+
275
+ console.log(wsEl.message); // latest message
276
+ console.log(wsEl.connected); // boolean
277
+ console.log(wsEl.loading); // boolean
278
+ console.log(wsEl.error); // error info or null
279
+ console.log(wsEl.readyState); // WebSocket readyState
280
+
281
+ // Close
282
+ wsEl.close();
283
+ ```
284
+
285
+ ## Optional DOM Triggering
286
+
287
+ If `autoTrigger` is enabled (default), clicking an element with `data-wstarget` triggers the corresponding `<wcs-ws>` element:
288
+
289
+ ```html
290
+ <button data-wstarget="my-ws">Connect</button>
291
+ <wcs-ws id="my-ws" url="wss://example.com/ws" manual></wcs-ws>
292
+ ```
293
+
294
+ Event delegation is used — works with dynamically added elements. The `closest()` API handles nested elements (e.g., icon inside a button).
295
+
296
+ If the target id does not match any element, or the matched element is not a `<wcs-ws>`, the click is silently ignored.
297
+
298
+ This is a convenience feature.
299
+ In wcstack applications, **state-driven triggering via `trigger`** is usually the primary pattern.
300
+
301
+ ## Elements
302
+
303
+ ### `<wcs-ws>`
304
+
305
+ | Attribute | Type | Default | Description |
306
+ |-----------|------|---------|-------------|
307
+ | `url` | `string` | — | WebSocket endpoint URL |
308
+ | `protocols` | `string` | | Comma-separated subprotocol list |
309
+ | `manual` | `boolean` | `false` | Disable auto-connect |
310
+ | `auto-reconnect` | `boolean` | `false` | Enable automatic reconnection |
311
+ | `reconnect-interval` | `number` | `3000` | Reconnection delay in ms |
312
+ | `max-reconnects` | `number` | `Infinity` | Maximum reconnection attempts |
313
+
314
+ | Property | Type | Description |
315
+ |----------|------|-------------|
316
+ | `message` | `any` | Latest received message (JSON auto-parsed) |
317
+ | `connected` | `boolean` | `true` while WebSocket is open |
318
+ | `loading` | `boolean` | `true` while connecting |
319
+ | `error` | `WcsWsError \| Event \| null` | Error info |
320
+ | `readyState` | `number` | WebSocket readyState constant |
321
+ | `trigger` | `boolean` | Set to `true` to open connection |
322
+ | `send` | `any` | Set to transmit data |
323
+
324
+ | Method | Description |
325
+ |--------|-------------|
326
+ | `connect()` | Open the WebSocket connection |
327
+ | `sendMessage(data)` | Send data over the connection |
328
+ | `close(code?, reason?)` | Close the connection |
329
+
330
+ ## wc-bindable-protocol
331
+
332
+ Both `WebSocketCore` and `<wcs-ws>` declare a `wc-bindable-protocol` contract, making them interoperable with any framework, adapter, remote proxy, or tooling layer that understands the protocol.
333
+
334
+ The current contract has three independent surfaces:
335
+
336
+ - `properties`: observable outputs consumed by `bind()` and framework adapters
337
+ - `inputs`: writable interface metadata for DOM configuration and command-like properties
338
+ - `commands`: callable methods for tools and remote-capable consumers
339
+
340
+ ### Core (`WebSocketCore`)
341
+
342
+ `WebSocketCore` declares the bindable async state that any runtime can subscribe to:
343
+
344
+ ```typescript
345
+ static wcBindable = {
346
+ protocol: "wc-bindable",
347
+ version: 1,
348
+ properties: [
349
+ { name: "message", event: "wcs-ws:message" },
350
+ { name: "connected", event: "wcs-ws:connected-changed" },
351
+ { name: "loading", event: "wcs-ws:loading-changed" },
352
+ { name: "error", event: "wcs-ws:error" },
353
+ { name: "readyState", event: "wcs-ws:readystate-changed" },
354
+ ],
355
+ commands: [
356
+ { name: "connect" },
357
+ { name: "send" },
358
+ { name: "close" },
359
+ ],
360
+ };
361
+ ```
362
+
363
+ Headless consumers call `core.connect(url)` directly — no `trigger` needed. The Core does not declare `inputs` because it does not expose settable `url` / option properties; those values are provided through the `connect()` command.
364
+
365
+ ### Shell (`<wcs-ws>`)
366
+
367
+ The Shell extends the Core declaration with DOM-facing inputs, command properties, and HTMLElement methods so binding systems can control the connection declaratively:
368
+
369
+ ```typescript
370
+ static wcBindable = {
371
+ ...WebSocketCore.wcBindable,
372
+ properties: [
373
+ ...WebSocketCore.wcBindable.properties,
374
+ { name: "trigger", event: "wcs-ws:trigger-changed" },
375
+ { name: "send", event: "wcs-ws:send-changed" },
376
+ ],
377
+ inputs: [
378
+ { name: "url", attribute: "url" },
379
+ { name: "protocols", attribute: "protocols" },
380
+ { name: "autoReconnect", attribute: "auto-reconnect" },
381
+ { name: "reconnectInterval", attribute: "reconnect-interval" },
382
+ { name: "maxReconnects", attribute: "max-reconnects" },
383
+ { name: "manual", attribute: "manual" },
384
+ { name: "trigger" },
385
+ { name: "send" },
386
+ ],
387
+ commands: [
388
+ { name: "connect" },
389
+ { name: "sendMessage" },
390
+ { name: "close" },
391
+ ],
392
+ };
393
+ ```
394
+
395
+ ## TypeScript Types
396
+
397
+ ```typescript
398
+ import type {
399
+ WcsWsError, WcsWsCoreValues, WcsWsValues,
400
+ WcsWsInputs, WcsWsCoreCommands, WcsWsCommands
401
+ } from "@wcstack/websocket";
402
+ ```
403
+
404
+ ```typescript
405
+ // WebSocket error
406
+ interface WcsWsError {
407
+ code?: number;
408
+ reason?: string;
409
+ message?: string;
410
+ }
411
+
412
+ // Core (headless) 5 async state properties
413
+ // T defaults to unknown; pass a type argument for typed `message`
414
+ interface WcsWsCoreValues<T = unknown> {
415
+ message: T;
416
+ connected: boolean;
417
+ loading: boolean;
418
+ error: WcsWsError | Event | null;
419
+ readyState: number;
420
+ }
421
+
422
+ // Shell (<wcs-ws>) extends Core with trigger and send
423
+ interface WcsWsValues<T = unknown> extends WcsWsCoreValues<T> {
424
+ trigger: boolean;
425
+ send: unknown;
426
+ }
427
+
428
+ interface WcsWsInputs {
429
+ url: string;
430
+ protocols: string;
431
+ autoReconnect: boolean;
432
+ reconnectInterval: number;
433
+ maxReconnects: number;
434
+ manual: boolean;
435
+ trigger: boolean;
436
+ send: unknown;
437
+ }
438
+
439
+ interface WcsWsCoreCommands {
440
+ connect(url: string, options?: {
441
+ protocols?: string | string[];
442
+ autoReconnect?: boolean;
443
+ reconnectInterval?: number;
444
+ maxReconnects?: number;
445
+ }): void;
446
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
447
+ close(code?: number, reason?: string): void;
448
+ }
449
+
450
+ interface WcsWsCommands {
451
+ connect(): void;
452
+ sendMessage(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
453
+ close(code?: number, reason?: string): void;
454
+ }
455
+ ```
456
+
457
+ ## Why this works well with `@wcstack/state`
458
+
459
+ `@wcstack/state` uses path strings as the only contract between UI and state.
460
+ `<wcs-ws>` fits this model naturally:
461
+
462
+ - state determines the `url` or fires `trigger`
463
+ - `<wcs-ws>` opens and manages the connection
464
+ - incoming data arrives as `message`; status as `connected`, `loading`, `error`
465
+ - UI binds to those paths without writing WebSocket glue code
466
+ - outgoing data flows via the `send` property
467
+
468
+ This makes real-time communication look like ordinary state updates.
469
+
470
+ ## Framework Integration
471
+
472
+ Since `<wcs-ws>` exposes a CSBC `wc-bindable-protocol` contract, it works with any framework through thin adapters from `@wc-bindable/*`.
473
+
474
+ ### React
475
+
476
+ ```tsx
477
+ import { useWcBindable } from "@wc-bindable/react";
478
+ import type { WcsWsValues } from "@wcstack/websocket";
479
+
480
+ interface ChatMessage { type: string; content: string; }
481
+
482
+ function Chat() {
483
+ const [ref, { message, connected, loading }] =
484
+ useWcBindable<HTMLElement, WcsWsValues<ChatMessage>>();
485
+
486
+ return (
487
+ <>
488
+ <wcs-ws ref={ref} url="wss://example.com/ws" auto-reconnect />
489
+ {loading && <p>Connecting...</p>}
490
+ {connected && <p>Connected</p>}
491
+ {message && <pre>{JSON.stringify(message)}</pre>}
492
+ </>
493
+ );
494
+ }
495
+ ```
496
+
497
+ ### Vue
498
+
499
+ ```vue
500
+ <script setup lang="ts">
501
+ import { useWcBindable } from "@wc-bindable/vue";
502
+ import type { WcsWsValues } from "@wcstack/websocket";
503
+
504
+ interface ChatMessage { type: string; content: string; }
505
+
506
+ const { ref, values } = useWcBindable<HTMLElement, WcsWsValues<ChatMessage>>();
507
+ </script>
508
+
509
+ <template>
510
+ <wcs-ws :ref="ref" url="wss://example.com/ws" auto-reconnect />
511
+ <p v-if="values.loading">Connecting...</p>
512
+ <p v-else-if="values.connected">Connected</p>
513
+ <pre v-if="values.message">{{ values.message }}</pre>
514
+ </template>
515
+ ```
516
+
517
+ ### Svelte
518
+
519
+ ```svelte
520
+ <script>
521
+ import { wcBindable } from "@wc-bindable/svelte";
522
+
523
+ let message = $state(null);
524
+ let connected = $state(false);
525
+ </script>
526
+
527
+ <wcs-ws url="wss://example.com/ws" auto-reconnect
528
+ use:wcBindable={{ onUpdate: (name, v) => {
529
+ if (name === "message") message = v;
530
+ if (name === "connected") connected = v;
531
+ }}} />
532
+
533
+ <p>{connected ? "Connected" : "Disconnected"}</p>
534
+ {#if message}
535
+ <pre>{JSON.stringify(message)}</pre>
536
+ {/if}
537
+ ```
538
+
539
+ ### Solid
540
+
541
+ ```tsx
542
+ import { createWcBindable } from "@wc-bindable/solid";
543
+ import type { WcsWsValues } from "@wcstack/websocket";
544
+
545
+ interface ChatMessage { type: string; content: string; }
546
+
547
+ function Chat() {
548
+ const [values, directive] = createWcBindable<WcsWsValues<ChatMessage>>();
549
+
550
+ return (
551
+ <>
552
+ <wcs-ws ref={directive} url="wss://example.com/ws" auto-reconnect />
553
+ <Show when={values.connected} fallback={<p>Disconnected</p>}>
554
+ <p>Connected</p>
555
+ </Show>
556
+ <Show when={values.message}>
557
+ <pre>{JSON.stringify(values.message)}</pre>
558
+ </Show>
559
+ </>
560
+ );
561
+ }
562
+ ```
563
+
564
+ ### Vanilla — `bind()` directly
565
+
566
+ ```javascript
567
+ import { bind } from "@wc-bindable/core";
568
+
569
+ const wsEl = document.querySelector("wcs-ws");
570
+
571
+ bind(wsEl, (name, value) => {
572
+ console.log(`${name} changed:`, value);
573
+ });
574
+ ```
575
+
576
+ ## Configuration
577
+
578
+ ```javascript
579
+ import { bootstrapWebSocket } from "@wcstack/websocket";
580
+
581
+ bootstrapWebSocket({
582
+ autoTrigger: true,
583
+ triggerAttribute: "data-wstarget",
584
+ tagNames: {
585
+ ws: "wcs-ws",
586
+ },
587
+ });
588
+ ```
589
+
590
+ ## Design Notes
591
+
592
+ - `message`, `connected`, `loading`, `error`, and `readyState` are **output state**
593
+ - `url`, `trigger`, and `send` are **input / command surface**
594
+ - `trigger` is intentionally one-way: writing `true` connects, reset emits completion
595
+ - `send` transmits immediately and resets to `null` — set it each time you want to send
596
+ - JSON messages are automatically parsed on receive; objects are auto-stringified on send
597
+ - `manual` is useful when connection timing should be controlled explicitly
598
+ - Auto-reconnect only fires on abnormal close (code other than 1000)
599
+
600
+ ## License
601
+
602
+ MIT