@@ -2,9 +2,16 @@ import { Readable } from "stream";
22import { createReadStream , promises as fs } from "fs" ;
33import { BaseStream } from "./base-stream" ;
44import { IFCA } from "../ifca" ;
5- import { AnyIterable , Constructor , DroppedChunk , ResolvablePromiseObject , TransformFunction } from "../types" ;
5+ import { AnyIterable , Constructor , DroppedChunk , ResolvablePromiseObject , TransformFunction , MaybePromise } from "../types" ;
66import { createResolvablePromiseObject , isAsyncFunction } from "../utils" ;
77
8+ type Reducer < T , U > = {
9+ isAsync : boolean ,
10+ value ?: U ,
11+ onFirstChunkCallback : Function ,
12+ onChunkCallback : ( chunk : T ) => MaybePromise < void >
13+ } ;
14+
815export class DataStream < T > implements BaseStream < T > , AsyncIterable < T > {
916 constructor ( ) {
1017 this . ifca = new IFCA < T , T , any > ( 2 , ( chunk : T ) => chunk ) ;
@@ -67,10 +74,27 @@ export class DataStream<T> implements BaseStream<T>, AsyncIterable<T> {
6774 return this . asNewFlattenedStream ( this . map < AnyIterable < U > , W > ( callback , ...args ) ) ;
6875 }
6976
77+ async reduce < U = T > ( callback : ( previousValue : U , currentChunk : T ) => MaybePromise < U > , initial ?: U ) : Promise < U > {
78+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce#parameters
79+ //
80+ // initialValue (optional):
81+ // A value to which previousValue is initialized the first time the callback is called.
82+ // If initialValue is specified, that also causes currentValue to be initialized to the first
83+ // value in the array. If initialValue is not specified, previousValue is initialized to the first
84+ // value in the array, and currentValue is initialized to the second value in the array.
85+
86+ const reducer = this . getReducer < U > ( callback , initial ) ;
87+ const reader = reducer . isAsync
88+ ? this . getReaderAsyncCallback ( true , reducer )
89+ : this . getReader ( true , reducer ) ;
90+
91+ return reader ( ) . then ( ( ) => reducer . value as U ) ;
92+ }
93+
7094 async toArray ( ) : Promise < T [ ] > {
7195 const chunks : Array < T > = [ ] ;
7296
73- await ( this . getReader ( true , chunk => { chunks . push ( chunk ) ; } ) ) ( ) ;
97+ await ( this . getReader ( true , { onChunkCallback : chunk => { chunks . push ( chunk ) ; } } ) ) ( ) ;
7498
7599 return chunks ;
76100 }
@@ -97,6 +121,37 @@ export class DataStream<T> implements BaseStream<T>, AsyncIterable<T> {
97121 }
98122 }
99123
124+ protected getReducer < U > (
125+ callback : ( previousValue : U , currentChunk : T ) => MaybePromise < U > ,
126+ initial ?: U
127+ ) : Reducer < T , U > {
128+ const reducer : any = {
129+ isAsync : isAsyncFunction ( callback ) ,
130+ value : initial
131+ } ;
132+
133+ reducer . onFirstChunkCallback = async ( chunk : T ) : Promise < void > => {
134+ if ( initial === undefined ) {
135+ // Here we should probably check if typeof chunk is U.
136+ reducer . value = chunk as unknown as U ;
137+ } else {
138+ reducer . value = await callback ( reducer . value as U , chunk ) ;
139+ }
140+ } ;
141+
142+ if ( reducer . isAsync ) {
143+ reducer . onChunkCallback = async ( chunk : T ) : Promise < void > => {
144+ reducer . value = await callback ( reducer . value as U , chunk ) as U ;
145+ } ;
146+ } else {
147+ reducer . onChunkCallback = ( chunk : T ) : void => {
148+ reducer . value = callback ( reducer . value as U , chunk ) as U ;
149+ } ;
150+ }
151+
152+ return reducer as Reducer < T , U > ;
153+ }
154+
100155 protected asNewFlattenedStream < U , W extends DataStream < AnyIterable < U > > > (
101156 fromStream : W ,
102157 onEndYield ?: ( ) => { yield : boolean , value ?: U }
@@ -116,34 +171,109 @@ export class DataStream<T> implements BaseStream<T>, AsyncIterable<T> {
116171 } ) ( fromStream ) ) ;
117172 }
118173
119- // For now this method assumes both callbacks are sync ones.
120174 protected getReader (
121175 uncork : boolean ,
122- onChunkCallback : ( chunk : T ) => void ,
123- onEndCallback ?: Function
176+ callbacks : {
177+ onChunkCallback : ( chunk : T ) => void ,
178+ onFirstChunkCallback ?: Function ,
179+ onEndCallback ?: Function
180+ }
124181 ) : ( ) => Promise < void > {
182+ /* eslint-disable complexity */
125183 return async ( ) => {
126184 if ( uncork && this . corked ) {
127185 this . _uncork ( ) ;
128186 }
129187
188+ let chunk = this . ifca . read ( ) ;
189+
190+ // A bit of code duplication but we don't want to have unnecessary if inside a while loop
191+ // which is called for every chunk or wrap the common code inside another function due to performance.
192+ if ( callbacks . onFirstChunkCallback ) {
193+ if ( chunk instanceof Promise ) {
194+ chunk = await chunk ;
195+ }
196+
197+ if ( chunk !== null ) {
198+ await callbacks . onFirstChunkCallback ( chunk ) ;
199+ chunk = this . ifca . read ( ) ;
200+ }
201+ }
202+
130203 // eslint-disable-next-line no-constant-condition
131204 while ( true ) {
132- let chunk = this . ifca . read ( ) ;
205+ if ( chunk instanceof Promise ) {
206+ chunk = await chunk ;
207+ }
208+
209+ if ( chunk === null ) {
210+ break ;
211+ }
212+
213+ callbacks . onChunkCallback ( chunk ) ;
214+
215+ chunk = this . ifca . read ( ) ;
216+ }
217+
218+ if ( callbacks . onEndCallback ) {
219+ await callbacks . onEndCallback . call ( this ) ;
220+ }
221+ } ;
222+ /* eslint-enable complexity */
223+ }
224+
225+ // This is duplicated '.getReader()' method with the only difference that 'onChunkCallback'
226+ // is an async function so we have to 'await' on it for each chunk. Since it has significant effect
227+ // on processing time (and makes it asynchronous) I have extracted it as a separate method.
228+ protected getReaderAsyncCallback (
229+ uncork : boolean ,
230+ callbacks : {
231+ onChunkCallback : ( chunk : T ) => MaybePromise < void > ,
232+ onFirstChunkCallback ?: Function ,
233+ onEndCallback ?: Function
234+ }
235+ ) : ( ) => Promise < void > {
236+ /* eslint-disable complexity */
237+ return async ( ) => {
238+ if ( uncork && this . corked ) {
239+ this . _uncork ( ) ;
240+ }
241+
242+ let chunk = this . ifca . read ( ) ;
133243
244+ // A bit of code duplication but we don't want to have unnecessary if inside a while loop
245+ // which is called for every chunk or wrap the common code inside another function due to performance.
246+ if ( callbacks . onFirstChunkCallback ) {
134247 if ( chunk instanceof Promise ) {
135248 chunk = await chunk ;
136249 }
250+
251+ if ( chunk !== null ) {
252+ await callbacks . onFirstChunkCallback ( chunk ) ;
253+ chunk = this . ifca . read ( ) ;
254+ }
255+ }
256+
257+ // eslint-disable-next-line no-constant-condition
258+ while ( true ) {
259+ if ( chunk instanceof Promise ) {
260+ chunk = await chunk ;
261+ }
262+
137263 if ( chunk === null ) {
138- if ( onEndCallback ) {
139- onEndCallback . call ( this ) ;
140- }
141264 break ;
142265 }
143266
144- onChunkCallback ( chunk ) ;
267+ await callbacks . onChunkCallback ( chunk ) ;
268+
269+ chunk = this . ifca . read ( ) ;
270+ }
271+
272+ if ( callbacks . onEndCallback ) {
273+ await callbacks . onEndCallback . call ( this ) ;
145274 }
146275 } ;
276+ /* eslint-enable complexity */
147277 }
148278
149279 // Native node readables also implement AsyncIterable interface.
0 commit comments