@push.rocks/smartstream 3.3.0 → 3.4.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/smartstream.classes.smartduplex.d.ts +10 -4
- package/dist_ts/smartstream.classes.smartduplex.js +182 -72
- package/dist_ts/smartstream.classes.streamintake.js +6 -3
- package/dist_ts/smartstream.classes.streamwrapper.js +11 -12
- package/dist_ts/smartstream.nodewebhelpers.d.ts +2 -2
- package/dist_ts/smartstream.nodewebhelpers.js +97 -36
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/classes.webduplexstream.js +20 -15
- package/package.json +4 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartstream.classes.smartduplex.ts +211 -78
- package/ts/smartstream.classes.streamintake.ts +5 -2
- package/ts/smartstream.classes.streamwrapper.ts +11 -12
- package/ts/smartstream.nodewebhelpers.ts +105 -37
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/classes.webduplexstream.ts +20 -13
|
@@ -56,67 +56,116 @@ export class SmartDuplex<TInput = any, TOutput = any> extends Duplex {
|
|
|
56
56
|
readableStream: ReadableStream<T>
|
|
57
57
|
): SmartDuplex<T, T> {
|
|
58
58
|
const smartDuplex = new SmartDuplex<T, T>({
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
objectMode: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Acquire reader ONCE
|
|
63
|
+
const reader = readableStream.getReader();
|
|
64
|
+
let reading = false;
|
|
65
|
+
|
|
66
|
+
// Override _read to pull from the web reader
|
|
67
|
+
smartDuplex._read = function (_size: number) {
|
|
68
|
+
if (reading) return;
|
|
69
|
+
reading = true;
|
|
70
|
+
reader.read().then(
|
|
71
|
+
({ value, done }) => {
|
|
72
|
+
reading = false;
|
|
73
|
+
if (done) {
|
|
74
|
+
smartDuplex.push(null);
|
|
75
|
+
} else {
|
|
76
|
+
smartDuplex.push(value);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
(err) => {
|
|
80
|
+
reading = false;
|
|
81
|
+
smartDuplex.destroy(err);
|
|
72
82
|
}
|
|
73
|
-
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Cancel reader on destroy
|
|
87
|
+
smartDuplex.on('close', () => {
|
|
88
|
+
reader.cancel().catch(() => {});
|
|
74
89
|
});
|
|
90
|
+
|
|
75
91
|
return smartDuplex;
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
// INSTANCE
|
|
79
|
-
private backpressuredArray: plugins.lik.BackpressuredArray<TOutput>;
|
|
95
|
+
private backpressuredArray: plugins.lik.BackpressuredArray<TOutput>;
|
|
80
96
|
public options: ISmartDuplexOptions<TInput, TOutput>;
|
|
81
|
-
private
|
|
97
|
+
private _consumerWantsData = false;
|
|
98
|
+
private _readFunctionRunning = false;
|
|
99
|
+
|
|
82
100
|
private debugLog(messageArg: string) {
|
|
83
|
-
// optional debug log
|
|
84
101
|
if (this.options.debug) {
|
|
85
102
|
console.log(messageArg);
|
|
86
103
|
}
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
constructor(optionsArg?: ISmartDuplexOptions<TInput, TOutput>) {
|
|
107
|
+
const safeOptions = optionsArg || {} as ISmartDuplexOptions<TInput, TOutput>;
|
|
90
108
|
super(
|
|
91
109
|
Object.assign(
|
|
92
110
|
{
|
|
93
111
|
highWaterMark: 1,
|
|
94
112
|
},
|
|
95
|
-
|
|
113
|
+
safeOptions
|
|
96
114
|
)
|
|
97
115
|
);
|
|
98
|
-
this.options =
|
|
116
|
+
this.options = safeOptions;
|
|
99
117
|
this.backpressuredArray = new plugins.lik.BackpressuredArray<TOutput>(
|
|
100
118
|
this.options.highWaterMark || 1
|
|
101
119
|
);
|
|
102
120
|
}
|
|
103
121
|
|
|
104
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Synchronously drains items from the backpressuredArray into the readable side.
|
|
124
|
+
* Stops when push() returns false (consumer is full) or array is empty.
|
|
125
|
+
*/
|
|
126
|
+
private _drainBackpressuredArray(): void {
|
|
127
|
+
while (this.backpressuredArray.data.length > 0) {
|
|
128
|
+
const nextChunk = this.backpressuredArray.shift();
|
|
129
|
+
if (nextChunk === null) {
|
|
130
|
+
// EOF signal — push null to end readable side
|
|
131
|
+
this.push(null);
|
|
132
|
+
this._consumerWantsData = false;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const canPushMore = this.push(nextChunk);
|
|
136
|
+
if (!canPushMore) {
|
|
137
|
+
this._consumerWantsData = false;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// _read must NOT be async — Node.js ignores the return value
|
|
144
|
+
public _read(size: number): void {
|
|
105
145
|
this.debugLog(`${this.options.name}: read was called`);
|
|
106
|
-
|
|
107
|
-
|
|
146
|
+
this._consumerWantsData = true;
|
|
147
|
+
|
|
148
|
+
// Drain any buffered items first
|
|
149
|
+
if (this.backpressuredArray.data.length > 0) {
|
|
150
|
+
this._drainBackpressuredArray();
|
|
108
151
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
152
|
+
|
|
153
|
+
// If readFunction exists and is not already running, start it
|
|
154
|
+
if (this.options.readFunction && !this._readFunctionRunning) {
|
|
155
|
+
this._readFunctionRunning = true;
|
|
156
|
+
this.options.readFunction().then(
|
|
157
|
+
() => { this._readFunctionRunning = false; },
|
|
158
|
+
(err) => { this._readFunctionRunning = false; this.destroy(err); }
|
|
159
|
+
);
|
|
115
160
|
}
|
|
116
161
|
}
|
|
117
162
|
|
|
118
163
|
public async backpressuredPush(pushArg: TOutput) {
|
|
119
164
|
const canPushMore = this.backpressuredArray.push(pushArg);
|
|
165
|
+
// Try to drain if the consumer wants data
|
|
166
|
+
if (this._consumerWantsData) {
|
|
167
|
+
this._drainBackpressuredArray();
|
|
168
|
+
}
|
|
120
169
|
if (!canPushMore) {
|
|
121
170
|
this.debugLog(`${this.options.name}: cannot push more`);
|
|
122
171
|
await this.backpressuredArray.waitForSpace();
|
|
@@ -126,83 +175,151 @@ export class SmartDuplex<TInput = any, TOutput = any> extends Duplex {
|
|
|
126
175
|
}
|
|
127
176
|
|
|
128
177
|
private asyncWritePromiseObjectmap = new plugins.lik.ObjectMap<Promise<any>>();
|
|
129
|
-
|
|
130
|
-
|
|
178
|
+
|
|
179
|
+
// _write must NOT be async — Node.js ignores the return value
|
|
180
|
+
public _write(chunk: TInput, encoding: string, callback: (error?: Error | null) => void) {
|
|
131
181
|
if (!this.options.writeFunction) {
|
|
132
182
|
return callback(new Error('No stream function provided'));
|
|
133
183
|
}
|
|
134
184
|
|
|
185
|
+
let callbackCalled = false;
|
|
186
|
+
const safeCallback = (err?: Error | null) => {
|
|
187
|
+
if (!callbackCalled) {
|
|
188
|
+
callbackCalled = true;
|
|
189
|
+
callback(err);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
135
193
|
let isTruncated = false;
|
|
136
194
|
const tools: IStreamTools = {
|
|
137
195
|
truncate: () => {
|
|
138
|
-
this.push(null);
|
|
139
196
|
isTruncated = true;
|
|
140
|
-
|
|
197
|
+
safeCallback();
|
|
198
|
+
this.push(null);
|
|
141
199
|
},
|
|
142
200
|
push: async (pushArg: TOutput) => {
|
|
143
201
|
return await this.backpressuredPush(pushArg);
|
|
144
202
|
},
|
|
145
203
|
};
|
|
146
204
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
205
|
+
const writeDeferred = plugins.smartpromise.defer();
|
|
206
|
+
this.asyncWritePromiseObjectmap.add(writeDeferred.promise);
|
|
207
|
+
|
|
208
|
+
this.options.writeFunction(chunk, tools).then(
|
|
209
|
+
(modifiedChunk) => {
|
|
210
|
+
if (isTruncated) {
|
|
211
|
+
writeDeferred.resolve();
|
|
212
|
+
this.asyncWritePromiseObjectmap.remove(writeDeferred.promise);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const finish = () => {
|
|
216
|
+
safeCallback();
|
|
217
|
+
writeDeferred.resolve();
|
|
218
|
+
this.asyncWritePromiseObjectmap.remove(writeDeferred.promise);
|
|
219
|
+
};
|
|
220
|
+
if (modifiedChunk !== undefined && modifiedChunk !== null) {
|
|
221
|
+
this.backpressuredPush(modifiedChunk).then(finish, (err) => {
|
|
222
|
+
safeCallback(err);
|
|
223
|
+
writeDeferred.resolve();
|
|
224
|
+
this.asyncWritePromiseObjectmap.remove(writeDeferred.promise);
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
finish();
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
(err) => {
|
|
231
|
+
safeCallback(err);
|
|
232
|
+
writeDeferred.resolve();
|
|
160
233
|
this.asyncWritePromiseObjectmap.remove(writeDeferred.promise);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
callback(err);
|
|
164
|
-
}
|
|
234
|
+
}
|
|
235
|
+
);
|
|
165
236
|
}
|
|
166
237
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
238
|
+
// _final must NOT be async — Node.js ignores the return value
|
|
239
|
+
public _final(callback: (error?: Error | null) => void) {
|
|
240
|
+
let callbackCalled = false;
|
|
241
|
+
const safeCallback = (err?: Error | null) => {
|
|
242
|
+
if (!callbackCalled) {
|
|
243
|
+
callbackCalled = true;
|
|
244
|
+
callback(err);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
176
247
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
248
|
+
Promise.all(this.asyncWritePromiseObjectmap.getArray()).then(() => {
|
|
249
|
+
if (this.options.finalFunction) {
|
|
250
|
+
const tools: IStreamTools = {
|
|
251
|
+
truncate: () => safeCallback(),
|
|
252
|
+
push: async (pipeObject) => {
|
|
253
|
+
return await this.backpressuredPush(pipeObject);
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
this.options.finalFunction(tools).then(
|
|
258
|
+
(finalChunk) => {
|
|
259
|
+
const pushNull = () => {
|
|
260
|
+
this.backpressuredArray.push(null);
|
|
261
|
+
if (this._consumerWantsData) {
|
|
262
|
+
this._drainBackpressuredArray();
|
|
263
|
+
}
|
|
264
|
+
safeCallback();
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
if (finalChunk !== undefined && finalChunk !== null) {
|
|
268
|
+
this.backpressuredPush(finalChunk).then(pushNull, (err) => {
|
|
269
|
+
safeCallback(err);
|
|
270
|
+
});
|
|
271
|
+
} else {
|
|
272
|
+
pushNull();
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
(err) => {
|
|
276
|
+
this.backpressuredArray.push(null);
|
|
277
|
+
if (this._consumerWantsData) {
|
|
278
|
+
this._drainBackpressuredArray();
|
|
279
|
+
}
|
|
280
|
+
safeCallback(err);
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
} else {
|
|
183
284
|
this.backpressuredArray.push(null);
|
|
184
|
-
|
|
185
|
-
|
|
285
|
+
if (this._consumerWantsData) {
|
|
286
|
+
this._drainBackpressuredArray();
|
|
287
|
+
}
|
|
288
|
+
safeCallback();
|
|
186
289
|
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
290
|
+
}, (err) => {
|
|
291
|
+
safeCallback(err);
|
|
292
|
+
});
|
|
190
293
|
}
|
|
191
294
|
|
|
192
295
|
public async getWebStreams(): Promise<{ readable: ReadableStream; writable: WritableStream }> {
|
|
193
296
|
const duplex = this;
|
|
297
|
+
let readableClosed = false;
|
|
298
|
+
|
|
194
299
|
const readable = new ReadableStream({
|
|
195
300
|
start(controller) {
|
|
196
|
-
|
|
301
|
+
const onReadable = () => {
|
|
197
302
|
let chunk;
|
|
198
303
|
while (null !== (chunk = duplex.read())) {
|
|
199
304
|
controller.enqueue(chunk);
|
|
200
305
|
}
|
|
201
|
-
}
|
|
306
|
+
};
|
|
202
307
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
308
|
+
const onEnd = () => {
|
|
309
|
+
if (!readableClosed) {
|
|
310
|
+
readableClosed = true;
|
|
311
|
+
controller.close();
|
|
312
|
+
}
|
|
313
|
+
cleanup();
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const cleanup = () => {
|
|
317
|
+
duplex.removeListener('readable', onReadable);
|
|
318
|
+
duplex.removeListener('end', onEnd);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
duplex.on('readable', onReadable);
|
|
322
|
+
duplex.on('end', onEnd);
|
|
206
323
|
},
|
|
207
324
|
cancel(reason) {
|
|
208
325
|
duplex.destroy(new Error(reason));
|
|
@@ -212,22 +329,38 @@ export class SmartDuplex<TInput = any, TOutput = any> extends Duplex {
|
|
|
212
329
|
const writable = new WritableStream({
|
|
213
330
|
write(chunk) {
|
|
214
331
|
return new Promise<void>((resolve, reject) => {
|
|
332
|
+
let resolved = false;
|
|
333
|
+
const onDrain = () => {
|
|
334
|
+
if (!resolved) {
|
|
335
|
+
resolved = true;
|
|
336
|
+
resolve();
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
215
340
|
const isBackpressured = !duplex.write(chunk, (error) => {
|
|
216
341
|
if (error) {
|
|
217
|
-
|
|
218
|
-
|
|
342
|
+
if (!resolved) {
|
|
343
|
+
resolved = true;
|
|
344
|
+
duplex.removeListener('drain', onDrain);
|
|
345
|
+
reject(error);
|
|
346
|
+
}
|
|
347
|
+
} else if (!isBackpressured && !resolved) {
|
|
348
|
+
resolved = true;
|
|
219
349
|
resolve();
|
|
220
350
|
}
|
|
221
351
|
});
|
|
222
352
|
|
|
223
353
|
if (isBackpressured) {
|
|
224
|
-
duplex.once('drain',
|
|
354
|
+
duplex.once('drain', onDrain);
|
|
225
355
|
}
|
|
226
356
|
});
|
|
227
357
|
},
|
|
228
358
|
close() {
|
|
229
359
|
return new Promise<void>((resolve, reject) => {
|
|
230
|
-
duplex.end(
|
|
360
|
+
duplex.end((err: Error | null) => {
|
|
361
|
+
if (err) reject(err);
|
|
362
|
+
else resolve();
|
|
363
|
+
});
|
|
231
364
|
});
|
|
232
365
|
},
|
|
233
366
|
abort(reason) {
|
|
@@ -6,8 +6,11 @@ export class StreamIntake<T> extends plugins.stream.Readable {
|
|
|
6
6
|
const intakeStream = new StreamIntake<U>(options);
|
|
7
7
|
|
|
8
8
|
if (inputStream instanceof plugins.stream.Readable) {
|
|
9
|
-
inputStream.on('
|
|
10
|
-
|
|
9
|
+
inputStream.on('readable', () => {
|
|
10
|
+
let chunk: U;
|
|
11
|
+
while (null !== (chunk = inputStream.read() as U)) {
|
|
12
|
+
intakeStream.pushData(chunk);
|
|
13
|
+
}
|
|
11
14
|
});
|
|
12
15
|
|
|
13
16
|
inputStream.on('end', () => {
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import * as plugins from './smartstream.plugins.js';
|
|
2
2
|
|
|
3
|
-
// interfaces
|
|
4
|
-
import { Transform } from 'stream';
|
|
5
|
-
|
|
6
3
|
export interface IErrorFunction {
|
|
7
4
|
(err: Error): any;
|
|
8
5
|
}
|
|
@@ -82,15 +79,17 @@ export class StreamWrapper {
|
|
|
82
79
|
|
|
83
80
|
this.streamStartedDeferred.resolve();
|
|
84
81
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
82
|
+
let resolved = false;
|
|
83
|
+
const safeResolve = () => {
|
|
84
|
+
if (!resolved) {
|
|
85
|
+
resolved = true;
|
|
86
|
+
done.resolve();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
finalStream.on('end', safeResolve);
|
|
91
|
+
finalStream.on('close', safeResolve);
|
|
92
|
+
finalStream.on('finish', safeResolve);
|
|
94
93
|
return done.promise;
|
|
95
94
|
}
|
|
96
95
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as plugins from './smartstream.plugins.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Creates a Web ReadableStream from a file.
|
|
4
|
+
* Creates a Web ReadableStream from a file using pull-based backpressure.
|
|
5
5
|
*
|
|
6
6
|
* @param filePath - The path to the file to be read
|
|
7
7
|
* @returns A Web ReadableStream that reads the file in chunks
|
|
@@ -11,23 +11,53 @@ export function createWebReadableStreamFromFile(filePath: string): ReadableStrea
|
|
|
11
11
|
|
|
12
12
|
return new ReadableStream({
|
|
13
13
|
start(controller) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
controller.enqueue(chunk as Uint8Array);
|
|
14
|
+
fileStream.on('error', (err) => {
|
|
15
|
+
controller.error(err);
|
|
17
16
|
});
|
|
18
17
|
|
|
19
|
-
// When the file stream ends, close the Web ReadableStream
|
|
20
18
|
fileStream.on('end', () => {
|
|
21
19
|
controller.close();
|
|
22
20
|
});
|
|
23
21
|
|
|
24
|
-
//
|
|
25
|
-
fileStream.
|
|
26
|
-
|
|
22
|
+
// Pause immediately — pull() will drive reads
|
|
23
|
+
fileStream.pause();
|
|
24
|
+
},
|
|
25
|
+
pull(controller) {
|
|
26
|
+
return new Promise<void>((resolve, reject) => {
|
|
27
|
+
const chunk = fileStream.read();
|
|
28
|
+
if (chunk !== null) {
|
|
29
|
+
controller.enqueue(chunk as Uint8Array);
|
|
30
|
+
resolve();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// No data available yet — wait for 'readable' or 'end'
|
|
34
|
+
const onReadable = () => {
|
|
35
|
+
cleanup();
|
|
36
|
+
const data = fileStream.read();
|
|
37
|
+
if (data !== null) {
|
|
38
|
+
controller.enqueue(data as Uint8Array);
|
|
39
|
+
}
|
|
40
|
+
resolve();
|
|
41
|
+
};
|
|
42
|
+
const onEnd = () => {
|
|
43
|
+
cleanup();
|
|
44
|
+
resolve();
|
|
45
|
+
};
|
|
46
|
+
const onError = (err: Error) => {
|
|
47
|
+
cleanup();
|
|
48
|
+
reject(err);
|
|
49
|
+
};
|
|
50
|
+
const cleanup = () => {
|
|
51
|
+
fileStream.removeListener('readable', onReadable);
|
|
52
|
+
fileStream.removeListener('end', onEnd);
|
|
53
|
+
fileStream.removeListener('error', onError);
|
|
54
|
+
};
|
|
55
|
+
fileStream.once('readable', onReadable);
|
|
56
|
+
fileStream.once('end', onEnd);
|
|
57
|
+
fileStream.once('error', onError);
|
|
27
58
|
});
|
|
28
59
|
},
|
|
29
60
|
cancel() {
|
|
30
|
-
// If the Web ReadableStream is canceled, destroy the file stream
|
|
31
61
|
fileStream.destroy();
|
|
32
62
|
}
|
|
33
63
|
});
|
|
@@ -43,23 +73,25 @@ export function convertWebReadableToNodeReadable(webStream: ReadableStream<Uint8
|
|
|
43
73
|
const reader = webStream.getReader();
|
|
44
74
|
|
|
45
75
|
return new plugins.stream.Readable({
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
read() {
|
|
77
|
+
reader.read().then(
|
|
78
|
+
({ value, done }) => {
|
|
79
|
+
if (done) {
|
|
80
|
+
this.push(null);
|
|
81
|
+
} else {
|
|
82
|
+
this.push(Buffer.from(value));
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
(err) => {
|
|
86
|
+
this.destroy(err);
|
|
53
87
|
}
|
|
54
|
-
|
|
55
|
-
this.destroy(err); // Handle errors by destroying the stream
|
|
56
|
-
}
|
|
88
|
+
);
|
|
57
89
|
}
|
|
58
90
|
});
|
|
59
91
|
}
|
|
60
92
|
|
|
61
93
|
/**
|
|
62
|
-
* Converts a Node.js Readable stream to a Web ReadableStream.
|
|
94
|
+
* Converts a Node.js Readable stream to a Web ReadableStream using pull-based backpressure.
|
|
63
95
|
*
|
|
64
96
|
* @param nodeStream - The Node.js Readable stream to convert
|
|
65
97
|
* @returns A Web ReadableStream that reads data from the Node.js Readable stream
|
|
@@ -67,16 +99,50 @@ export function convertWebReadableToNodeReadable(webStream: ReadableStream<Uint8
|
|
|
67
99
|
export function convertNodeReadableToWebReadable(nodeStream: plugins.stream.Readable): ReadableStream<Uint8Array> {
|
|
68
100
|
return new ReadableStream({
|
|
69
101
|
start(controller) {
|
|
70
|
-
nodeStream.on('
|
|
71
|
-
controller.
|
|
102
|
+
nodeStream.on('error', (err) => {
|
|
103
|
+
controller.error(err);
|
|
72
104
|
});
|
|
73
105
|
|
|
74
106
|
nodeStream.on('end', () => {
|
|
75
107
|
controller.close();
|
|
76
108
|
});
|
|
77
109
|
|
|
78
|
-
|
|
79
|
-
|
|
110
|
+
// Pause immediately — pull() will drive reads
|
|
111
|
+
nodeStream.pause();
|
|
112
|
+
},
|
|
113
|
+
pull(controller) {
|
|
114
|
+
return new Promise<void>((resolve, reject) => {
|
|
115
|
+
const chunk = nodeStream.read();
|
|
116
|
+
if (chunk !== null) {
|
|
117
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
118
|
+
resolve();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// No data available yet — wait for 'readable' or 'end'
|
|
122
|
+
const onReadable = () => {
|
|
123
|
+
cleanup();
|
|
124
|
+
const data = nodeStream.read();
|
|
125
|
+
if (data !== null) {
|
|
126
|
+
controller.enqueue(new Uint8Array(data));
|
|
127
|
+
}
|
|
128
|
+
resolve();
|
|
129
|
+
};
|
|
130
|
+
const onEnd = () => {
|
|
131
|
+
cleanup();
|
|
132
|
+
resolve();
|
|
133
|
+
};
|
|
134
|
+
const onError = (err: Error) => {
|
|
135
|
+
cleanup();
|
|
136
|
+
reject(err);
|
|
137
|
+
};
|
|
138
|
+
const cleanup = () => {
|
|
139
|
+
nodeStream.removeListener('readable', onReadable);
|
|
140
|
+
nodeStream.removeListener('end', onEnd);
|
|
141
|
+
nodeStream.removeListener('error', onError);
|
|
142
|
+
};
|
|
143
|
+
nodeStream.once('readable', onReadable);
|
|
144
|
+
nodeStream.once('end', onEnd);
|
|
145
|
+
nodeStream.once('error', onError);
|
|
80
146
|
});
|
|
81
147
|
},
|
|
82
148
|
cancel() {
|
|
@@ -95,19 +161,23 @@ export function convertWebWritableToNodeWritable(webWritable: WritableStream<Uin
|
|
|
95
161
|
const writer = webWritable.getWriter();
|
|
96
162
|
|
|
97
163
|
return new plugins.stream.Writable({
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
callback()
|
|
102
|
-
|
|
103
|
-
callback(err);
|
|
104
|
-
}
|
|
164
|
+
write(chunk, encoding, callback) {
|
|
165
|
+
writer.write(new Uint8Array(chunk)).then(
|
|
166
|
+
() => callback(),
|
|
167
|
+
(err) => callback(err)
|
|
168
|
+
);
|
|
105
169
|
},
|
|
106
170
|
final(callback) {
|
|
107
171
|
writer.close().then(() => callback()).catch(callback);
|
|
108
172
|
},
|
|
109
173
|
destroy(err, callback) {
|
|
110
|
-
|
|
174
|
+
if (err) {
|
|
175
|
+
writer.abort(err).then(() => callback(err)).catch(() => callback(err));
|
|
176
|
+
} else {
|
|
177
|
+
// Clean destroy — just release the lock
|
|
178
|
+
writer.releaseLock();
|
|
179
|
+
callback(null);
|
|
180
|
+
}
|
|
111
181
|
}
|
|
112
182
|
});
|
|
113
183
|
}
|
|
@@ -133,7 +203,7 @@ export function convertNodeWritableToWebWritable(nodeWritable: plugins.stream.Wr
|
|
|
133
203
|
},
|
|
134
204
|
close() {
|
|
135
205
|
return new Promise((resolve, reject) => {
|
|
136
|
-
nodeWritable.end((err) => {
|
|
206
|
+
nodeWritable.end((err: Error | null) => {
|
|
137
207
|
if (err) {
|
|
138
208
|
reject(err);
|
|
139
209
|
} else {
|
|
@@ -143,9 +213,7 @@ export function convertNodeWritableToWebWritable(nodeWritable: plugins.stream.Wr
|
|
|
143
213
|
});
|
|
144
214
|
},
|
|
145
215
|
abort(reason) {
|
|
146
|
-
|
|
147
|
-
nodeWritable.destroy(reason);
|
|
148
|
-
});
|
|
216
|
+
nodeWritable.destroy(reason instanceof Error ? reason : new Error(String(reason)));
|
|
149
217
|
}
|
|
150
218
|
});
|
|
151
|
-
}
|
|
219
|
+
}
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartstream',
|
|
6
|
-
version: '3.
|
|
6
|
+
version: '3.4.0',
|
|
7
7
|
description: 'A library to simplify the creation and manipulation of Node.js streams, providing utilities for handling transform, duplex, and readable/writable streams effectively in TypeScript.'
|
|
8
8
|
}
|