stack-base-iterator
Version:
Base iterator for values retrieved using a stack of async functions returning values
186 lines (185 loc) • 7.43 kB
JavaScript
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 };