@push.rocks/smartstream 3.3.0 → 3.4.2
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/.smartconfig.json +27 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/smartstream.classes.smartduplex.d.ts +12 -6
- 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 +16 -13
- package/dist_ts/smartstream.functions.d.ts +2 -2
- package/dist_ts/smartstream.functions.js +1 -1
- 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 +24 -16
- package/license +21 -0
- package/package.json +12 -10
- package/readme.md +145 -15
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartstream.classes.smartduplex.ts +214 -81
- package/ts/smartstream.classes.streamintake.ts +5 -2
- package/ts/smartstream.classes.streamwrapper.ts +18 -14
- package/ts/smartstream.functions.ts +2 -2
- package/ts/smartstream.nodewebhelpers.ts +105 -37
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/classes.webduplexstream.ts +24 -14
|
@@ -7,11 +7,11 @@ export interface IStreamTools {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface IStreamWriteFunction<T, rT> {
|
|
10
|
-
(chunkArg: T, toolsArg: IStreamTools): Promise<rT>;
|
|
10
|
+
(chunkArg: T, toolsArg: IStreamTools): Promise<rT | void | null | undefined>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface IStreamFinalFunction<rT> {
|
|
14
|
-
(toolsArg: IStreamTools): Promise<rT>;
|
|
14
|
+
(toolsArg: IStreamTools): Promise<rT | void | null | undefined>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface ISmartDuplexOptions<TInput, TOutput> extends DuplexOptions {
|
|
@@ -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 | null>;
|
|
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 =
|
|
99
|
-
this.backpressuredArray = new plugins.lik.BackpressuredArray<TOutput>(
|
|
116
|
+
this.options = safeOptions;
|
|
117
|
+
this.backpressuredArray = new plugins.lik.BackpressuredArray<TOutput | null>(
|
|
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
|
}
|
|
@@ -62,7 +59,7 @@ export class StreamWrapper {
|
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
// combine the stream
|
|
65
|
-
let finalStream = null;
|
|
62
|
+
let finalStream: plugins.stream.Duplex | null = null;
|
|
66
63
|
let firstIteration: boolean = true;
|
|
67
64
|
for (const stream of streamExecutionArray) {
|
|
68
65
|
if (firstIteration === true) {
|
|
@@ -74,23 +71,30 @@ export class StreamWrapper {
|
|
|
74
71
|
for (const customEventObject of this.customEventObjectArray) {
|
|
75
72
|
stream.on(customEventObject.eventName, customEventObject.eventFunction);
|
|
76
73
|
}
|
|
77
|
-
if (!firstIteration) {
|
|
74
|
+
if (!firstIteration && finalStream) {
|
|
78
75
|
finalStream = finalStream.pipe(stream);
|
|
79
76
|
}
|
|
80
77
|
firstIteration = false;
|
|
81
78
|
}
|
|
82
79
|
|
|
80
|
+
if (!finalStream) {
|
|
81
|
+
done.resolve();
|
|
82
|
+
return done.promise;
|
|
83
|
+
}
|
|
84
|
+
|
|
83
85
|
this.streamStartedDeferred.resolve();
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
let resolved = false;
|
|
88
|
+
const safeResolve = () => {
|
|
89
|
+
if (!resolved) {
|
|
90
|
+
resolved = true;
|
|
91
|
+
done.resolve();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
finalStream.on('end', safeResolve);
|
|
96
|
+
finalStream.on('close', safeResolve);
|
|
97
|
+
finalStream.on('finish', safeResolve);
|
|
94
98
|
return done.promise;
|
|
95
99
|
}
|
|
96
100
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type DuplexOptions } from 'stream';
|
|
2
2
|
import { SmartDuplex } from './smartstream.classes.smartduplex.js';
|
|
3
3
|
|
|
4
4
|
export interface AsyncTransformFunction<TInput, TOutput> {
|
|
@@ -7,7 +7,7 @@ export interface AsyncTransformFunction<TInput, TOutput> {
|
|
|
7
7
|
|
|
8
8
|
export function createTransformFunction<TInput, TOutput>(
|
|
9
9
|
asyncFunction: AsyncTransformFunction<TInput, TOutput>,
|
|
10
|
-
options?:
|
|
10
|
+
options?: DuplexOptions
|
|
11
11
|
): SmartDuplex {
|
|
12
12
|
const smartDuplexStream = new SmartDuplex({
|
|
13
13
|
...options,
|