@silasdevs/transport 1.0.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/LICENSE +21 -0
- package/README.md +509 -0
- package/dist/index.d.ts +396 -0
- package/dist/index.js +571 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Silas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
# @silasdevs/transport
|
|
2
|
+
|
|
3
|
+
> Generic WebSocket transport with injectable protocol schema, unified handler system, and Promise-based messaging.
|
|
4
|
+
|
|
5
|
+
Designed as the communication companion to `@silasdevs/core` (state management). Both libraries work together but are fully decoupled — use either one independently.
|
|
6
|
+
|
|
7
|
+
- **Injectable protocol** — configure wire field names, codes, serialization, and ID generation. No built-in defaults — you define the entire schema.
|
|
8
|
+
- **Channel-optional** — protocols that use named channels (like internal APIs) and channel-less protocols (like WhiteBit, Binance) both work out of the box.
|
|
9
|
+
- **Unified handlers** — persistent (server pushes) and ephemeral (request/response) in a single registry with automatic cleanup.
|
|
10
|
+
- **ID-based routing** — responses are matched to requests by message ID, with a secondary index fallback for protocols that omit channel fields in responses.
|
|
11
|
+
- **Three send modes** — `request()` (Promise), `fire()` (callback), `send()` (fire-and-forget).
|
|
12
|
+
- **Auto-reconnect** — configurable delay, max attempts, and backoff strategy.
|
|
13
|
+
- **Typed events** — lifecycle hooks via a typed event emitter instead of setter functions.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @silasdevs/transport
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createTransport } from '@silasdevs/transport';
|
|
25
|
+
import type { ProtocolSchema } from '@silasdevs/transport';
|
|
26
|
+
|
|
27
|
+
const protocol: ProtocolSchema = {
|
|
28
|
+
fields: {
|
|
29
|
+
requestChannel: 'action', // wire field for channel on outgoing messages
|
|
30
|
+
responseChannel: 'action', // wire field for channel on incoming messages
|
|
31
|
+
messageId: 'reqId', // wire field for the unique message ID
|
|
32
|
+
code: 'status', // wire field for result code
|
|
33
|
+
description: 'desc', // wire field for human-readable description
|
|
34
|
+
payload: 'payload', // wire field for data on outgoing messages
|
|
35
|
+
body: 'payload', // wire field for data on incoming messages
|
|
36
|
+
},
|
|
37
|
+
codes: {
|
|
38
|
+
success: 'OK',
|
|
39
|
+
interim: 'PENDING',
|
|
40
|
+
error: ['FAIL'],
|
|
41
|
+
},
|
|
42
|
+
generateId: () => Math.floor(Math.random() * 1_000_000_000) + 1,
|
|
43
|
+
encode: (msg) => JSON.stringify(msg),
|
|
44
|
+
decode: (raw) => { try { return JSON.parse(raw); } catch { return null; } },
|
|
45
|
+
flattenOutgoing: true,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const transport = createTransport({
|
|
49
|
+
url: 'wss://api.example.com/websocket',
|
|
50
|
+
protocol,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
transport.connect();
|
|
54
|
+
|
|
55
|
+
// Promise-based request
|
|
56
|
+
const res = await transport.request({
|
|
57
|
+
channel: 'usuario',
|
|
58
|
+
data: { id: 5 },
|
|
59
|
+
});
|
|
60
|
+
console.log(res.data); // { usuario: [{ id: 5, nombre: 'Ana' }] }
|
|
61
|
+
|
|
62
|
+
// Persistent handler for server pushes
|
|
63
|
+
transport.addHandler('entrega', 'sync', (msg) => {
|
|
64
|
+
console.log('Delivery update:', msg.data);
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Channel-less Protocols
|
|
69
|
+
|
|
70
|
+
Not all WebSocket APIs use channel fields. For protocols like WhiteBit or Binance where routing is done purely by message ID, simply omit `requestChannel` and `responseChannel`:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const whitebitProtocol: ProtocolSchema = {
|
|
74
|
+
fields: {
|
|
75
|
+
// No requestChannel or responseChannel — routing is ID-based only.
|
|
76
|
+
messageId: 'id',
|
|
77
|
+
code: 'status', // not used by WhiteBit, but required field
|
|
78
|
+
description: 'error',
|
|
79
|
+
payload: 'params',
|
|
80
|
+
body: 'result',
|
|
81
|
+
},
|
|
82
|
+
// No codes — all responses are treated as success.
|
|
83
|
+
generateId: () => Math.floor(Math.random() * 1_000_000_000) + 1,
|
|
84
|
+
encode: (msg) => JSON.stringify(msg),
|
|
85
|
+
decode: (raw) => { try { return JSON.parse(raw); } catch { return null; } },
|
|
86
|
+
flattenOutgoing: false,
|
|
87
|
+
includeIdInRequest: true, // WhiteBit expects the ID on the wire
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const transport = createTransport({
|
|
91
|
+
url: 'wss://api.whitebit.com/ws',
|
|
92
|
+
protocol: whitebitProtocol,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
transport.connect();
|
|
96
|
+
|
|
97
|
+
// No channel needed — just send data
|
|
98
|
+
const res = await transport.request({
|
|
99
|
+
data: { method: 'server.ping', params: [] },
|
|
100
|
+
});
|
|
101
|
+
console.log(res.data); // { result: 'pong' }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Internally, channel-less messages use the wildcard `'*'` for handler routing. You can register persistent handlers on `'*'` to receive spontaneous server pushes:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
transport.addHandler('*', 'push-listener', (msg) => {
|
|
108
|
+
console.log('Server push:', msg.data);
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Send Modes
|
|
113
|
+
|
|
114
|
+
### `request()` — Promise-based
|
|
115
|
+
|
|
116
|
+
Resolves on success, rejects on failure or timeout. Interim responses are handled transparently.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import type { TransportError } from '@silasdevs/transport';
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const res = await transport.request(
|
|
123
|
+
{ channel: 'usuario', data: { id: 5 } },
|
|
124
|
+
{ timeout: 10_000 },
|
|
125
|
+
);
|
|
126
|
+
console.log(res.code); // 'OK'
|
|
127
|
+
console.log(res.data); // server payload
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (err instanceof Error) {
|
|
130
|
+
// Timeout or network error (plain Error).
|
|
131
|
+
console.error('Timeout or connection error:', err.message);
|
|
132
|
+
} else {
|
|
133
|
+
// Protocol-level failure — TransportError shape.
|
|
134
|
+
const e = err as TransportError;
|
|
135
|
+
console.error('Protocol error:', e.code, e.error, e.data);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Timeout behaviour:**
|
|
141
|
+
- `timeout` defaults to `30_000` ms.
|
|
142
|
+
- Pass `timeout: 0` to disable the timeout entirely (request waits indefinitely).
|
|
143
|
+
- Interim responses (`codes.interim`) do **not** reset the timer. The clock starts when `request()` is called and the same deadline applies throughout.
|
|
144
|
+
- On timeout the promise rejects with a plain `Error` (not a `TransportError`), so `err instanceof Error` reliably identifies timeouts.
|
|
145
|
+
- If `disconnect()` is called while a request is pending the promise rejects when the timeout fires (no early rejection).
|
|
146
|
+
|
|
147
|
+
### `fire()` — Callback-based
|
|
148
|
+
|
|
149
|
+
Return `false` to keep the handler alive (interim pattern). The callback receives **every** message including interim ones — unlike `request()` it does not skip them.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const unsub = transport.fire(
|
|
153
|
+
{ channel: 'proceso', data: { id: 1 } },
|
|
154
|
+
(msg) => {
|
|
155
|
+
if (msg.code === 'PENDING') {
|
|
156
|
+
console.log('Still processing...', msg.data);
|
|
157
|
+
return false; // keep listening — handler stays registered
|
|
158
|
+
}
|
|
159
|
+
console.log('Done:', msg.data);
|
|
160
|
+
// return void/true → auto-remove handler
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Cancel early if needed
|
|
165
|
+
unsub();
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
> **Note:** `fire()` has no built-in timeout. Use the returned `unsub()` to cancel if necessary.
|
|
169
|
+
|
|
170
|
+
### `send()` — Fire-and-forget
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
transport.send({ channel: 'ping' });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Handlers
|
|
177
|
+
|
|
178
|
+
Two types, one registry:
|
|
179
|
+
|
|
180
|
+
| Type | Key | Lifetime | Use case |
|
|
181
|
+
|---|---|---|---|
|
|
182
|
+
| **persistent** | string name | Until explicitly removed | Server pushes, entity sync |
|
|
183
|
+
| **ephemeral** | numeric messageId | Auto-removed on definitive response | Request/response pairs |
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// Persistent — receives all 'entrega' pushes (messageId=0)
|
|
187
|
+
const unsub = transport.addHandler('entrega', 'my-sync', (msg) => {
|
|
188
|
+
console.log('Push:', msg.data);
|
|
189
|
+
});
|
|
190
|
+
unsub(); // or transport.removeHandler('entrega', 'my-sync')
|
|
191
|
+
|
|
192
|
+
// Ephemeral — created automatically by request() and fire()
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Ephemeral handlers auto-remove when the callback returns anything other than exactly `false` (including `void`, `true`, `null`, `0`, `""`). Return `false` to keep alive (interim pattern).
|
|
196
|
+
|
|
197
|
+
**Handler execution order:** persistent handlers execute in the order they were registered. Ephemeral handlers take priority over persistent handlers for the same (channel, messageId) pair.
|
|
198
|
+
|
|
199
|
+
### Handler Routing
|
|
200
|
+
|
|
201
|
+
Messages are routed through a 4-step priority chain:
|
|
202
|
+
|
|
203
|
+
1. **Ephemeral by (channel, messageId)** — exact match for request/response pairs
|
|
204
|
+
2. **Persistent by channel** — all matching handlers execute
|
|
205
|
+
3. **ID-only fallback** — if the message has a `messageId` but no matching channel, a secondary index resolves the original channel (useful when responses omit the channel field)
|
|
206
|
+
4. **Unhandled** — emits `message:unhandled` event
|
|
207
|
+
|
|
208
|
+
## Protocol Schema
|
|
209
|
+
|
|
210
|
+
You must provide a `ProtocolSchema` when creating a transport. There are no built-in defaults.
|
|
211
|
+
|
|
212
|
+
### `ProtocolFields`
|
|
213
|
+
|
|
214
|
+
Maps canonical field names to actual wire field names:
|
|
215
|
+
|
|
216
|
+
| Field | Required | Description |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `requestChannel` | No | Wire field for channel on outgoing messages. Omit for channel-less protocols. |
|
|
219
|
+
| `responseChannel` | No | Wire field for channel on incoming messages. Omit for channel-less protocols. |
|
|
220
|
+
| `subscriptionChannel` | No | Fallback channel field for subscription/event messages (e.g. Binance `"e"`). |
|
|
221
|
+
| `messageId` | Yes | Wire field for the unique message ID. |
|
|
222
|
+
| `code` | Yes | Wire field for the result code. |
|
|
223
|
+
| `description` | Yes | Wire field for human-readable description. |
|
|
224
|
+
| `payload` | Yes | Wire field for data on outgoing messages. |
|
|
225
|
+
| `body` | Yes | Wire field for data on incoming messages. |
|
|
226
|
+
|
|
227
|
+
### `ProtocolCodes`
|
|
228
|
+
|
|
229
|
+
All fields are optional. When the entire `codes` object is omitted, all responses resolve immediately:
|
|
230
|
+
|
|
231
|
+
| Field | Type | Description |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `success` | `string` | Value indicating success. When undefined, all non-interim/non-error responses succeed. |
|
|
234
|
+
| `interim` | `string` | Value indicating an interim/partial response (keep listening). |
|
|
235
|
+
| `error` | `string[]` | Value(s) indicating an error. Multiple codes supported. |
|
|
236
|
+
|
|
237
|
+
### `ProtocolSchema`
|
|
238
|
+
|
|
239
|
+
| Field | Required | Description |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| `fields` | Yes | Maps canonical field names to wire field names. |
|
|
242
|
+
| `codes` | No | Special result code values for classification. |
|
|
243
|
+
| `generateId` | Yes | Function that generates a unique numeric message ID. |
|
|
244
|
+
| `encode` | Yes | Serialize a message object to a string for the wire. |
|
|
245
|
+
| `decode` | Yes | Deserialize a raw wire string to an object (return `null` on failure). |
|
|
246
|
+
| `flattenOutgoing` | Yes | `true` = spread data onto root; `false` = nest under payload field. |
|
|
247
|
+
| `includeIdInRequest` | No | `true` = include messageId on the wire; `false` (default) = ID used internally only. |
|
|
248
|
+
|
|
249
|
+
### Example: Channel-based Protocol
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
const protocol: ProtocolSchema = {
|
|
253
|
+
fields: {
|
|
254
|
+
requestChannel: 'action',
|
|
255
|
+
responseChannel: 'action',
|
|
256
|
+
messageId: 'reqId',
|
|
257
|
+
code: 'status',
|
|
258
|
+
description: 'desc',
|
|
259
|
+
payload: 'payload',
|
|
260
|
+
body: 'payload',
|
|
261
|
+
},
|
|
262
|
+
codes: {
|
|
263
|
+
success: 'OK',
|
|
264
|
+
interim: 'PENDING',
|
|
265
|
+
error: ['ERROR'],
|
|
266
|
+
},
|
|
267
|
+
generateId: () => Math.floor(Math.random() * 1_000_000_000) + 1,
|
|
268
|
+
encode: (msg) => JSON.stringify(msg),
|
|
269
|
+
decode: (raw) => { try { return JSON.parse(raw); } catch { return null; } },
|
|
270
|
+
flattenOutgoing: true,
|
|
271
|
+
};
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Example: Channel-less Protocol (WhiteBit-style)
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
const protocol: ProtocolSchema = {
|
|
278
|
+
fields: {
|
|
279
|
+
messageId: 'id',
|
|
280
|
+
code: 'status',
|
|
281
|
+
description: 'error',
|
|
282
|
+
payload: 'params',
|
|
283
|
+
body: 'result',
|
|
284
|
+
},
|
|
285
|
+
generateId: () => Math.floor(Math.random() * 1_000_000_000) + 1,
|
|
286
|
+
encode: (msg) => JSON.stringify(msg),
|
|
287
|
+
decode: (raw) => { try { return JSON.parse(raw); } catch { return null; } },
|
|
288
|
+
flattenOutgoing: false,
|
|
289
|
+
includeIdInRequest: true,
|
|
290
|
+
};
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Wire Formats
|
|
294
|
+
|
|
295
|
+
**Outgoing (channel-based, data flattened)**:
|
|
296
|
+
```json
|
|
297
|
+
{ "action": "usuario", "reqId": 742381923, "id": 5, "nombre": "Ana" }
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Outgoing (channel-based, data nested)**:
|
|
301
|
+
```json
|
|
302
|
+
{ "action": "usuario", "reqId": 742381923, "payload": { "id": 5, "nombre": "Ana" } }
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Outgoing (channel-less)**:
|
|
306
|
+
```json
|
|
307
|
+
{ "id": 742381923, "params": { "method": "server.ping" } }
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Incoming (channel-based)**:
|
|
311
|
+
```json
|
|
312
|
+
{
|
|
313
|
+
"action": "usuario",
|
|
314
|
+
"reqId": 742381923,
|
|
315
|
+
"status": "OK",
|
|
316
|
+
"desc": "Success",
|
|
317
|
+
"payload": { "usuario": [{ "id": 5, "nombre": "Ana" }] }
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Incoming (channel-less)**:
|
|
322
|
+
```json
|
|
323
|
+
{ "id": 742381923, "result": { "pong": true } }
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Events
|
|
327
|
+
|
|
328
|
+
Typed lifecycle events:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
transport.on('connected', (evt) => console.log('Connected'));
|
|
332
|
+
transport.on('disconnected', ({ code, reason }) => console.log('Disconnected:', code));
|
|
333
|
+
transport.on('reconnecting', ({ attempt, delayMs }) => console.log(`Retry #${attempt}`));
|
|
334
|
+
transport.on('error', (evt) => console.error('WS error'));
|
|
335
|
+
|
|
336
|
+
transport.on('message:raw', ({ data }) => console.log('Raw:', data));
|
|
337
|
+
transport.on('message:parsed', (msg) => console.log('Parsed:', msg.channel));
|
|
338
|
+
transport.on('message:unhandled', (msg) => console.log('Unhandled:', msg.channel));
|
|
339
|
+
|
|
340
|
+
transport.on('send:before', ({ payload }) => console.log('Sending:', payload));
|
|
341
|
+
transport.on('send:after', ({ payload }) => console.log('Sent:', payload));
|
|
342
|
+
transport.on('send:error', ({ reason }) => console.error('Send failed:', reason));
|
|
343
|
+
|
|
344
|
+
// All .on() calls return an unsubscribe function
|
|
345
|
+
const unsub = transport.on('connected', handler);
|
|
346
|
+
unsub();
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Reconnection
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
const transport = createTransport({
|
|
353
|
+
url: 'wss://api.example.com/ws',
|
|
354
|
+
protocol,
|
|
355
|
+
reconnect: {
|
|
356
|
+
auto: true, // default: true
|
|
357
|
+
delayMs: 10_000, // default: 10s
|
|
358
|
+
maxAttempts: Infinity, // default: Infinity
|
|
359
|
+
backoff: 'fixed', // 'fixed' | 'exponential' (default: 'fixed')
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Disable reconnection
|
|
364
|
+
const transport2 = createTransport({
|
|
365
|
+
url: 'wss://...',
|
|
366
|
+
protocol,
|
|
367
|
+
reconnect: false,
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Backoff strategies:**
|
|
372
|
+
- `'fixed'` — waits exactly `delayMs` between every attempt.
|
|
373
|
+
- `'exponential'` — delay doubles each attempt: `delayMs × 2^attempt`, capped at **60 seconds**.
|
|
374
|
+
- Example with `delayMs: 1000`: 1 s → 2 s → 4 s → 8 s → … → 60 s
|
|
375
|
+
|
|
376
|
+
**`disconnect({ clean: true })`** — setting `clean: true` additionally clears all stale ephemeral handlers (request/response pairs that will never resolve). Without `clean`, persistent handlers remain registered.
|
|
377
|
+
|
|
378
|
+
**Reconnect on send failure:** if `send()` is called while the socket is closed or closing and `reconnect.auto` is `true`, a reconnect attempt is scheduled with a 1 s delay.
|
|
379
|
+
|
|
380
|
+
## Debug Logging
|
|
381
|
+
|
|
382
|
+
Pass `debug: true` to enable console logging for all connection lifecycle events, sends, receives, and handler routing:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
const transport = createTransport({
|
|
386
|
+
url: 'wss://api.example.com/ws',
|
|
387
|
+
protocol,
|
|
388
|
+
debug: true, // logs to console.log with '[silas/transport]' prefix
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Toggle at runtime
|
|
392
|
+
transport.debug(true);
|
|
393
|
+
transport.debug(false);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Logged events include: connecting, connected, disconnected, reconnecting, send (before/after/error), message received, message decoded, handler matched/unmatched.
|
|
397
|
+
|
|
398
|
+
## Channel-less Protocol Gotcha
|
|
399
|
+
|
|
400
|
+
If your protocol has no `responseChannel` defined, incoming messages always resolve to channel `'*'`. Persistent handlers registered on a **named** channel will **never** fire for those messages:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
// ❌ Will never fire — no responseChannel means all messages arrive on '*'.
|
|
404
|
+
transport.addHandler('priceUpdate', 'prices', (msg) => { /* ... */ });
|
|
405
|
+
|
|
406
|
+
// ✅ Register on '*' to receive all channel-less pushes.
|
|
407
|
+
transport.addHandler('*', 'prices', (msg) => { /* ... */ });
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Integration with @silasdevs/core
|
|
411
|
+
|
|
412
|
+
The bridge lives in the consumer, not in either library:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
import { createTransport } from '@silasdevs/transport';
|
|
416
|
+
import { createStore, defineSchema } from '@silasdevs/core/store';
|
|
417
|
+
|
|
418
|
+
const store = createStore({
|
|
419
|
+
schema: defineSchema({
|
|
420
|
+
tables: {
|
|
421
|
+
usuario: { key: 'id', version: 'version' },
|
|
422
|
+
entrega: { key: 'id', version: 'version' },
|
|
423
|
+
},
|
|
424
|
+
}),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const transport = createTransport({
|
|
428
|
+
url: 'wss://api.example.com/ws',
|
|
429
|
+
protocol,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Bridge: classify incoming data into the store
|
|
433
|
+
transport.on('message:parsed', (msg) => {
|
|
434
|
+
if (msg.data) {
|
|
435
|
+
store.classify(msg.data);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
transport.connect();
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## API Reference
|
|
443
|
+
|
|
444
|
+
### Factory
|
|
445
|
+
|
|
446
|
+
| Export | Description |
|
|
447
|
+
|---|---|
|
|
448
|
+
| `createTransport(opts)` | Create a Transport instance |
|
|
449
|
+
|
|
450
|
+
### Transport Instance
|
|
451
|
+
|
|
452
|
+
| Method | Description |
|
|
453
|
+
|---|---|
|
|
454
|
+
| `connect()` | Open WebSocket (idempotent) |
|
|
455
|
+
| `disconnect({ clean? })` | Close WebSocket |
|
|
456
|
+
| `request(msg, opts?)` | Promise-based send |
|
|
457
|
+
| `fire(msg, cb, opts?)` | Callback-based send |
|
|
458
|
+
| `send(msg)` | Fire-and-forget send |
|
|
459
|
+
| `addHandler(channel, name, cb)` | Register persistent handler |
|
|
460
|
+
| `removeHandler(channel, name)` | Remove persistent handler |
|
|
461
|
+
| `on(event, cb)` | Subscribe to lifecycle event |
|
|
462
|
+
| `once(event, cb)` | Subscribe once |
|
|
463
|
+
| `debug(enabled)` | Toggle debug logging |
|
|
464
|
+
| `destroy()` | Disconnect + cleanup everything |
|
|
465
|
+
| `state` | Current `TransportState` (readonly) |
|
|
466
|
+
| `protocol` | Resolved `ProtocolSchema` (readonly) |
|
|
467
|
+
|
|
468
|
+
### Protocol
|
|
469
|
+
|
|
470
|
+
| Export | Description |
|
|
471
|
+
|---|---|
|
|
472
|
+
| `normalizeIncoming(raw, schema)` | Wire → `IncomingMessage` |
|
|
473
|
+
| `buildOutgoing(msg, id, schema)` | `OutgoingMessage` → wire |
|
|
474
|
+
|
|
475
|
+
### Utilities
|
|
476
|
+
|
|
477
|
+
| Export | Description |
|
|
478
|
+
|---|---|
|
|
479
|
+
| `createEmitter<T>()` | Typed event emitter factory |
|
|
480
|
+
| `createHandlerStore()` | Handler registry factory |
|
|
481
|
+
|
|
482
|
+
### Types
|
|
483
|
+
|
|
484
|
+
All types are exported for consumers:
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
import type {
|
|
488
|
+
ProtocolSchema,
|
|
489
|
+
ProtocolFields,
|
|
490
|
+
ProtocolCodes,
|
|
491
|
+
IncomingMessage,
|
|
492
|
+
OutgoingMessage,
|
|
493
|
+
Handler,
|
|
494
|
+
HandlerCallback,
|
|
495
|
+
Transport,
|
|
496
|
+
TransportOptions,
|
|
497
|
+
TransportState,
|
|
498
|
+
TransportEvents,
|
|
499
|
+
TransportError,
|
|
500
|
+
TransportEmitter,
|
|
501
|
+
ReconnectOptions,
|
|
502
|
+
RequestOptions,
|
|
503
|
+
FireOptions,
|
|
504
|
+
} from '@silasdevs/transport';
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## License
|
|
508
|
+
|
|
509
|
+
MIT © Silas
|