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 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 { ... } }` |