stream-guard 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/README.md ADDED
@@ -0,0 +1,382 @@
1
+ # stream-guard
2
+
3
+ [![npm version](https://img.shields.io/npm/v/stream-guard.svg)](https://www.npmjs.com/package/stream-guard)
4
+ [![Tests](https://img.shields.io/github/actions/workflow/status/your-org/stream-guard/test.yml?label=tests)](https://github.com/your-org/stream-guard/actions)
5
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](https://www.npmjs.com/package/stream-guard)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ **Production-grade circuit breaker for Node.js streams. Prevents leaks, hangs, and OOM crashes.**
10
+
11
+ ---
12
+
13
+ ## The Problem: Why Streams Are Dangerous
14
+
15
+ Node.js streams are powerful but **dangerous by default**:
16
+
17
+ | Threat | Description | Impact |
18
+ |--------|-------------|--------|
19
+ | **Slow Loris Attack** | Malicious client sends data extremely slowly | Server hangs forever, file descriptors exhausted |
20
+ | **Zombie Connections** | Stream stops emitting data but never closes | Memory leak, connection pool exhaustion |
21
+ | **Infinite Data** | Unbounded streams consume unlimited memory | OOM crash, service restart |
22
+ | **Memory Leaks** | Event listeners pile up on long-lived streams | Gradual memory growth until crash |
23
+
24
+ ### The "Just Use setTimeout" Fallacy
25
+
26
+ ```javascript
27
+ // ❌ This is NOT enough
28
+ const timeout = setTimeout(() => {
29
+ stream.destroy();
30
+ }, 30000);
31
+
32
+ stream.on('data', () => {
33
+ clearTimeout(timeout);
34
+ // Now what? Stream could still stall between chunks!
35
+ });
36
+ ```
37
+
38
+ The `setTimeout` approach fails because:
39
+ - It doesn't detect **stalls between chunks**
40
+ - It doesn't limit **total bytes transferred**
41
+ - It doesn't protect against **memory exhaustion**
42
+ - Timer cleanup is **error-prone** and often missed
43
+
44
+ ---
45
+
46
+ ## The Solution: stream-guard
47
+
48
+ `stream-guard` wraps any Node.js stream with configurable safety constraints:
49
+
50
+ ```typescript
51
+ import { guard } from 'stream-guard';
52
+ import { createReadStream } from 'node:fs';
53
+
54
+ // ✅ Protected stream - will be destroyed if any limit is violated
55
+ const stream = guard(createReadStream('data.json'), {
56
+ timeout: 30_000, // Absolute timeout: 30 seconds max
57
+ stalled: 5_000, // Stall detection: 5 seconds without data = dead
58
+ maxBytes: 10 * 1024 * 1024, // Byte limit: 10MB max
59
+ });
60
+
61
+ stream.pipe(response);
62
+ ```
63
+
64
+ When a limit is violated, the stream is **immediately destroyed** with a typed error:
65
+
66
+ ```typescript
67
+ import { guard, StreamTimeoutError, StreamStalledError } from 'stream-guard';
68
+
69
+ stream.on('error', (err) => {
70
+ if (err instanceof StreamTimeoutError) {
71
+ console.log(`Timeout after ${err.elapsedMs}ms`);
72
+ } else if (err instanceof StreamStalledError) {
73
+ console.log(`Stalled for ${err.idleMs}ms`);
74
+ }
75
+ });
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Installation
81
+
82
+ ```bash
83
+ npm install stream-guard
84
+ ```
85
+
86
+ ```bash
87
+ yarn add stream-guard
88
+ ```
89
+
90
+ ```bash
91
+ pnpm add stream-guard
92
+ ```
93
+
94
+ ---
95
+
96
+ ## API Reference
97
+
98
+ ### `guard(stream, options)`
99
+
100
+ Wraps a stream with safety constraints. Returns the same stream instance.
101
+
102
+ ```typescript
103
+ function guard<T extends GuardableStream>(stream: T, options?: GuardOptions): T
104
+ ```
105
+
106
+ #### Options
107
+
108
+ | Option | Type | Default | Description |
109
+ |--------|------|---------|-------------|
110
+ | `timeout` | `number` | - | **Absolute timeout** in milliseconds. Stream is destroyed if it doesn't complete within this time. |
111
+ | `stalled` | `number` | - | **Stall protection** in milliseconds. Stream is destroyed if no data flows for this duration. |
112
+ | `maxBytes` | `number` | - | **Byte limit**. Stream is destroyed if total bytes exceed this value. |
113
+ | `maxHeap` | `number` | - | **Heap limit** in bytes. Stream is destroyed if `process.memoryUsage().heapUsed` exceeds this. |
114
+ | `heapCheckInterval` | `number` | `100` | Interval in ms between heap checks. |
115
+ | `heapCheckChunks` | `number` | - | Check heap every N chunks instead of by time interval. |
116
+ | `onDestroy` | `(error: Error) => void` | - | Callback when stream is destroyed by guard. |
117
+
118
+ #### Returns
119
+
120
+ The same stream instance with guards attached. Works seamlessly with `.pipe()`.
121
+
122
+ ---
123
+
124
+ ### `createGuard(defaultOptions)`
125
+
126
+ Creates a reusable guard factory with preset defaults.
127
+
128
+ ```typescript
129
+ const apiGuard = createGuard({
130
+ timeout: 30_000,
131
+ maxBytes: 5 * 1024 * 1024,
132
+ });
133
+
134
+ // All streams use these defaults
135
+ apiGuard(stream1);
136
+ apiGuard(stream2, { stalled: 5000 }); // Can still override
137
+ ```
138
+
139
+ ---
140
+
141
+ ### `isGuarded(stream)`
142
+
143
+ Check if a stream has already been guarded.
144
+
145
+ ```typescript
146
+ if (!isGuarded(stream)) {
147
+ guard(stream, options);
148
+ }
149
+ ```
150
+
151
+ ---
152
+
153
+ ### `getGuardMetadata(stream)`
154
+
155
+ Access guard metadata for monitoring.
156
+
157
+ ```typescript
158
+ const metadata = getGuardMetadata(stream);
159
+
160
+ console.log(metadata.guardId); // Unique guard ID
161
+ console.log(metadata.getBytesTransferred()); // Bytes so far
162
+ console.log(metadata.getElapsedTime()); // Time since guard attached
163
+ console.log(metadata.isActive()); // Guard still active?
164
+
165
+ metadata.release(); // Manually remove guards
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Error Classes
171
+
172
+ All errors extend `StreamGuardError` and include:
173
+ - `timestamp`: When the error occurred
174
+ - `constraint`: Which limit was violated
175
+
176
+ | Error Class | Constraint | Properties |
177
+ |-------------|------------|------------|
178
+ | `StreamTimeoutError` | `timeout` | `timeoutMs`, `elapsedMs` |
179
+ | `StreamStalledError` | `stalled` | `stalledMs`, `idleMs` |
180
+ | `StreamLimitError` | `maxBytes` | `maxBytes`, `bytesTransferred` |
181
+ | `StreamHeapError` | `maxHeap` | `maxHeap`, `heapUsed` |
182
+
183
+ ### Type Guards
184
+
185
+ ```typescript
186
+ import {
187
+ isStreamGuardError,
188
+ isStreamTimeoutError,
189
+ isStreamStalledError,
190
+ isStreamLimitError,
191
+ isStreamHeapError,
192
+ } from 'stream-guard';
193
+
194
+ stream.on('error', (err) => {
195
+ if (isStreamGuardError(err)) {
196
+ metrics.increment(`stream.guard.${err.constraint}`);
197
+ }
198
+ });
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Usage Examples
204
+
205
+ ### HTTP File Upload Protection
206
+
207
+ ```typescript
208
+ import { guard, StreamLimitError } from 'stream-guard';
209
+ import express from 'express';
210
+
211
+ app.post('/upload', (req, res) => {
212
+ const upload = guard(req, {
213
+ timeout: 60_000, // 1 minute max
214
+ stalled: 10_000, // 10s stall = abort
215
+ maxBytes: 50 * 1024 * 1024, // 50MB max
216
+ });
217
+
218
+ upload.on('error', (err) => {
219
+ if (err instanceof StreamLimitError) {
220
+ res.status(413).send('File too large');
221
+ } else {
222
+ res.status(408).send('Upload timeout');
223
+ }
224
+ });
225
+
226
+ upload.pipe(fs.createWriteStream('/uploads/file'));
227
+ });
228
+ ```
229
+
230
+ ### Database Export with Memory Protection
231
+
232
+ ```typescript
233
+ import { guard } from 'stream-guard';
234
+
235
+ const exportStream = guard(db.query('SELECT * FROM huge_table').stream(), {
236
+ timeout: 300_000, // 5 minutes
237
+ maxHeap: 500 * 1024 * 1024, // 500MB heap limit
238
+ heapCheckInterval: 50,
239
+ onDestroy: (err) => {
240
+ logger.error('Export aborted', { error: err.message });
241
+ alertOps('Database export failed - memory limit');
242
+ },
243
+ });
244
+
245
+ exportStream.pipe(csvTransform).pipe(response);
246
+ ```
247
+
248
+ ### Microservice Request Streaming
249
+
250
+ ```typescript
251
+ import { createGuard } from 'stream-guard';
252
+
253
+ // Reusable guard for all API responses
254
+ const apiGuard = createGuard({
255
+ timeout: 30_000,
256
+ stalled: 5_000,
257
+ maxBytes: 10 * 1024 * 1024,
258
+ });
259
+
260
+ async function fetchFromService(url: string) {
261
+ const response = await fetch(url);
262
+ return apiGuard(Readable.fromWeb(response.body));
263
+ }
264
+ ```
265
+
266
+ ### WebSocket Backpressure Protection
267
+
268
+ ```typescript
269
+ import { guard } from 'stream-guard';
270
+
271
+ ws.on('connection', (socket) => {
272
+ const guarded = guard(socket, {
273
+ stalled: 30_000, // Client must ACK within 30s
274
+ maxBytes: 100 * 1024 * 1024, // 100MB per connection
275
+ });
276
+
277
+ guarded.on('error', (err) => {
278
+ logger.warn('WebSocket terminated', { reason: err.message });
279
+ socket.terminate();
280
+ });
281
+ });
282
+ ```
283
+
284
+ ---
285
+
286
+ ## Why stream-guard?
287
+
288
+ | Feature | `setTimeout` | `stream.timeout()` | **stream-guard** |
289
+ |---------|--------------|-------------------|------------------|
290
+ | Absolute timeout | ✅ | ❌ | ✅ |
291
+ | Stall detection | ❌ | ❌ | ✅ |
292
+ | Byte limits | ❌ | ❌ | ✅ |
293
+ | Heap protection | ❌ | ❌ | ✅ |
294
+ | Automatic cleanup | ❌ | ❌ | ✅ |
295
+ | Typed errors | ❌ | ❌ | ✅ |
296
+ | Zero dependencies | N/A | N/A | ✅ |
297
+ | Works with pipe() | ❌ | ✅ | ✅ |
298
+
299
+ ---
300
+
301
+ ## Best Practices
302
+
303
+ ### 1. Always set a timeout for network streams
304
+
305
+ ```typescript
306
+ // Every external stream should have an absolute timeout
307
+ guard(httpResponse, { timeout: 30_000 });
308
+ ```
309
+
310
+ ### 2. Use stall detection for user-facing uploads
311
+
312
+ ```typescript
313
+ // Slow uploads are often attacks or broken clients
314
+ guard(uploadStream, { stalled: 10_000 });
315
+ ```
316
+
317
+ ### 3. Combine with byte limits for untrusted input
318
+
319
+ ```typescript
320
+ guard(request, {
321
+ timeout: 60_000,
322
+ stalled: 5_000,
323
+ maxBytes: 10 * 1024 * 1024,
324
+ });
325
+ ```
326
+
327
+ ### 4. Monitor with onDestroy callback
328
+
329
+ ```typescript
330
+ guard(stream, {
331
+ timeout: 30_000,
332
+ onDestroy: (err) => {
333
+ metrics.increment('stream.killed', { reason: err.constraint });
334
+ },
335
+ });
336
+ ```
337
+
338
+ ### 5. Use metadata for debugging
339
+
340
+ ```typescript
341
+ const guarded = guard(stream, options);
342
+
343
+ setInterval(() => {
344
+ const meta = getGuardMetadata(guarded);
345
+ if (meta?.isActive()) {
346
+ logger.debug('Stream progress', {
347
+ bytes: meta.getBytesTransferred(),
348
+ elapsed: meta.getElapsedTime(),
349
+ });
350
+ }
351
+ }, 5000);
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Requirements
357
+
358
+ - Node.js 18.0.0 or later
359
+ - TypeScript 5.0+ (for type definitions)
360
+
361
+ ---
362
+
363
+ ## License
364
+
365
+ MIT © 2026
366
+
367
+ ---
368
+
369
+ ## Contributing
370
+
371
+ 1. Fork the repository
372
+ 2. Create a feature branch (`git checkout -b feature/amazing`)
373
+ 3. Run tests (`npm test`)
374
+ 4. Commit changes (`git commit -m 'Add amazing feature'`)
375
+ 5. Push to branch (`git push origin feature/amazing`)
376
+ 6. Open a Pull Request
377
+
378
+ ---
379
+
380
+ <p align="center">
381
+ <strong>Stop streams from ruining your day.</strong>
382
+ </p>