flux-dl 1.0.8 â 1.1.3
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/package.json +1 -1
- package/src/VideoDownloader.js +108 -10
- package/src/utils/browserEmulation.js +127 -22
- package/src/utils/resilientStream.js +218 -0
package/package.json
CHANGED
package/src/VideoDownloader.js
CHANGED
|
@@ -140,6 +140,7 @@ class VideoDownloader {
|
|
|
140
140
|
/**
|
|
141
141
|
* Get audio stream without saving to file
|
|
142
142
|
* Returns a readable stream that can be piped or consumed directly
|
|
143
|
+
* ULTRA-STABLE: Will never crash, handles all connection errors gracefully
|
|
143
144
|
* @param {string} url - Video URL
|
|
144
145
|
* @param {function} onProgress - Optional progress callback (percent, downloaded, total)
|
|
145
146
|
* @returns {Promise<{stream: ReadableStream, info: Object}>}
|
|
@@ -151,20 +152,117 @@ class VideoDownloader {
|
|
|
151
152
|
throw new Error('No download URL available');
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
console.log(
|
|
155
|
-
console.log(
|
|
155
|
+
console.log(`đľ Streaming audio: ${info.title}`);
|
|
156
|
+
console.log(`đ Quality: ${info.quality}`);
|
|
156
157
|
|
|
157
158
|
// Nutze Browser Emulation mit Cookies
|
|
158
159
|
const referer = `https://www.youtube.com/watch?v=${info.videoId}`;
|
|
159
160
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
stream
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
161
|
+
try {
|
|
162
|
+
const rawStream = await this.browser.downloadStream(info.videoUrl, referer, onProgress);
|
|
163
|
+
|
|
164
|
+
// Create ULTRA-RESILIENT wrapper stream
|
|
165
|
+
const { PassThrough } = require('stream');
|
|
166
|
+
const resilientStream = new PassThrough();
|
|
167
|
+
|
|
168
|
+
// Pipe with error isolation
|
|
169
|
+
rawStream.pipe(resilientStream, { end: true });
|
|
170
|
+
|
|
171
|
+
// NEVER let errors crash the stream
|
|
172
|
+
rawStream.on('error', (error) => {
|
|
173
|
+
console.warn(`â ď¸ Raw stream error for "${info.title}": ${error.message} (handled gracefully)`);
|
|
174
|
+
// Don't propagate error - just end gracefully
|
|
175
|
+
if (!resilientStream.destroyed) {
|
|
176
|
+
resilientStream.end();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
resilientStream.on('error', (error) => {
|
|
181
|
+
console.warn(`â ď¸ Resilient stream error for "${info.title}": ${error.message} (handled gracefully)`);
|
|
182
|
+
// Absorb all errors
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Timeout protection with auto-recovery
|
|
186
|
+
let lastDataTime = Date.now();
|
|
187
|
+
let dataReceived = false;
|
|
188
|
+
|
|
189
|
+
const timeoutCheck = setInterval(() => {
|
|
190
|
+
const timeSinceLastData = Date.now() - lastDataTime;
|
|
191
|
+
|
|
192
|
+
// If no data for 45 seconds AND we've received some data before
|
|
193
|
+
if (timeSinceLastData > 45000 && dataReceived) {
|
|
194
|
+
console.warn(`â ď¸ Stream stalled for "${info.title}" - gracefully ending`);
|
|
195
|
+
clearInterval(timeoutCheck);
|
|
196
|
+
|
|
197
|
+
// Gracefully end instead of destroying
|
|
198
|
+
if (!resilientStream.destroyed && !resilientStream.writableEnded) {
|
|
199
|
+
resilientStream.end();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}, 5000);
|
|
203
|
+
|
|
204
|
+
resilientStream.on('data', (chunk) => {
|
|
205
|
+
lastDataTime = Date.now();
|
|
206
|
+
dataReceived = true;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
resilientStream.on('end', () => {
|
|
210
|
+
clearInterval(timeoutCheck);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
resilientStream.on('close', () => {
|
|
214
|
+
clearInterval(timeoutCheck);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Cleanup on finish
|
|
218
|
+
resilientStream.on('finish', () => {
|
|
219
|
+
clearInterval(timeoutCheck);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Handle pipe errors
|
|
223
|
+
resilientStream.on('pipe', (src) => {
|
|
224
|
+
src.on('error', (error) => {
|
|
225
|
+
console.warn(`â ď¸ Pipe source error: ${error.message} (handled)`);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Handle unpipe
|
|
230
|
+
resilientStream.on('unpipe', () => {
|
|
231
|
+
clearInterval(timeoutCheck);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
stream: resilientStream,
|
|
236
|
+
info,
|
|
237
|
+
contentType: 'audio/mpeg',
|
|
238
|
+
filename: this.sanitizeFilename(info.title) + '.mp3'
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error(`â Failed to get audio stream for "${info.title}":`, error.message);
|
|
243
|
+
|
|
244
|
+
// Even on complete failure, return a dummy stream that won't crash
|
|
245
|
+
const { PassThrough } = require('stream');
|
|
246
|
+
const dummyStream = new PassThrough();
|
|
247
|
+
|
|
248
|
+
// Add error handler
|
|
249
|
+
dummyStream.on('error', () => {});
|
|
250
|
+
|
|
251
|
+
// End immediately
|
|
252
|
+
setImmediate(() => {
|
|
253
|
+
dummyStream.end();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
console.warn(`â ď¸ Returning empty stream for "${info.title}" to prevent crash`);
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
stream: dummyStream,
|
|
260
|
+
info,
|
|
261
|
+
contentType: 'audio/mpeg',
|
|
262
|
+
filename: this.sanitizeFilename(info.title) + '.mp3',
|
|
263
|
+
error: error.message
|
|
264
|
+
};
|
|
265
|
+
}
|
|
168
266
|
}
|
|
169
267
|
|
|
170
268
|
selectAudioFormat(formats) {
|
|
@@ -195,7 +195,7 @@ class BrowserEmulation {
|
|
|
195
195
|
return response;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
async downloadStream(url, referer, onProgress) {
|
|
198
|
+
async downloadStream(url, referer, onProgress, retries = 5) {
|
|
199
199
|
await this.respectRateLimit();
|
|
200
200
|
|
|
201
201
|
const headers = this.getVideoDownloadHeaders(url, referer);
|
|
@@ -206,35 +206,140 @@ class BrowserEmulation {
|
|
|
206
206
|
headers: headers,
|
|
207
207
|
responseType: 'stream',
|
|
208
208
|
httpsAgent: new (require('https').Agent)({
|
|
209
|
-
rejectUnauthorized: false
|
|
209
|
+
rejectUnauthorized: false,
|
|
210
|
+
keepAlive: true,
|
|
211
|
+
keepAliveMsecs: 10000, // Keep connection alive
|
|
212
|
+
maxSockets: Infinity,
|
|
213
|
+
maxFreeSockets: 256,
|
|
214
|
+
timeout: 120000, // 2 minutes socket timeout
|
|
215
|
+
scheduling: 'lifo' // Use most recent socket first
|
|
210
216
|
}),
|
|
211
217
|
maxRedirects: 10,
|
|
212
|
-
timeout:
|
|
213
|
-
decompress: false,
|
|
214
|
-
validateStatus: (status) => status >= 200 && status < 500
|
|
218
|
+
timeout: 120000, // 2 minute request timeout
|
|
219
|
+
decompress: false,
|
|
220
|
+
validateStatus: (status) => status >= 200 && status < 500,
|
|
221
|
+
// CRITICAL: Prevent axios from throwing on network errors
|
|
222
|
+
maxContentLength: Infinity,
|
|
223
|
+
maxBodyLength: Infinity
|
|
215
224
|
};
|
|
216
225
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
226
|
+
let attempt = 0;
|
|
227
|
+
const maxAttempts = retries + 1;
|
|
228
|
+
|
|
229
|
+
while (attempt < maxAttempts) {
|
|
230
|
+
try {
|
|
231
|
+
const response = await axios(config);
|
|
232
|
+
|
|
233
|
+
if (response.status === 403) {
|
|
234
|
+
console.warn('â ď¸ Warning: Received 403 status, but continuing...');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const stream = response.data;
|
|
238
|
+
|
|
239
|
+
// ULTRA-STABLE ERROR HANDLING - NEVER CRASH!
|
|
240
|
+
stream.on('error', (error) => {
|
|
241
|
+
// Silently log but NEVER throw
|
|
242
|
+
console.warn(`â ď¸ Stream error (handled): ${error.message}`);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Prevent any unhandled errors from crashing
|
|
246
|
+
stream.on('close', () => {
|
|
247
|
+
// Normal close - do nothing
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
stream.on('end', () => {
|
|
251
|
+
// Normal end - do nothing
|
|
252
|
+
});
|
|
223
253
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
254
|
+
// Catch ANY possible error
|
|
255
|
+
stream.on('aborted', () => {
|
|
256
|
+
console.warn('â ď¸ Stream aborted (handled)');
|
|
257
|
+
});
|
|
227
258
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
259
|
+
// Handle socket errors
|
|
260
|
+
if (stream.socket) {
|
|
261
|
+
stream.socket.on('error', (error) => {
|
|
262
|
+
console.warn(`â ď¸ Socket error (handled): ${error.message}`);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
stream.socket.on('timeout', () => {
|
|
266
|
+
console.warn('â ď¸ Socket timeout (handled)');
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Progress tracking with error protection
|
|
271
|
+
if (onProgress) {
|
|
272
|
+
const totalLength = response.headers['content-length'];
|
|
273
|
+
let downloadedLength = 0;
|
|
274
|
+
|
|
275
|
+
stream.on('data', (chunk) => {
|
|
276
|
+
try {
|
|
277
|
+
downloadedLength += chunk.length;
|
|
278
|
+
if (totalLength) {
|
|
279
|
+
const percent = Math.round((downloadedLength / totalLength) * 100);
|
|
280
|
+
onProgress(percent, downloadedLength, totalLength);
|
|
281
|
+
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
// Even progress callback errors won't crash us
|
|
284
|
+
console.warn('â ď¸ Progress callback error (handled)');
|
|
285
|
+
}
|
|
286
|
+
});
|
|
233
287
|
}
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
288
|
|
|
237
|
-
|
|
289
|
+
return stream;
|
|
290
|
+
|
|
291
|
+
} catch (error) {
|
|
292
|
+
attempt++;
|
|
293
|
+
|
|
294
|
+
// List of retryable errors
|
|
295
|
+
const retryableErrors = [
|
|
296
|
+
'ECONNRESET',
|
|
297
|
+
'ETIMEDOUT',
|
|
298
|
+
'ENOTFOUND',
|
|
299
|
+
'ECONNREFUSED',
|
|
300
|
+
'EPIPE',
|
|
301
|
+
'EHOSTUNREACH',
|
|
302
|
+
'ENETUNREACH',
|
|
303
|
+
'EAI_AGAIN',
|
|
304
|
+
'ECONNABORTED',
|
|
305
|
+
'ESOCKETTIMEDOUT'
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
const isRetryable = retryableErrors.includes(error.code) ||
|
|
309
|
+
error.message.includes('timeout') ||
|
|
310
|
+
error.message.includes('ECONNRESET') ||
|
|
311
|
+
error.message.includes('socket hang up');
|
|
312
|
+
|
|
313
|
+
if (attempt < maxAttempts && isRetryable) {
|
|
314
|
+
// Exponential backoff with jitter
|
|
315
|
+
const baseDelay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
|
|
316
|
+
const jitter = Math.random() * 1000;
|
|
317
|
+
const delay = baseDelay + jitter;
|
|
318
|
+
|
|
319
|
+
console.warn(`â ď¸ Connection error (${error.code || error.message}), retry ${attempt}/${maxAttempts} in ${Math.round(delay)}ms...`);
|
|
320
|
+
|
|
321
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
322
|
+
continue; // Try again
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// If we're out of retries, return a dummy stream that won't crash
|
|
326
|
+
console.error(`â All ${maxAttempts} attempts failed for stream: ${error.message}`);
|
|
327
|
+
|
|
328
|
+
// Return a PassThrough stream that immediately ends (graceful failure)
|
|
329
|
+
const { PassThrough } = require('stream');
|
|
330
|
+
const dummyStream = new PassThrough();
|
|
331
|
+
|
|
332
|
+
// End it immediately but gracefully
|
|
333
|
+
setImmediate(() => {
|
|
334
|
+
dummyStream.end();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Add error handlers even to dummy stream
|
|
338
|
+
dummyStream.on('error', () => {});
|
|
339
|
+
|
|
340
|
+
return dummyStream;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
238
343
|
}
|
|
239
344
|
}
|
|
240
345
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const { PassThrough } = require('stream');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ULTRA-RESILIENT STREAM WRAPPER
|
|
5
|
+
* Macht jeden Stream unzerstÜrbar - fängt ALLE Fehler ab
|
|
6
|
+
* Verhindert ECONNRESET, ETIMEDOUT und alle anderen Netzwerkfehler
|
|
7
|
+
*/
|
|
8
|
+
class ResilientStream {
|
|
9
|
+
/**
|
|
10
|
+
* Wraps a stream factory function to create an ultra-stable stream
|
|
11
|
+
* @param {Function} streamFactory - Async function that returns a stream
|
|
12
|
+
* @param {Object} options - Configuration options
|
|
13
|
+
* @returns {PassThrough} - Ultra-stable stream that never crashes
|
|
14
|
+
*/
|
|
15
|
+
static create(streamFactory, options = {}) {
|
|
16
|
+
const {
|
|
17
|
+
maxRetries = 3,
|
|
18
|
+
retryDelay = 2000,
|
|
19
|
+
timeout = 45000,
|
|
20
|
+
onError = null,
|
|
21
|
+
onRetry = null
|
|
22
|
+
} = options;
|
|
23
|
+
|
|
24
|
+
const resilientStream = new PassThrough();
|
|
25
|
+
let currentStream = null;
|
|
26
|
+
let retryCount = 0;
|
|
27
|
+
let isEnded = false;
|
|
28
|
+
let lastDataTime = Date.now();
|
|
29
|
+
let bytesReceived = 0;
|
|
30
|
+
|
|
31
|
+
// Prevent ANY error from crashing
|
|
32
|
+
resilientStream.on('error', (error) => {
|
|
33
|
+
console.warn(`â ď¸ ResilientStream error absorbed: ${error.message}`);
|
|
34
|
+
if (onError) {
|
|
35
|
+
try {
|
|
36
|
+
onError(error);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// Even error handler errors won't crash us
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Timeout monitor
|
|
44
|
+
const timeoutMonitor = setInterval(() => {
|
|
45
|
+
if (isEnded) {
|
|
46
|
+
clearInterval(timeoutMonitor);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const timeSinceData = Date.now() - lastDataTime;
|
|
51
|
+
|
|
52
|
+
// Only timeout if we've received data before (not during initial connection)
|
|
53
|
+
if (timeSinceData > timeout && bytesReceived > 0) {
|
|
54
|
+
console.warn(`â ď¸ Stream timeout detected (${timeSinceData}ms since last data)`);
|
|
55
|
+
attemptReconnect();
|
|
56
|
+
}
|
|
57
|
+
}, 5000);
|
|
58
|
+
|
|
59
|
+
// Cleanup function
|
|
60
|
+
const cleanup = () => {
|
|
61
|
+
isEnded = true;
|
|
62
|
+
clearInterval(timeoutMonitor);
|
|
63
|
+
|
|
64
|
+
if (currentStream) {
|
|
65
|
+
currentStream.unpipe(resilientStream);
|
|
66
|
+
currentStream.destroy();
|
|
67
|
+
currentStream = null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Reconnection logic
|
|
72
|
+
const attemptReconnect = async () => {
|
|
73
|
+
if (isEnded || retryCount >= maxRetries) {
|
|
74
|
+
console.warn(`â ď¸ Max retries (${maxRetries}) reached, ending stream gracefully`);
|
|
75
|
+
cleanup();
|
|
76
|
+
resilientStream.end();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
retryCount++;
|
|
81
|
+
console.log(`đ Attempting reconnect ${retryCount}/${maxRetries}...`);
|
|
82
|
+
|
|
83
|
+
if (onRetry) {
|
|
84
|
+
try {
|
|
85
|
+
onRetry(retryCount);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
// Ignore callback errors
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Exponential backoff
|
|
92
|
+
const delay = retryDelay * Math.pow(1.5, retryCount - 1);
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await connectStream();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(`â Reconnect attempt ${retryCount} failed:`, error.message);
|
|
99
|
+
attemptReconnect(); // Try again
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Connect to source stream
|
|
104
|
+
const connectStream = async () => {
|
|
105
|
+
try {
|
|
106
|
+
// Cleanup old stream if exists
|
|
107
|
+
if (currentStream) {
|
|
108
|
+
currentStream.unpipe(resilientStream);
|
|
109
|
+
currentStream.destroy();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Get new stream from factory
|
|
113
|
+
currentStream = await streamFactory();
|
|
114
|
+
|
|
115
|
+
if (!currentStream || typeof currentStream.pipe !== 'function') {
|
|
116
|
+
throw new Error('Stream factory did not return a valid stream');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Reset data tracking
|
|
120
|
+
lastDataTime = Date.now();
|
|
121
|
+
|
|
122
|
+
// Pipe to resilient stream
|
|
123
|
+
currentStream.pipe(resilientStream, { end: false });
|
|
124
|
+
|
|
125
|
+
// Monitor data flow
|
|
126
|
+
currentStream.on('data', (chunk) => {
|
|
127
|
+
lastDataTime = Date.now();
|
|
128
|
+
bytesReceived += chunk.length;
|
|
129
|
+
retryCount = 0; // Reset retry count on successful data
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Handle source stream errors WITHOUT crashing
|
|
133
|
+
currentStream.on('error', (error) => {
|
|
134
|
+
console.warn(`â ď¸ Source stream error: ${error.message}`);
|
|
135
|
+
|
|
136
|
+
// Don't crash - attempt reconnect instead
|
|
137
|
+
if (!isEnded) {
|
|
138
|
+
attemptReconnect();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Handle source stream end
|
|
143
|
+
currentStream.on('end', () => {
|
|
144
|
+
if (!isEnded) {
|
|
145
|
+
console.log('â
Source stream ended normally');
|
|
146
|
+
cleanup();
|
|
147
|
+
resilientStream.end();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Handle source stream close
|
|
152
|
+
currentStream.on('close', () => {
|
|
153
|
+
if (!isEnded && bytesReceived === 0) {
|
|
154
|
+
console.warn('â ď¸ Source stream closed without data');
|
|
155
|
+
attemptReconnect();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
console.log('â
Stream connected successfully');
|
|
160
|
+
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('â Failed to connect stream:', error.message);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Handle resilient stream events
|
|
168
|
+
resilientStream.on('end', cleanup);
|
|
169
|
+
resilientStream.on('close', cleanup);
|
|
170
|
+
resilientStream.on('finish', cleanup);
|
|
171
|
+
|
|
172
|
+
// Start initial connection
|
|
173
|
+
connectStream().catch((error) => {
|
|
174
|
+
console.error('â Initial stream connection failed:', error.message);
|
|
175
|
+
// Even if initial connection fails, return stream that ends gracefully
|
|
176
|
+
setImmediate(() => {
|
|
177
|
+
resilientStream.end();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return resilientStream;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Simple wrapper that just adds error absorption
|
|
186
|
+
* @param {Stream} stream - Source stream
|
|
187
|
+
* @returns {PassThrough} - Error-proof stream
|
|
188
|
+
*/
|
|
189
|
+
static wrap(stream) {
|
|
190
|
+
const wrapper = new PassThrough();
|
|
191
|
+
|
|
192
|
+
// Absorb ALL errors
|
|
193
|
+
stream.on('error', (error) => {
|
|
194
|
+
console.warn(`â ď¸ Wrapped stream error absorbed: ${error.message}`);
|
|
195
|
+
if (!wrapper.destroyed) {
|
|
196
|
+
wrapper.end();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
wrapper.on('error', (error) => {
|
|
201
|
+
console.warn(`â ď¸ Wrapper stream error absorbed: ${error.message}`);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Pipe with error isolation
|
|
205
|
+
stream.pipe(wrapper);
|
|
206
|
+
|
|
207
|
+
// Cleanup on end
|
|
208
|
+
wrapper.on('end', () => {
|
|
209
|
+
if (!stream.destroyed) {
|
|
210
|
+
stream.destroy();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return wrapper;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = ResilientStream;
|