UNPKG

asynciterator

Version:

An asynchronous iterator library for advanced object pipelines.

1,261 lines 82.2 kB
"use strict"; /** * An asynchronous iterator library for advanced object pipelines * @module asynciterator */ Object.defineProperty(exports, "__esModule", { value: true }); exports.isIterator = exports.isIterable = exports.isSourceExpression = exports.isPromise = exports.isEventEmitter = exports.isFunction = exports.range = exports.union = exports.fromIterable = exports.fromIterator = exports.fromArray = exports.single = exports.empty = exports.wrap = exports.WrappingIterator = exports.ClonedIterator = exports.UnionIterator = exports.MultiTransformIterator = exports.SimpleTransformIterator = exports.TransformIterator = exports.BufferedIterator = exports.MappingIterator = exports.DESTINATION = exports.identity = exports.IntegerIterator = exports.ArrayIterator = exports.SingletonIterator = exports.EmptyIterator = exports.AsyncIterator = exports.DESTROYED = exports.ENDED = exports.CLOSED = exports.CLOSING = exports.OPEN = exports.INIT = exports.setTaskScheduler = exports.getTaskScheduler = exports.scheduleTask = exports.LinkedList = void 0; const events_1 = require("events"); const linkedlist_1 = require("./linkedlist.js"); Object.defineProperty(exports, "LinkedList", { enumerable: true, get: function () { return linkedlist_1.LinkedList; } }); const taskscheduler_1 = require("./taskscheduler.js"); let taskScheduler = taskscheduler_1.createTaskScheduler(); /** Schedules the given task for asynchronous execution. */ function scheduleTask(task) { taskScheduler(task); } exports.scheduleTask = scheduleTask; /** Returns the asynchronous task scheduler. */ function getTaskScheduler() { return taskScheduler; } exports.getTaskScheduler = getTaskScheduler; /** Sets the asynchronous task scheduler. */ function setTaskScheduler(scheduler) { taskScheduler = scheduler; } exports.setTaskScheduler = setTaskScheduler; /** ID of the INIT state. An iterator is initializing if it is preparing main item generation. It can already produce items. @type integer */ exports.INIT = 1 << 0; /** ID of the OPEN state. An iterator is open if it can generate new items. @type integer */ exports.OPEN = 1 << 1; /** ID of the CLOSING state. An iterator is closing if item generation is pending but will not be scheduled again. @type integer */ exports.CLOSING = 1 << 2; /** ID of the CLOSED state. An iterator is closed if it no longer actively generates new items. Items might still be available. @type integer */ exports.CLOSED = 1 << 3; /** ID of the ENDED state. An iterator has ended if no further items will become available. The 'end' event is guaranteed to have been called when in this state. @type integer */ exports.ENDED = 1 << 4; /** ID of the DESTROYED state. An iterator has been destroyed after calling {@link module:asynciterator.AsyncIterator#destroy}. The 'end' event has not been called, as pending elements were voided. @type integer */ exports.DESTROYED = 1 << 5; /** An asynchronous iterator provides pull-based access to a stream of objects. @extends module:asynciterator.EventEmitter */ class AsyncIterator extends events_1.EventEmitter { /** Creates a new `AsyncIterator`. */ constructor(initialState = exports.OPEN) { super(); this._readable = false; this._state = initialState; this.on('newListener', waitForDataListener); } /** Changes the iterator to the given state if possible and necessary, possibly emitting events to signal that change. @protected @param {integer} newState The ID of the new state @param {boolean} [eventAsync=false] Whether resulting events should be emitted asynchronously @returns {boolean} Whether the state was changed @emits module:asynciterator.AsyncIterator.end */ _changeState(newState, eventAsync = false) { // Validate the state change const valid = newState > this._state && this._state < exports.ENDED; if (valid) { this._state = newState; // Emit the `end` event when changing to ENDED if (newState === exports.ENDED) { if (!eventAsync) this.emit('end'); else taskScheduler(() => this.emit('end')); } } return valid; } /** Tries to read the next item from the iterator. This is the main method for reading the iterator in _on-demand mode_, where new items are only created when needed by consumers. If no items are currently available, this methods returns `null`. The {@link module:asynciterator.event:readable} event will then signal when new items might be ready. To read all items from the iterator, switch to _flow mode_ by subscribing to the {@link module:asynciterator.event:data} event. When in flow mode, do not use the `read` method. @returns {object?} The next item, or `null` if none is available */ read() { return null; } /** The iterator emits a `readable` event when it might have new items available after having had no items available right before this event. If the iterator is not in flow mode, items can be retrieved by calling {@link module:asynciterator.AsyncIterator#read}. @event module:asynciterator.readable */ /** The iterator emits a `data` event with a new item as soon as it becomes available. When one or more listeners are attached to the `data` event, the iterator switches to _flow mode_, generating and emitting new items as fast as possible. This drains the source and might create backpressure on the consumers, so only subscribe to this event if this behavior is intended. In flow mode, don't use {@link module:asynciterator.AsyncIterator#read}. To switch back to _on-demand mode_, remove all listeners from the `data` event. You can then obtain items through `read` again. @event module:asynciterator.data @param {object} item The new item */ /** Invokes the callback for each remaining item in the iterator. Switches the iterator to flow mode. @param {Function} callback A function that will be called with each item @param {object?} self The `this` pointer for the callback */ forEach(callback, self) { this.on('data', bind(callback, self)); } /** Stops the iterator from generating new items. Already generated items or terminating items can still be emitted. After this, the iterator will end asynchronously. @emits module:asynciterator.AsyncIterator.end */ close() { if (this._changeState(exports.CLOSED)) this._endAsync(); } /** Destroy the iterator and stop it from generating new items. This will not do anything if the iterator was already ended or destroyed. All internal resources will be released an no new items will be emitted, even not already generated items. Implementors should not override this method, but instead implement {@link module:asynciterator.AsyncIterator#_destroy}. @param {Error} [cause] An optional error to emit. @emits module:asynciterator.AsyncIterator.end @emits module:asynciterator.AsyncIterator.error Only if an error is passed. */ destroy(cause) { if (!this.done) { this._destroy(cause, error => { cause = cause || error; if (cause) this.emit('error', cause); this._end(true); }); } } /** Called by {@link module:asynciterator.AsyncIterator#destroy}. Implementers can override this, but this should not be called directly. @param {?Error} cause The reason why the iterator is destroyed. @param {Function} callback A callback function with an optional error argument. */ _destroy(cause, callback) { callback(); } /** Ends the iterator and cleans up. Should never be called before {@link module:asynciterator.AsyncIterator#close}; typically, `close` is responsible for calling `_end`. @param {boolean} [destroy] If the iterator should be forcefully destroyed. @protected @emits module:asynciterator.AsyncIterator.end */ _end(destroy = false) { if (this._changeState(destroy ? exports.DESTROYED : exports.ENDED)) { this._readable = false; this.removeAllListeners('readable'); this.removeAllListeners('data'); this.removeAllListeners('end'); } } /** Asynchronously calls `_end`. @protected */ _endAsync() { taskScheduler(() => this._end()); } /** The `end` event is emitted after the last item of the iterator has been read. @event module:asynciterator.end */ /** Gets or sets whether this iterator might have items available for read. A value of `false` means there are _definitely_ no items available; a value of `true` means items _might_ be available. @type boolean @emits module:asynciterator.AsyncIterator.readable */ get readable() { return this._readable; } set readable(readable) { readable = Boolean(readable) && !this.done; // Set the readable value only if it has changed if (this._readable !== readable) { this._readable = readable; // If the iterator became readable, emit the `readable` event if (readable) taskScheduler(() => this.emit('readable')); } } /** Gets whether the iterator has stopped generating new items. @type boolean @readonly */ get closed() { return this._state >= exports.CLOSING; } /** Gets whether the iterator has finished emitting items. @type boolean @readonly */ get ended() { return this._state === exports.ENDED; } /** Gets whether the iterator has been destroyed. @type boolean @readonly */ get destroyed() { return this._state === exports.DESTROYED; } /** Gets whether the iterator will not emit anymore items, either due to being closed or due to being destroyed. @type boolean @readonly */ get done() { return this._state >= exports.ENDED; } /* Generates a textual representation of the iterator. */ toString() { const details = this._toStringDetails(); return `[${this.constructor.name}${details ? ` ${details}` : ''}]`; } /** Generates details for a textual representation of the iterator. @protected */ _toStringDetails() { return ''; } /** Consume all remaining items of the iterator into an array that will be returned asynchronously. @param {object} [options] Settings for array creation @param {integer} [options.limit] The maximum number of items to place in the array. */ toArray(options) { const items = []; const limit = typeof (options === null || options === void 0 ? void 0 : options.limit) === 'number' ? options.limit : Infinity; return this.ended || limit <= 0 ? Promise.resolve(items) : new Promise((resolve, reject) => { // Collect and return all items up to the limit const resolveItems = () => resolve(items); const pushItem = (item) => { items.push(item); if (items.length >= limit) { this.removeListener('error', reject); this.removeListener('data', pushItem); this.removeListener('end', resolveItems); resolve(items); } }; // Start item collection this.on('error', reject); this.on('data', pushItem); this.on('end', resolveItems); }); } /** Retrieves the property with the given name from the iterator. If no callback is passed, it returns the value of the property or `undefined` if the property is not set. If a callback is passed, it returns `undefined` and calls the callback with the property the moment it is set. @param {string} propertyName The name of the property to retrieve @param {Function?} [callback] A one-argument callback to receive the property value @returns {object?} The value of the property (if set and no callback is given) */ getProperty(propertyName, callback) { const properties = this._properties; // If no callback was passed, return the property value if (!callback) return properties && properties[propertyName]; // If the value has been set, send it through the callback if (properties && (propertyName in properties)) { taskScheduler(() => callback(properties[propertyName])); } // If the value was not set, store the callback for when the value will be set else { let propertyCallbacks; if (!(propertyCallbacks = this._propertyCallbacks)) this._propertyCallbacks = propertyCallbacks = Object.create(null); if (propertyName in propertyCallbacks) propertyCallbacks[propertyName].push(callback); else propertyCallbacks[propertyName] = [callback]; } return undefined; } /** Sets the property with the given name to the value. @param {string} propertyName The name of the property to set @param {object?} value The new value of the property */ setProperty(propertyName, value) { const properties = this._properties || (this._properties = Object.create(null)); properties[propertyName] = value; // Execute getter callbacks that were waiting for this property to be set const propertyCallbacks = this._propertyCallbacks || {}; const callbacks = propertyCallbacks[propertyName]; if (callbacks) { delete propertyCallbacks[propertyName]; taskScheduler(() => { for (const callback of callbacks) callback(value); }); // Remove _propertyCallbacks if no pending callbacks are left for (propertyName in propertyCallbacks) return; delete this._propertyCallbacks; } } /** Retrieves all properties of the iterator. @returns {object} An object with property names as keys. */ getProperties() { const properties = this._properties; const copy = {}; for (const name in properties) copy[name] = properties[name]; return copy; } /** Sets all of the given properties. @param {object} properties Key/value pairs of properties to set */ setProperties(properties) { for (const propertyName in properties) this.setProperty(propertyName, properties[propertyName]); } /** Copies the given properties from the source iterator. @param {module:asynciterator.AsyncIterator} source The iterator to copy from @param {Array} propertyNames List of property names to copy */ copyProperties(source, propertyNames) { for (const propertyName of propertyNames) { source.getProperty(propertyName, value => this.setProperty(propertyName, value)); } } /** Transforms items from this iterator. After this operation, only read the returned iterator instead of the current one. @param {object|Function} [options] Settings of the iterator, or the transformation function @param {integer} [options.maxbufferSize=4] The maximum number of items to keep in the buffer @param {boolean} [options.autoStart=true] Whether buffering starts directly after construction @param {integer} [options.offset] The number of items to skip @param {integer} [options.limit] The maximum number of items @param {Function} [options.filter] A function to synchronously filter items from the source @param {Function} [options.map] A function to synchronously transform items from the source @param {Function} [options.transform] A function to asynchronously transform items from the source @param {boolean} [options.optional=false] If transforming is optional, the original item is pushed when its mapping yields `null` or its transformation yields no items @param {Array|module:asynciterator.AsyncIterator} [options.prepend] Items to insert before the source items @param {Array|module:asynciterator.AsyncIterator} [options.append] Items to insert after the source items @returns {module:asynciterator.AsyncIterator} A new iterator that maps the items from this iterator */ transform(options) { return new SimpleTransformIterator(this, options); } /** Maps items from this iterator using the given function. After this operation, only read the returned iterator instead of the current one. @param {Function} map A mapping function to call on this iterator's (remaining) items @param {object?} self The `this` pointer for the mapping function @returns {module:asynciterator.AsyncIterator} A new iterator that maps the items from this iterator */ map(map, self) { return new MappingIterator(this, bind(map, self)); } filter(filter, self) { return this.map(function (item) { return filter.call(self || this, item) ? item : null; }); } /** * Returns a new iterator containing all of the unique items in the original iterator. * @param by - The derived value by which to determine uniqueness (e.g., stringification). Defaults to the identity function. * @returns An iterator with duplicates filtered out. */ uniq(by = identity) { const uniques = new Set(); return this.filter(function (item) { const hashed = by.call(this, item); if (!uniques.has(hashed)) { uniques.add(hashed); return true; } return false; }); } /** Prepends the items after those of the current iterator. After this operation, only read the returned iterator instead of the current one. @param {Array|module:asynciterator.AsyncIterator} items Items to insert before this iterator's (remaining) items @returns {module:asynciterator.AsyncIterator} A new iterator that prepends items to this iterator */ prepend(items) { return this.transform({ prepend: items }); } /** Appends the items after those of the current iterator. After this operation, only read the returned iterator instead of the current one. @param {Array|module:asynciterator.AsyncIterator} items Items to insert after this iterator's (remaining) items @returns {module:asynciterator.AsyncIterator} A new iterator that appends items to this iterator */ append(items) { return this.transform({ append: items }); } /** Surrounds items of the current iterator with the given items. After this operation, only read the returned iterator instead of the current one. @param {Array|module:asynciterator.AsyncIterator} prepend Items to insert before this iterator's (remaining) items @param {Array|module:asynciterator.AsyncIterator} append Items to insert after this iterator's (remaining) items @returns {module:asynciterator.AsyncIterator} A new iterator that appends and prepends items to this iterator */ surround(prepend, append) { return this.transform({ prepend, append }); } /** Skips the given number of items from the current iterator. The current iterator may not be read anymore until the returned iterator ends. @param {integer} offset The number of items to skip @returns {module:asynciterator.AsyncIterator} A new iterator that skips the given number of items */ skip(offset) { return this.map(item => offset-- > 0 ? null : item); } /** Limits the current iterator to the given number of items. The current iterator may not be read anymore until the returned iterator ends. @param {integer} limit The maximum number of items @returns {module:asynciterator.AsyncIterator} A new iterator with at most the given number of items */ take(limit) { return this.transform({ limit }); } /** Limits the current iterator to the given range. The current iterator may not be read anymore until the returned iterator ends. @param {integer} start Index of the first item to return @param {integer} end Index of the last item to return @returns {module:asynciterator.AsyncIterator} A new iterator with items in the given range */ range(start, end) { return this.transform({ offset: start, limit: Math.max(end - start + 1, 0) }); } /** Creates a copy of the current iterator, containing all items emitted from this point onward. Further copies can be created; they will all start from this same point. After this operation, only read the returned copies instead of the original iterator. @returns {module:asynciterator.AsyncIterator} A new iterator that contains all future items of this iterator */ clone() { return new ClonedIterator(this); } /** * An AsyncIterator is async iterable. * This allows iterators to be used via the for-await syntax. * * In cases where the returned EcmaScript AsyncIterator will not be fully consumed, * it is recommended to manually listen for error events on the main AsyncIterator * to avoid uncaught error messages. * * @returns {ESAsyncIterator<T>} An EcmaScript AsyncIterator */ [Symbol.asyncIterator]() { const it = this; let currentResolve = null; let currentReject = null; let pendingError = null; it.addListener('readable', tryResolve); it.addListener('end', tryResolve); it.addListener('error', tryReject); // Tries to emit an item or signal the end of the iterator function tryResolve() { if (currentResolve !== null) { if (pendingError !== null) { tryReject(pendingError); } else if (it.done) { currentResolve({ done: true, value: undefined }); currentResolve = currentReject = null; removeListeners(); } else { const value = it.read(); if (value !== null) { currentResolve({ done: false, value }); currentResolve = currentReject = null; } } } } // Tries to emit an error function tryReject(error) { if (currentReject !== null) { currentReject(error); currentResolve = currentReject = pendingError = null; removeListeners(); } else if (pendingError === null) { pendingError = error; } } // Cleans up all attached listeners function removeListeners() { it.removeListener('readable', tryResolve); it.removeListener('end', tryResolve); it.removeListener('error', tryReject); } // An EcmaScript AsyncIterator exposes the next() function that can be invoked repeatedly return { next() { return new Promise((resolve, reject) => { currentResolve = resolve; currentReject = reject; tryResolve(); }); }, }; } } exports.AsyncIterator = AsyncIterator; // Starts emitting `data` events when `data` listeners are added function waitForDataListener(eventName) { if (eventName === 'data') { this.removeListener('newListener', waitForDataListener); addSingleListener(this, 'readable', emitData); if (this.readable) taskScheduler(() => emitData.call(this)); } } // Emits new items though `data` events as long as there are `data` listeners function emitData() { // While there are `data` listeners and items, emit them let item; while (this.listenerCount('data') !== 0 && (item = this.read()) !== null) this.emit('data', item); // Stop draining the source if there are no more `data` listeners if (this.listenerCount('data') === 0 && !this.done) { this.removeListener('readable', emitData); addSingleListener(this, 'newListener', waitForDataListener); } } // Adds the listener to the event, if it has not been added previously. function addSingleListener(source, eventName, listener) { if (!source.listeners(eventName).includes(listener)) source.on(eventName, listener); } /** An iterator that doesn't emit any items. @extends module:asynciterator.AsyncIterator */ class EmptyIterator extends AsyncIterator { /** Creates a new `EmptyIterator`. */ constructor() { super(); this._changeState(exports.ENDED, true); } } exports.EmptyIterator = EmptyIterator; /** An iterator that emits a single item. @extends module:asynciterator.AsyncIterator */ class SingletonIterator extends AsyncIterator { /** Creates a new `SingletonIterator`. @param {object} item The item that will be emitted. */ constructor(item) { super(); this._item = item; if (item === null) this.close(); else this.readable = true; } /* Reads the item from the iterator. */ read() { const item = this._item; this._item = null; this.close(); return item; } /* Generates details for a textual representation of the iterator. */ _toStringDetails() { return this._item === null ? '' : `(${this._item})`; } } exports.SingletonIterator = SingletonIterator; /** An iterator that emits the items of a given array. @extends module:asynciterator.AsyncIterator */ class ArrayIterator extends AsyncIterator { /** Creates a new `ArrayIterator`. @param {Array} items The items that will be emitted. @param {boolean} [options.autoStart=true] Whether buffering starts directly after construction @param {boolean} [options.preserve=true] If false, the passed array can be safely modified */ constructor(items = [], { autoStart = true, preserve = true } = {}) { super(); const buffer = preserve || !Array.isArray(items) ? [...items] : items; this._index = 0; this._sourceStarted = autoStart !== false; this._truncateThreshold = preserve ? -1 : 64; if (this._sourceStarted && buffer.length === 0) this.close(); else this._buffer = buffer; this.readable = true; } /* Reads an item from the iterator. */ read() { if (!this._sourceStarted) this._sourceStarted = true; let item = null; if (this._buffer) { // Emit the current item if (this._index < this._buffer.length) item = this._buffer[this._index++]; // Close when all elements have been returned if (this._index === this._buffer.length) { delete this._buffer; this.close(); } // Do need keep old items around indefinitely else if (this._index === this._truncateThreshold) { this._buffer.splice(0, this._truncateThreshold); this._index = 0; } } return item; } /* Generates details for a textual representation of the iterator. */ _toStringDetails() { return `(${this._buffer ? this._buffer.length - this._index : 0})`; } /* Called by {@link module:asynciterator.AsyncIterator#destroy} */ _destroy(cause, callback) { delete this._buffer; callback(); } /** Consume all remaining items of the iterator into an array that will be returned asynchronously. @param {object} [options] Settings for array creation @param {integer} [options.limit] The maximum number of items to place in the array. */ toArray(options = {}) { if (!this._buffer) return Promise.resolve([]); // Determine start and end index const { length } = this._buffer; const start = this._index; const end = typeof options.limit !== 'number' ? length : start + options.limit; // Slice the items off the buffer const items = this._buffer.slice(start, end); this._index = end; // Close this iterator when we're past the end if (end >= length) this.close(); return Promise.resolve(items); } } exports.ArrayIterator = ArrayIterator; /** An iterator that enumerates integers in a certain range. @extends module:asynciterator.AsyncIterator */ class IntegerIterator extends AsyncIterator { /** Creates a new `IntegerIterator`. @param {object} [options] Settings of the iterator @param {integer} [options.start=0] The first number to emit @param {integer} [options.end=Infinity] The last number to emit @param {integer} [options.step=1] The increment between two numbers */ constructor({ start = 0, step = 1, end } = {}) { super(); // Determine the first number if (Number.isFinite(start)) start = Math.trunc(start); this._next = start; // Determine step size if (Number.isFinite(step)) step = Math.trunc(step); this._step = step; // Determine the last number const ascending = step >= 0; const direction = ascending ? Infinity : -Infinity; if (Number.isFinite(end)) end = Math.trunc(end); else if (end !== -direction) end = direction; this._last = end; // Start iteration if there is at least one item; close otherwise if (!Number.isFinite(start) || (ascending ? start > end : start < end)) this.close(); else this.readable = true; } /* Reads an item from the iterator. */ read() { if (this.closed) return null; const current = this._next, step = this._step, last = this._last, next = this._next += step; if (step >= 0 ? next > last : next < last) this.close(); return current; } /* Generates details for a textual representation of the iterator. */ _toStringDetails() { return `(${this._next}...${this._last})`; } } exports.IntegerIterator = IntegerIterator; /** Function that maps an element to itself. */ function identity(item) { return item; } exports.identity = identity; /** Key indicating the current consumer of a source. */ exports.DESTINATION = Symbol('destination'); /** An iterator that synchronously transforms every item from its source by applying a mapping function. @extends module:asynciterator.AsyncIterator */ class MappingIterator extends AsyncIterator { /** * Applies the given mapping to the source iterator. */ constructor(source, map = identity, options = {}) { super(); this._map = map; this._source = ensureSourceAvailable(source); this._destroySource = options.destroySource !== false; // Close if the source is already empty if (source.done) { this.close(); } // Otherwise, wire up the source for reading else { this._source[exports.DESTINATION] = this; this._source.on('end', destinationClose); this._source.on('error', destinationEmitError); this._source.on('readable', destinationSetReadable); this.readable = this._source.readable; } } /* Tries to read the next item from the iterator. */ read() { if (!this.done) { // Try to read an item that maps to a non-null value if (this._source.readable) { let item, mapped; while ((item = this._source.read()) !== null) { if ((mapped = this._map(item)) !== null) return mapped; } } this.readable = false; // Close this iterator if the source is empty if (this._source.done) this.close(); } return null; } /* Cleans up the source iterator and ends. */ _end(destroy) { this._source.removeListener('end', destinationClose); this._source.removeListener('error', destinationEmitError); this._source.removeListener('readable', destinationSetReadable); delete this._source[exports.DESTINATION]; if (this._destroySource) this._source.destroy(); super._end(destroy); } } exports.MappingIterator = MappingIterator; // Validates an AsyncIterator for use as a source within another AsyncIterator function ensureSourceAvailable(source, allowDestination = false) { if (!source || !isFunction(source.read) || !isFunction(source.on)) throw new TypeError(`Invalid source: ${source}`); if (!allowDestination && source[exports.DESTINATION]) throw new Error('The source already has a destination'); return source; } /** An iterator that maintains an internal buffer of items. This class serves as a base class for other iterators with a typically complex item generation process. @extends module:asynciterator.AsyncIterator */ class BufferedIterator extends AsyncIterator { /** Creates a new `BufferedIterator`. @param {object} [options] Settings of the iterator @param {integer} [options.maxBufferSize=4] The number of items to preload in the internal buffer @param {boolean} [options.autoStart=true] Whether buffering starts directly after construction */ constructor({ maxBufferSize = 4, autoStart = true } = {}) { super(exports.INIT); this._buffer = new linkedlist_1.LinkedList(); this._maxBufferSize = 4; this._reading = true; this._pushedCount = 0; this.maxBufferSize = maxBufferSize; taskScheduler(() => this._init(autoStart)); this._sourceStarted = autoStart !== false; } /** The maximum number of items to preload in the internal buffer. A `BufferedIterator` tries to fill its buffer as far as possible. Set to `Infinity` to fully drain the source. @type number */ get maxBufferSize() { return this._maxBufferSize; } set maxBufferSize(maxBufferSize) { // Allow only positive integers and infinity if (maxBufferSize !== Infinity) { maxBufferSize = !Number.isFinite(maxBufferSize) ? 4 : Math.max(Math.trunc(maxBufferSize), 1); } // Only set the maximum buffer size if it changes if (this._maxBufferSize !== maxBufferSize) { this._maxBufferSize = maxBufferSize; // Ensure sufficient elements are buffered if (this._state === exports.OPEN) this._fillBuffer(); } } /** Initializing the iterator by calling {@link BufferedIterator#_begin} and changing state from INIT to OPEN. @protected @param {boolean} autoStart Whether reading of items should immediately start after OPEN. */ _init(autoStart) { // Perform initialization tasks let doneCalled = false; this._reading = true; this._begin(() => { if (doneCalled) throw new Error('done callback called multiple times'); doneCalled = true; // Open the iterator and start buffering this._reading = false; this._changeState(exports.OPEN); if (autoStart) this._fillBufferAsync(); // If reading should not start automatically, the iterator doesn't become readable. // Therefore, mark the iterator as (potentially) readable so consumers know it might be read. else this.readable = true; }); } /** Writes beginning items and opens iterator resources. Should never be called before {@link BufferedIterator#_init}; typically, `_init` is responsible for calling `_begin`. @protected @param {function} done To be called when initialization is complete */ _begin(done) { done(); } /** Tries to read the next item from the iterator. If the buffer is empty, this method calls {@link BufferedIterator#_read} to fetch items. @returns {object?} The next item, or `null` if none is available */ read() { if (this.done) return null; // An explicit read kickstarts the source if (!this._sourceStarted) this._sourceStarted = true; // Try to retrieve an item from the buffer const buffer = this._buffer; let item; if (buffer.empty) { item = null; this.readable = false; } else { item = buffer.shift(); } // If the buffer is becoming empty, either fill it or end the iterator if (!this._reading && buffer.length < this._maxBufferSize) { // If the iterator is not closed and thus may still generate new items, fill the buffer if (!this.closed) this._fillBufferAsync(); // No new items will be generated, so if none are buffered, the iterator ends here else if (buffer.empty) this._endAsync(); } return item; } /** Tries to generate the given number of items. Implementers should add `count` items through {@link BufferedIterator#_push}. @protected @param {integer} count The number of items to generate @param {function} done To be called when reading is complete */ _read(count, done) { done(); } /** Adds an item to the internal buffer. @protected @param {object} item The item to add @emits module:asynciterator.AsyncIterator.readable */ _push(item) { if (!this.done) { this._pushedCount++; this._buffer.push(item); this.readable = true; } } /** Fills the internal buffer until `this._maxBufferSize` items are present. This method calls {@link BufferedIterator#_read} to fetch items. @protected @emits module:asynciterator.AsyncIterator.readable */ _fillBuffer() { let neededItems; // Avoid recursive reads if (this._reading) { // Do nothing } // If iterator closing started in the meantime, don't generate new items anymore else if (this.closed) { this._completeClose(); } // Otherwise, try to fill empty spaces in the buffer by generating new items else if ((neededItems = Math.min(this._maxBufferSize - this._buffer.length, 128)) > 0) { // Acquire reading lock and start reading, counting pushed items this._pushedCount = 0; this._reading = true; this._read(neededItems, () => { // Verify the callback is only called once if (!neededItems) throw new Error('done callback called multiple times'); neededItems = 0; // Release reading lock this._reading = false; // If the iterator was closed while reading, complete closing if (this.closed) { this._completeClose(); } // If the iterator pushed one or more items, // it might currently be able to generate additional items // (even though all pushed items might already have been read) else if (this._pushedCount) { this.readable = true; // If the buffer is insufficiently full, continue filling if (this._buffer.length < this._maxBufferSize / 2) this._fillBufferAsync(); } }); } } /** Schedules `_fillBuffer` asynchronously. */ _fillBufferAsync() { // Acquire reading lock to avoid recursive reads if (!this._reading) { this._reading = true; taskScheduler(() => { // Release reading lock so _fillBuffer` can take it this._reading = false; this._fillBuffer(); }); } } /** Stops the iterator from generating new items after a possible pending read operation has finished. Already generated, pending, or terminating items can still be emitted. After this, the iterator will end asynchronously. @emits module:asynciterator.AsyncIterator.end */ close() { // If the iterator is not currently reading, we can close immediately if (!this._reading) this._completeClose(); // Closing cannot complete when reading, so temporarily assume CLOSING state // `_fillBuffer` becomes responsible for calling `_completeClose` else this._changeState(exports.CLOSING); } /** Stops the iterator from generating new items, switching from `CLOSING` state into `CLOSED` state. @protected @emits module:asynciterator.AsyncIterator.end */ _completeClose() { if (this._changeState(exports.CLOSED)) { // Write possible terminating items this._reading = true; this._flush(() => { if (!this._reading) throw new Error('done callback called multiple times'); this._reading = false; // If no items are left, end the iterator // Otherwise, `read` becomes responsible for ending the iterator if (this._buffer.empty) this._endAsync(); }); } } /* Called by {@link module:asynciterator.AsyncIterator#destroy} */ _destroy(cause, callback) { this._buffer.clear(); callback(); } /** Writes terminating items and closes iterator resources. Should never be called before {@link BufferedIterator#close}; typically, `close` is responsible for calling `_flush`. @protected @param {function} done To be called when termination is complete */ _flush(done) { done(); } /** Generates details for a textual representation of the iterator. @protected */ _toStringDetails() { const buffer = this._buffer; return `{${buffer.empty ? '' : `next: ${buffer.first}, `}buffer: ${buffer.length}}`; } } exports.BufferedIterator = BufferedIterator; /** An iterator that generates items based on a source iterator. This class serves as a base class for other iterators. @extends module:asynciterator.BufferedIterator */ class TransformIterator extends BufferedIterator { /** Creates a new `TransformIterator`. @param {module:asynciterator.AsyncIterator|Readable} [source] The source this iterator generates items from @param {object} [options] Settings of the iterator @param {integer} [options.maxBufferSize=4] The maximum number of items to keep in the buffer @param {boolean} [options.autoStart=true] Whether buffering starts directly after construction @param {boolean} [options.optional=false] If transforming is optional, the original item is pushed when its transformation yields no items @param {boolean} [options.destroySource=true] Whether the source should be destroyed when this transformed iterator is closed or destroyed @param {module:asynciterator.AsyncIterator} [options.source] The source this iterator generates items from */ constructor(source, options = source || {}) { super(options); this._boundPush = (item) => this._push(item); // Shift parameters if needed if (!isSourceExpression(source)) source = options.source; // The passed source is an AsyncIterator or readable stream if (isEventEmitter(source)) { this.source = source; } // The passed value is a promise or source creation function else if (source) { this._createSource = isPromise(source) ? () => source : source; if (this._sourceStarted) this._loadSourceAsync(); } // Set other options this._optional = Boolean(options.optional); this._destroySource = options.destroySource !== false; } /** The source this iterator generates items from. @type module:asynciterator.AsyncIterator */ get source() { if (isFunction(this._createSource)) this._loadSourceAsync(); return this._source; } set source(value) { // Validate and set source const source = this._source = this._validateSource(value); source[exports.DESTINATION] = this; // Do not read the source if this iterator already ended if (this.done) { if (this._destroySource) source.destroy(); } // Close this iterator if the source already ended else if (source.done) { this.close(); } // Otherwise, react to source events else { source.on('end', destinationCloseWhenDone); source.on('readable', destinationFillBuffer); source.on('error', destinationEmitError); } } /** Initializes a source that was set through a promise @protected */ _loadSourceAsync() { if (isFunction(this._createSource)) { // Assign the source after resolving Promise.resolve(this._createSource()).then(source => { delete this._createSource; this.source = source; this._fillBuffer(); }, error => this.emit('error', error)); // Signal that source creation is pending this._createSource = null; } } /** Validates whether the given iterator can be used as a source. @protected @param {object} source The source to validate @param {boolean} allowDestination Whether the source can already have a destination */ _validateSource(source, allowDestination = false) { if (this._source || typeof this._createSource !== 'undefined') throw new Error('The source cannot be changed after it has been set'); return ensureSourceAvailable(source, allowDestination); } /** Tries to read transformed items. */ _read(count, done) { const next = () => { // Continue transforming until at least `count` items have been pushed if (this._pushedCount < count && !this.closed) taskScheduler(() => this._readAndTransform(next, done)); else done(); }; this._readAndTransform(next, done); } /** Reads a transforms an item */ _readAndTransform(next, done) { // If the source exists and still can read items, // try to read and transform the next item. let item; const source = this.source; if (!source || source.done || (item = source.read()) === null) done(); else if (!this._optional) this._transform(item, next, this._boundPush); else this._optionalTransform(item, next); } /** Tries to transform the item; if the transformation yields no items, pushes the original item. */ _optionalTransform(item, done) { const pushedCount = this._pushedCount; this._transform(item, () => { if (pushedCount === this._pushedCount) this._push(item); done(); }, this._boundPush); } /** Generates items based on the item from the source. Implementers should add items through {@link BufferedIterator#_push}. The default implementation pushes the source item as-is. @protected @param {object} item The last read item from the source @param {function} done To be called when reading is complete @param {function} push A callback to push zero or more transformation results. */ _transform(item, done, push) { push(item); done(); } /** Closes the iterator when pending items are transformed. @protected */ _closeWhenDone() { this.close(); } /* Cleans up the source iterator and ends. */ _end(destroy) { const source = this._source; if (source) { source.removeListener('end', destinationCloseWhenDone); source.removeListener('error', destinationEmitError); source.removeListener('readable', destinationFillBuffer); delete source[exports.DESTINATION]; if (this._destroySource) source.dest