UNPKG

stack-base-iterator

Version:

Base iterator for values retrieved using a stack of async functions returning values

186 lines (185 loc) 7.43 kB
import compat from 'async-compat'; import nextCallback from 'iterator-next-callback'; import { createProcessor } from 'maximize-iterator'; import Pinkie from 'pinkie-promise'; import LinkedList from './LinkedList.js'; const root = typeof window === 'undefined' ? global : window; // biome-ignore lint/suspicious/noShadowRestrictedNames: Legacy const Symbol = typeof root.Symbol === 'undefined' ? { asyncIterator: undefined } : root.Symbol; export { default as LinkedList } from './LinkedList.js'; let StackBaseIterator = class StackBaseIterator { isDone() { return this.done; } push(fn, ...rest) { if (this.done) return console.log('Attempting to push on a done iterator'); this.stack.push(fn); !rest.length || rest.forEach((x)=>{ this.stack.push(x); }); this._pump(); } next() { return new Pinkie((resolve, reject)=>{ this._processOrQueue((err, result)=>{ err ? reject(err) : resolve(result); }); }); } [Symbol.asyncIterator]() { return this; } forEach(fn, options, callback) { if (typeof fn !== 'function') throw new Error('Missing each function'); if (typeof options === 'function') { callback = options; options = {}; } if (typeof callback === 'function') { if (this.done) return callback(null, true); options = options || {}; const processorOptions = { each: fn, callbacks: options.callbacks || false, concurrency: options.concurrency || 1, limit: options.limit || Infinity, error: options.error || function defaultError() { return true; // default is exit on error }, total: 0, counter: 0, canProcess: ()=>{ return !this.done && this.stack.length > 0 && this.queued.length < this.stack.length; } }; let callbackFired = false; let processor = createProcessor(nextCallback(this), processorOptions, (err)=>{ // Guard against double callback (can happen if end() is called while microtask is pending) if (callbackFired) return; callbackFired = true; // Defer completion decision AND processor removal to give deferred work a chance to push // Processor must stay in list so _pump() can signal it to process new items setTimeout(()=>{ if (!this.destroyed) this.processors.remove(processor); processor = null; options = null; const done = !this.stack.length && this.pending === 0; if ((err || done) && !this.done) this.end(err); callback(err, this.done || done); }, 0); }); this.processors.push(processor); this._pump(); return; } return new Promise((resolve, reject)=>this.forEach(fn, options, (err, done)=>{ err ? reject(err) : resolve(done); })); } end(err) { if (this.done) return; this.done = true; while(this.processors.length > 0)this.processors.pop()(err || true); while(this.processing.length > 0)err ? this.processing.pop()(err) : this.processing.pop()(null, { done: true, value: null }); while(this.queued.length > 0)err ? this.queued.pop()(err) : this.queued.pop()(null, { done: true, value: null }); while(this.stack.length > 0)this.stack.pop(); } destroy(err) { if (this.destroyed) throw new Error('Already destroyed'); this.destroyed = true; this.end(err); } _pump() { // Flush loop pattern: if already flushing, the outer loop will handle new work // This prevents stack overflow by avoiding recursion entirely if (this.flushing) return; this.flushing = true; if (!this.done && this.processors.length > 0 && this.stack.length > 0 && this.stack.length > this.queued.length) this.processors.last()(false); // try to queue more while(this.stack.length > 0 && this.queued.length > 0){ this._processOrQueue(this.queued.pop()); if (!this.done && this.processors.length > 0 && this.stack.length > 0 && this.stack.length > this.queued.length) this.processors.last()(false); // try to queue more } this.flushing = false; } _scheduleEndCheck() { // Defer end check to give other deferred work a chance to push if (this.endScheduled || this.done) return; this.endScheduled = true; setTimeout(()=>{ this.endScheduled = false; // Re-check ALL conditions after deferral if (this.stack.length === 0 && this.processing.length === 0 && this.pending === 0 && !this.done) { this.end(); } }, 0); } _processOrQueue(callback) { if (this.done) return callback(null, { done: true, value: null }); // nothing to process so queue if (this.stack.length === 0) { this.queued.push(callback); return; } // process next const next = this.stack.pop(); this.processing.push(callback); this.pending++; let callbackFired = false; next(this, (err, result)=>{ // Guard against callback being called multiple times (buggy iterators) if (callbackFired) { console.warn('stack-base-iterator: callback called multiple times - this indicates a bug in the iterator implementation'); return; } callbackFired = true; this.pending--; this.processing.remove(callback); // done if (this.done) return callback(null, { done: true, value: null }); // skip error if (err && compat.defaultValue(this.options.error(err), true)) err = null; // handle callback if (err) callback(err); else if (!result) { this.queued.push(callback); setTimeout(()=>this._pump(), 0); // Deferred to start new call stack } else callback(null, result); // Only schedule end check when we might actually be done // This prevents premature end checks from earlier items if (this.stack.length === 0 && this.processing.length === 0 && this.pending === 0) { this._scheduleEndCheck(); } }); } constructor(options = {}){ this.options = { ...options }; this.options.error = options.error || function defaultError(err) { return !!err; // fail on errors }; this.done = false; this.stack = []; this.queued = []; this.processors = new LinkedList(); this.processing = new LinkedList(); this.flushing = false; this.pending = 0; this.endScheduled = false; } }; export { StackBaseIterator as default };