yinzerflow 0.7.0 → 0.8.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/CHANGELOG.md +22 -0
- package/docs/core/websockets.md +343 -0
- package/index.d.ts +269 -0
- package/index.js +33 -21
- package/index.js.map +16 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-04-24
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **WebSocket RFC 6455 frame protocol implementation** — Added complete frame protocol support with constants and type definitions for RFC 6455 compliance (4237a80)
|
|
8
|
+
- **WebSocket handshake validation and connection class** — Implemented handshake validation logic and connection management class for WebSocket clients (6b4ca03)
|
|
9
|
+
- **WebSocket framework integration with hooks and configuration** — Integrated WebSocket support into YinzerFlow with lifecycle hooks and configurable options (d344156)
|
|
10
|
+
- **WebSocket channel pub/sub with encode-once broadcast** — Added publish/subscribe messaging system for WebSocket channels with efficient broadcast encoding (97de59a)
|
|
11
|
+
- **WebSocket security controls and integration tests** — Implemented security controls and comprehensive integration test suite (3e6a2f5)
|
|
12
|
+
- **WebSocket exports, documentation and verification** — Added public API exports, documentation, and verification sweep across the WebSocket implementation (3bbfb6a)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- **Fix strict TypeScript build errors with exactOptionalPropertyTypes** — Resolved strict TypeScript compilation errors related to optional property types in WebSocket implementation (0e43ea7)
|
|
17
|
+
- **Security hardening and naming convention improvements** — Applied security hardening measures, refactored test helpers to follow DRY principles, and improved naming conventions for consistency (bbe71b8)
|
|
18
|
+
|
|
19
|
+
### Internal
|
|
20
|
+
|
|
21
|
+
- **Format WebSocket exports for readability** — Improved code formatting and organization of WebSocket exports (ebfe6d3)
|
|
22
|
+
- **Improve YinzerFlow code formatting** — Enhanced overall code formatting and consistency across the framework (1bcd868)
|
|
23
|
+
- **Format WebSocket code consistently** — Applied consistent code formatting standards to WebSocket implementation (3e76ffc)
|
|
24
|
+
|
|
3
25
|
## [0.7.0] - 2026-02-20
|
|
4
26
|
|
|
5
27
|
### Bug Fixes
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# 📖 WebSockets
|
|
2
|
+
|
|
3
|
+
YinzerFlow includes production-level WebSocket support built on raw RFC 6455 — the same TCP-level approach as the HTTP server. No external dependencies.
|
|
4
|
+
|
|
5
|
+
- **Channel-based pub/sub** with encode-once broadcast (frame encoded once, raw bytes to all subscribers)
|
|
6
|
+
- **Per-socket typed data** — generics flow through all handlers
|
|
7
|
+
- **Backpressure handling** — `'buffer'` (safe default) or `'drop'` (for real-time data like trading quotes)
|
|
8
|
+
- **Hook integration** — `wsBeforeMessage` / `wsAfterMessage` for cross-cutting concerns
|
|
9
|
+
- **Security** — origin validation, per-IP connection limits
|
|
10
|
+
- **Zero overhead** when no `app.ws()` routes registered
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { YinzerFlow } from 'yinzerflow';
|
|
14
|
+
|
|
15
|
+
const app = new YinzerFlow({ port: 3000 });
|
|
16
|
+
|
|
17
|
+
app.ws('/chat', {
|
|
18
|
+
open(ws) {
|
|
19
|
+
ws.subscribe('general');
|
|
20
|
+
},
|
|
21
|
+
message(ws, data) {
|
|
22
|
+
ws.publish('general', data as string);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await app.listen();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
# ⚙️ Usage
|
|
30
|
+
|
|
31
|
+
## 🎛️ Settings
|
|
32
|
+
|
|
33
|
+
### websocket.maxPayloadLength — @default <span style="color: #2ecc71">`16777216`</span> (16MB)
|
|
34
|
+
|
|
35
|
+
Maximum incoming message payload in bytes. Frames exceeding this limit trigger a close with code 1009.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
const app = new YinzerFlow({
|
|
39
|
+
websocket: {
|
|
40
|
+
maxPayloadLength: 1_048_576, // 1MB
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### websocket.idleTimeout — @default <span style="color: #2ecc71">`120`</span> (seconds)
|
|
46
|
+
|
|
47
|
+
Seconds of inactivity before closing the connection. Set to `0` to disable.
|
|
48
|
+
|
|
49
|
+
<span style="color: #f39c12">**⚡ Performance:**</span> For high-frequency data streams (trading quotes), the idle timeout resets on every received frame — frequent data prevents timeout.
|
|
50
|
+
|
|
51
|
+
### websocket.maxConnectionsPerIp — @default <span style="color: #2ecc71">`50`</span>
|
|
52
|
+
|
|
53
|
+
Maximum concurrent WebSocket connections from a single IP address.
|
|
54
|
+
|
|
55
|
+
### websocket.allowedOrigins — @default <span style="color: #2ecc71">`[]`</span> (allow all)
|
|
56
|
+
|
|
57
|
+
List of allowed origins for upgrade requests. Empty array allows all origins (including missing origin headers from non-browser clients).
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const app = new YinzerFlow({
|
|
61
|
+
websocket: {
|
|
62
|
+
allowedOrigins: ['https://app.example.com', 'https://admin.example.com'],
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### websocket.backpressure.strategy — @default <span style="color: #2ecc71">`'buffer'`</span>
|
|
68
|
+
|
|
69
|
+
How to handle outgoing data when the client can't keep up:
|
|
70
|
+
|
|
71
|
+
<aside>
|
|
72
|
+
|
|
73
|
+
Options: `'buffer' | 'drop'`
|
|
74
|
+
|
|
75
|
+
- `'buffer'`: Queue messages up to `limit` bytes, then close the connection. Safe default — no data loss unless the client is overwhelmed.
|
|
76
|
+
- `'drop'`: Silently discard messages. Ideal for real-time data streams (trading quotes, live telemetry) where stale data is worse than gaps.
|
|
77
|
+
</aside>
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Trading data — drop stale quotes rather than buffering
|
|
81
|
+
const app = new YinzerFlow({
|
|
82
|
+
websocket: {
|
|
83
|
+
backpressure: {
|
|
84
|
+
strategy: 'drop',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### websocket.backpressure.limit — @default <span style="color: #2ecc71">`1048576`</span> (1MB)
|
|
91
|
+
|
|
92
|
+
Maximum bytes to queue before closing the connection (only applies to `'buffer'` strategy).
|
|
93
|
+
|
|
94
|
+
## 🔧 Route Registration
|
|
95
|
+
|
|
96
|
+
### app.ws(path, handlers, options?)
|
|
97
|
+
|
|
98
|
+
Register a WebSocket route. Supports parameterized paths (`:param`) just like HTTP routes.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Simple echo server
|
|
102
|
+
app.ws('/echo', {
|
|
103
|
+
message(ws, data) {
|
|
104
|
+
ws.send(data);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Parameterized path
|
|
109
|
+
app.ws('/chat/:room', {
|
|
110
|
+
upgrade(req) {
|
|
111
|
+
return { room: req.params.room };
|
|
112
|
+
},
|
|
113
|
+
open(ws) {
|
|
114
|
+
ws.subscribe(`room:${ws.data.room}`);
|
|
115
|
+
},
|
|
116
|
+
message(ws, data) {
|
|
117
|
+
ws.publish(`room:${ws.data.room}`, data as string);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Handler Lifecycle
|
|
123
|
+
|
|
124
|
+
| Handler | When | Use for |
|
|
125
|
+
|---------|------|---------|
|
|
126
|
+
| `upgrade(req)` | Before handshake | Auth, attach per-socket data, reject with `false` |
|
|
127
|
+
| `open(ws)` | After handshake | Subscribe to channels, send welcome message |
|
|
128
|
+
| `message(ws, data, isBinary)` | On each message | Business logic, pub/sub |
|
|
129
|
+
| `close(ws, code, reason)` | On disconnect | Cleanup (unsubscribe is automatic) |
|
|
130
|
+
| `error(ws, error)` | On socket error | Logging, alerting |
|
|
131
|
+
| `drain(ws)` | When backpressure clears | Resume sending |
|
|
132
|
+
|
|
133
|
+
### Per-Socket Typed Data
|
|
134
|
+
|
|
135
|
+
The `upgrade` handler's return value becomes `ws.data` — fully typed via generics:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
app.ws<{ userId: string; role: string }>('/api/ws', {
|
|
139
|
+
upgrade(req) {
|
|
140
|
+
const token = req.headers.authorization;
|
|
141
|
+
const user = validateToken(token);
|
|
142
|
+
if (!user) return false; // 403 rejection
|
|
143
|
+
return { userId: user.id, role: user.role };
|
|
144
|
+
},
|
|
145
|
+
message(ws) {
|
|
146
|
+
console.log(ws.data.userId); // ✅ typed as string
|
|
147
|
+
console.log(ws.data.role); // ✅ typed as string
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 🔔 Pub/Sub
|
|
153
|
+
|
|
154
|
+
### Channel Subscriptions
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
ws.subscribe('channel-name'); // Join a channel
|
|
158
|
+
ws.unsubscribe('channel-name'); // Leave a channel
|
|
159
|
+
ws.isSubscribed('channel-name'); // Check membership
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Broadcasting
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// From inside a WS handler — excludes the sender
|
|
166
|
+
ws.publish('quotes', JSON.stringify({ symbol: 'AAPL', price: 178.50 }));
|
|
167
|
+
|
|
168
|
+
// From outside WS context (HTTP routes, timers, background jobs)
|
|
169
|
+
app.publish('quotes', JSON.stringify({ symbol: 'AAPL', price: 178.50 }));
|
|
170
|
+
|
|
171
|
+
// Check subscriber count
|
|
172
|
+
const count = app.subscriberCount('quotes');
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
<span style="color: #3498db">**💡 Tip:**</span> `ws.publish()` excludes the sender automatically. `app.publish()` sends to all subscribers (no sender context).
|
|
176
|
+
|
|
177
|
+
### Encode-Once Broadcast
|
|
178
|
+
|
|
179
|
+
When publishing to a channel, the WebSocket frame is encoded **once** into raw bytes, then the same `Buffer` is written to every subscriber. This is O(messageSize + N×write) instead of O(N×messageSize) — critical for high-throughput scenarios like trading quote distribution.
|
|
180
|
+
|
|
181
|
+
## 🪝 Hooks
|
|
182
|
+
|
|
183
|
+
### WS Message Hooks
|
|
184
|
+
|
|
185
|
+
Global hooks that run before/after every WebSocket message handler:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
app.wsBeforeMessage([
|
|
189
|
+
async (ws, data, isBinary) => {
|
|
190
|
+
console.log(`[WS] ${ws.remoteAddress}: ${String(data)}`);
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
app.wsAfterMessage([
|
|
195
|
+
async (ws, data, isBinary) => {
|
|
196
|
+
// Metrics, logging, etc.
|
|
197
|
+
},
|
|
198
|
+
]);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## 📡 The `ws` Object
|
|
202
|
+
|
|
203
|
+
Available in all handlers:
|
|
204
|
+
|
|
205
|
+
| Property/Method | Type | Description |
|
|
206
|
+
|----------------|------|-------------|
|
|
207
|
+
| `ws.send(data)` | `(string \| Buffer) => void` | Send a message |
|
|
208
|
+
| `ws.close(code?, reason?)` | `(number?, string?) => void` | Close connection |
|
|
209
|
+
| `ws.ping(data?)` | `(Buffer?) => void` | Send ping |
|
|
210
|
+
| `ws.subscribe(channel)` | `(string) => void` | Join channel |
|
|
211
|
+
| `ws.unsubscribe(channel)` | `(string) => void` | Leave channel |
|
|
212
|
+
| `ws.publish(channel, data)` | `(string, string \| Buffer) => number` | Broadcast (excludes self) |
|
|
213
|
+
| `ws.isSubscribed(channel)` | `(string) => boolean` | Check membership |
|
|
214
|
+
| `ws.data` | `T` (readonly) | Per-socket data from upgrade |
|
|
215
|
+
| `ws.readyState` | `number` (readonly) | Connection state |
|
|
216
|
+
| `ws.remoteAddress` | `string` (readonly) | Client IP |
|
|
217
|
+
| `ws.bufferedAmount` | `number` (readonly) | Queued bytes |
|
|
218
|
+
|
|
219
|
+
# ✨ Best Practices
|
|
220
|
+
|
|
221
|
+
- **Authenticate in `upgrade`** — reject unauthorized connections before the handshake completes, not after
|
|
222
|
+
- **Use channels for grouping** — `ws.subscribe('user:123')` rather than manual connection tracking
|
|
223
|
+
- **Set `strategy: 'drop'` for real-time data** — stale trading quotes are worse than gaps
|
|
224
|
+
- **Keep message handlers fast** — expensive work should be dispatched to background jobs
|
|
225
|
+
- **Clean up in `close`** — though channel unsubscription is automatic
|
|
226
|
+
|
|
227
|
+
# 💻 Examples
|
|
228
|
+
|
|
229
|
+
### Production API — Real-Time Trading Data
|
|
230
|
+
|
|
231
|
+
**Use Case:** Push market quotes to subscribed clients with minimal latency
|
|
232
|
+
|
|
233
|
+
**Description:** HTTP endpoints ingest quote data and publish to WebSocket subscribers. Backpressure uses `'drop'` strategy — stale quotes are discarded if a client can't keep up.
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { YinzerFlow } from 'yinzerflow';
|
|
237
|
+
|
|
238
|
+
const app = new YinzerFlow({
|
|
239
|
+
port: 3000,
|
|
240
|
+
websocket: {
|
|
241
|
+
maxPayloadLength: 65_536,
|
|
242
|
+
idleTimeout: 300,
|
|
243
|
+
allowedOrigins: ['https://trading.example.com'],
|
|
244
|
+
backpressure: { strategy: 'drop' },
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// WebSocket: clients subscribe to symbol channels
|
|
249
|
+
app.ws<{ symbols: Array<string> }>('/quotes', {
|
|
250
|
+
upgrade(req) {
|
|
251
|
+
const token = req.headers.authorization;
|
|
252
|
+
const user = validateToken(token);
|
|
253
|
+
if (!user) return false;
|
|
254
|
+
const symbols = req.query.symbols?.split(',') ?? [];
|
|
255
|
+
return { symbols };
|
|
256
|
+
},
|
|
257
|
+
open(ws) {
|
|
258
|
+
for (const symbol of ws.data.symbols) {
|
|
259
|
+
ws.subscribe(`quote:${symbol}`);
|
|
260
|
+
}
|
|
261
|
+
ws.send(JSON.stringify({ type: 'subscribed', symbols: ws.data.symbols }));
|
|
262
|
+
},
|
|
263
|
+
message(ws, data) {
|
|
264
|
+
const msg = JSON.parse(data as string);
|
|
265
|
+
if (msg.type === 'subscribe') {
|
|
266
|
+
ws.subscribe(`quote:${msg.symbol}`);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// HTTP: ingest quotes and broadcast to subscribers
|
|
272
|
+
app.post('/api/quotes', (ctx) => {
|
|
273
|
+
const { symbol, price, volume } = ctx.request.body;
|
|
274
|
+
const count = app.publish(
|
|
275
|
+
`quote:${symbol}`,
|
|
276
|
+
JSON.stringify({ type: 'quote', symbol, price, volume, ts: Date.now() }),
|
|
277
|
+
);
|
|
278
|
+
return { broadcast: count };
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await app.listen();
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Dev API — Echo Server
|
|
285
|
+
|
|
286
|
+
**Use Case:** Development and testing
|
|
287
|
+
|
|
288
|
+
**Description:** Simple echo server with logging for debugging WebSocket connections.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import { YinzerFlow } from 'yinzerflow';
|
|
292
|
+
|
|
293
|
+
const app = new YinzerFlow({
|
|
294
|
+
port: 3000,
|
|
295
|
+
logging: { level: 'debug' },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
app.ws('/echo', {
|
|
299
|
+
open(ws) {
|
|
300
|
+
ws.send('Connected to echo server');
|
|
301
|
+
},
|
|
302
|
+
message(ws, data, isBinary) {
|
|
303
|
+
ws.send(data);
|
|
304
|
+
},
|
|
305
|
+
close(ws, code, reason) {
|
|
306
|
+
console.log(`Disconnected: ${code} ${reason}`);
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await app.listen();
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## 🚀 Performance Notes
|
|
314
|
+
|
|
315
|
+
- **Encode-once broadcast**: Publishing to N subscribers encodes the frame once. 1000 subscribers = 1 encode + 1000 writes, not 1000 encodes.
|
|
316
|
+
- **In-place XOR unmasking**: Client frames are unmasked by mutating the buffer directly — zero allocation.
|
|
317
|
+
- **`Buffer.allocUnsafe`** for outgoing frames — skips zero-fill since the entire buffer is written before use.
|
|
318
|
+
- **Zero overhead** when no `app.ws()` routes: no upgrade detection, no Sets allocated, no security instances created.
|
|
319
|
+
- **Idle timeout** resets on every received frame — high-frequency data streams won't trigger timeout.
|
|
320
|
+
|
|
321
|
+
## 🔒 Security Notes
|
|
322
|
+
|
|
323
|
+
### 🛡️ Origin Validation
|
|
324
|
+
- **Problem**: Unauthorized origins connecting to WebSocket endpoints
|
|
325
|
+
- **YinzerFlow Solution**: `websocket.allowedOrigins` validates the Origin header during upgrade. Case-insensitive matching. Empty list = allow all (for server-to-server or development).
|
|
326
|
+
|
|
327
|
+
### 🛡️ Connection Limits
|
|
328
|
+
- **Problem**: Connection exhaustion from a single IP
|
|
329
|
+
- **YinzerFlow Solution**: `websocket.maxConnectionsPerIp` limits concurrent connections. Returns 429 when exceeded. Counter automatically decrements on disconnect.
|
|
330
|
+
|
|
331
|
+
### 🛡️ Payload Size Limits
|
|
332
|
+
- **Problem**: Memory exhaustion from oversized messages
|
|
333
|
+
- **YinzerFlow Solution**: `websocket.maxPayloadLength` checked on frame header before buffering. Exceeding triggers close with code 1009 (Too Large).
|
|
334
|
+
|
|
335
|
+
## 🔧 Troubleshooting
|
|
336
|
+
|
|
337
|
+
| Symptom | Cause | Fix |
|
|
338
|
+
|---------|-------|-----|
|
|
339
|
+
| 403 on upgrade | Origin not in `allowedOrigins` | Add origin or set `allowedOrigins: []` |
|
|
340
|
+
| 429 on upgrade | Too many connections from IP | Increase `maxConnectionsPerIp` |
|
|
341
|
+
| Connection drops silently | Idle timeout | Increase `idleTimeout` or send periodic pings |
|
|
342
|
+
| Messages not received | Client backpressured with `'drop'` | Switch to `'buffer'` or increase client throughput |
|
|
343
|
+
| `ws.data` is `undefined` | No `upgrade` handler | Add `upgrade(req) { return { ... } }` |
|