432 lines
12 KiB
JavaScript
432 lines
12 KiB
JavaScript
export const SKIP = {};
|
|
const DONE = {
|
|
value: null,
|
|
done: true,
|
|
};
|
|
const RETURN_DONE = {
|
|
// we allow this one to be mutated
|
|
value: null,
|
|
done: true,
|
|
};
|
|
if (!Symbol.asyncIterator) {
|
|
Symbol.asyncIterator = Symbol.for('Symbol.asyncIterator');
|
|
}
|
|
const NO_OPTIONS = {};
|
|
|
|
export class RangeIterable {
|
|
constructor(sourceArray) {
|
|
if (sourceArray) {
|
|
this.iterate = sourceArray[Symbol.iterator].bind(sourceArray);
|
|
}
|
|
}
|
|
map(func) {
|
|
let source = this;
|
|
let iterable = new RangeIterable();
|
|
iterable.iterate = (options = NO_OPTIONS) => {
|
|
const { async } = options;
|
|
let iterator =
|
|
source[async ? Symbol.asyncIterator : Symbol.iterator](options);
|
|
if (!async) source.isSync = true;
|
|
let i = -1;
|
|
return {
|
|
next(resolvedResult) {
|
|
let result;
|
|
do {
|
|
let iteratorResult;
|
|
try {
|
|
if (resolvedResult) {
|
|
iteratorResult = resolvedResult;
|
|
resolvedResult = null; // don't go in this branch on next iteration
|
|
} else {
|
|
i++;
|
|
iteratorResult = iterator.next();
|
|
if (iteratorResult.then) {
|
|
if (!async) {
|
|
this.throw(
|
|
new Error(
|
|
'Can not synchronously iterate with promises as iterator results',
|
|
),
|
|
);
|
|
}
|
|
return iteratorResult.then(
|
|
(iteratorResult) => this.next(iteratorResult),
|
|
(error) => {
|
|
return this.throw(error);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
if (iteratorResult.done === true) {
|
|
this.done = true;
|
|
if (iterable.onDone) iterable.onDone();
|
|
return iteratorResult;
|
|
}
|
|
try {
|
|
result = func.call(source, iteratorResult.value, i);
|
|
if (result && result.then && async) {
|
|
// if async, wait for promise to resolve before returning iterator result
|
|
return result.then(
|
|
(result) =>
|
|
result === SKIP
|
|
? this.next()
|
|
: {
|
|
value: result,
|
|
},
|
|
(error) => {
|
|
if (options.continueOnRecoverableError)
|
|
error.continueIteration = true;
|
|
return this.throw(error);
|
|
},
|
|
);
|
|
}
|
|
} catch (error) {
|
|
// if the error came from the user function, we can potentially mark it for continuing iteration
|
|
if (options.continueOnRecoverableError)
|
|
error.continueIteration = true;
|
|
throw error; // throw to next catch to handle
|
|
}
|
|
} catch (error) {
|
|
if (iterable.handleError) {
|
|
// if we have handleError, we can use it to further handle errors
|
|
try {
|
|
result = iterable.handleError(error, i);
|
|
} catch (error2) {
|
|
return this.throw(error2);
|
|
}
|
|
} else return this.throw(error);
|
|
}
|
|
} while (result === SKIP);
|
|
if (result === DONE) {
|
|
return this.return();
|
|
}
|
|
return {
|
|
value: result,
|
|
};
|
|
},
|
|
return(value) {
|
|
if (!this.done) {
|
|
RETURN_DONE.value = value;
|
|
this.done = true;
|
|
if (iterable.onDone) iterable.onDone();
|
|
iterator.return?.();
|
|
}
|
|
return RETURN_DONE;
|
|
},
|
|
throw(error) {
|
|
if (error.continueIteration) {
|
|
// if it's a recoverable error, we can return or throw without closing the iterator
|
|
if (iterable.returnRecoverableErrors)
|
|
try {
|
|
return {
|
|
value: iterable.returnRecoverableErrors(error),
|
|
};
|
|
} catch (error) {
|
|
// if this throws, we need to go back to closing the iterator
|
|
this.return();
|
|
throw error;
|
|
}
|
|
if (options.continueOnRecoverableError) throw error; // throw without closing iterator
|
|
}
|
|
// else we are done with the iterator (and can throw)
|
|
this.return();
|
|
throw error;
|
|
},
|
|
};
|
|
};
|
|
return iterable;
|
|
}
|
|
[Symbol.asyncIterator](options) {
|
|
if (options) options = { ...options, async: true };
|
|
else options = { async: true };
|
|
return (this.iterator = this.iterate(options));
|
|
}
|
|
[Symbol.iterator](options) {
|
|
return (this.iterator = this.iterate(options));
|
|
}
|
|
filter(func) {
|
|
let iterable = this.map((element) => {
|
|
let result = func(element);
|
|
// handle promise
|
|
if (result?.then)
|
|
return result.then((result) => (result ? element : SKIP));
|
|
else return result ? element : SKIP;
|
|
});
|
|
let iterate = iterable.iterate;
|
|
iterable.iterate = (options = NO_OPTIONS) => {
|
|
// explicitly prevent continue on recoverable error with filter
|
|
if (options.continueOnRecoverableError)
|
|
options = { ...options, continueOnRecoverableError: false };
|
|
return iterate(options);
|
|
};
|
|
return iterable;
|
|
}
|
|
|
|
forEach(callback) {
|
|
let iterator = (this.iterator = this.iterate());
|
|
let result;
|
|
while ((result = iterator.next()).done !== true) {
|
|
callback(result.value);
|
|
}
|
|
}
|
|
concat(secondIterable) {
|
|
let concatIterable = new RangeIterable();
|
|
concatIterable.iterate = (options = NO_OPTIONS) => {
|
|
let iterator = (this.iterator = this.iterate(options));
|
|
let isFirst = true;
|
|
function iteratorDone(result) {
|
|
if (isFirst) {
|
|
try {
|
|
isFirst = false;
|
|
iterator =
|
|
secondIterable[
|
|
options.async ? Symbol.asyncIterator : Symbol.iterator
|
|
]();
|
|
result = iterator.next();
|
|
if (concatIterable.onDone) {
|
|
if (result.then) {
|
|
if (!options.async)
|
|
throw new Error(
|
|
'Can not synchronously iterate with promises as iterator results',
|
|
);
|
|
result.then(
|
|
(result) => {
|
|
if (result.done()) concatIterable.onDone();
|
|
},
|
|
(error) => {
|
|
this.return();
|
|
throw error;
|
|
},
|
|
);
|
|
} else if (result.done) concatIterable.onDone();
|
|
}
|
|
} catch (error) {
|
|
this.throw(error);
|
|
}
|
|
} else {
|
|
if (concatIterable.onDone) concatIterable.onDone();
|
|
}
|
|
return result;
|
|
}
|
|
return {
|
|
next() {
|
|
try {
|
|
let result = iterator.next();
|
|
if (result.then) {
|
|
if (!options.async)
|
|
throw new Error(
|
|
'Can not synchronously iterate with promises as iterator results',
|
|
);
|
|
return result.then((result) => {
|
|
if (result.done) return iteratorDone(result);
|
|
return result;
|
|
});
|
|
}
|
|
if (result.done) return iteratorDone(result);
|
|
return result;
|
|
} catch (error) {
|
|
this.throw(error);
|
|
}
|
|
},
|
|
return(value) {
|
|
if (!this.done) {
|
|
RETURN_DONE.value = value;
|
|
this.done = true;
|
|
if (concatIterable.onDone) concatIterable.onDone();
|
|
iterator.return();
|
|
}
|
|
return RETURN_DONE;
|
|
},
|
|
throw(error) {
|
|
if (options.continueOnRecoverableError) throw error;
|
|
this.return();
|
|
throw error;
|
|
},
|
|
};
|
|
};
|
|
return concatIterable;
|
|
}
|
|
|
|
flatMap(callback) {
|
|
let mappedIterable = new RangeIterable();
|
|
mappedIterable.iterate = (options = NO_OPTIONS) => {
|
|
let iterator = (this.iterator = this.iterate(options));
|
|
let isFirst = true;
|
|
let currentSubIterator;
|
|
return {
|
|
next(resolvedResult) {
|
|
try {
|
|
do {
|
|
if (currentSubIterator) {
|
|
let result;
|
|
if (resolvedResult) {
|
|
result = resolvedResult;
|
|
resolvedResult = undefined;
|
|
} else result = currentSubIterator.next();
|
|
if (result.then) {
|
|
if (!options.async)
|
|
throw new Error(
|
|
'Can not synchronously iterate with promises as iterator results',
|
|
);
|
|
return result.then((result) => this.next(result));
|
|
}
|
|
if (!result.done) {
|
|
return result;
|
|
}
|
|
}
|
|
let result;
|
|
if (resolvedResult != undefined) {
|
|
result = resolvedResult;
|
|
resolvedResult = undefined;
|
|
} else result = iterator.next();
|
|
if (result.then) {
|
|
if (!options.async)
|
|
throw new Error(
|
|
'Can not synchronously iterate with promises as iterator results',
|
|
);
|
|
currentSubIterator = undefined;
|
|
return result.then((result) => this.next(result));
|
|
}
|
|
if (result.done) {
|
|
if (mappedIterable.onDone) mappedIterable.onDone();
|
|
return result;
|
|
}
|
|
try {
|
|
let value = callback(result.value);
|
|
if (value?.then) {
|
|
if (!options.async)
|
|
throw new Error(
|
|
'Can not synchronously iterate with promises as iterator results',
|
|
);
|
|
return value.then(
|
|
(value) => {
|
|
if (
|
|
Array.isArray(value) ||
|
|
value instanceof RangeIterable
|
|
) {
|
|
currentSubIterator = value[Symbol.iterator]();
|
|
return this.next();
|
|
} else {
|
|
currentSubIterator = null;
|
|
return { value };
|
|
}
|
|
},
|
|
(error) => {
|
|
if (options.continueOnRecoverableError)
|
|
error.continueIteration = true;
|
|
this.throw(error);
|
|
},
|
|
);
|
|
}
|
|
if (Array.isArray(value) || value instanceof RangeIterable)
|
|
currentSubIterator = value[Symbol.iterator]();
|
|
else {
|
|
currentSubIterator = null;
|
|
return { value };
|
|
}
|
|
} catch (error) {
|
|
if (options.continueOnRecoverableError)
|
|
error.continueIteration = true;
|
|
throw error;
|
|
}
|
|
} while (true);
|
|
} catch (error) {
|
|
this.throw(error);
|
|
}
|
|
},
|
|
return() {
|
|
if (mappedIterable.onDone) mappedIterable.onDone();
|
|
if (currentSubIterator) currentSubIterator.return();
|
|
return iterator.return();
|
|
},
|
|
throw(error) {
|
|
if (options.continueOnRecoverableError) throw error;
|
|
if (mappedIterable.onDone) mappedIterable.onDone();
|
|
if (currentSubIterator) currentSubIterator.return();
|
|
this.return();
|
|
throw error;
|
|
},
|
|
};
|
|
};
|
|
return mappedIterable;
|
|
}
|
|
|
|
slice(start, end) {
|
|
let iterable = this.map((element, i) => {
|
|
if (i < start) return SKIP;
|
|
if (i >= end) {
|
|
DONE.value = element;
|
|
return DONE;
|
|
}
|
|
return element;
|
|
});
|
|
iterable.handleError = (error, i) => {
|
|
if (i < start) return SKIP;
|
|
if (i >= end) {
|
|
return DONE;
|
|
}
|
|
throw error;
|
|
};
|
|
return iterable;
|
|
}
|
|
mapError(catch_callback) {
|
|
let iterable = this.map((element) => {
|
|
return element;
|
|
});
|
|
let iterate = iterable.iterate;
|
|
iterable.iterate = (options = NO_OPTIONS) => {
|
|
// we need to ensure the whole stack
|
|
// of iterables is set up to handle recoverable errors and continue iteration
|
|
return iterate({ ...options, continueOnRecoverableError: true });
|
|
};
|
|
iterable.returnRecoverableErrors = catch_callback;
|
|
return iterable;
|
|
}
|
|
next() {
|
|
if (!this.iterator) this.iterator = this.iterate();
|
|
return this.iterator.next();
|
|
}
|
|
toJSON() {
|
|
if (this.asArray && this.asArray.forEach) {
|
|
return this.asArray;
|
|
}
|
|
const error = new Error(
|
|
'Can not serialize async iterables without first calling resolving asArray',
|
|
);
|
|
error.resolution = this.asArray;
|
|
throw error;
|
|
//return Array.from(this)
|
|
}
|
|
get asArray() {
|
|
if (this._asArray) return this._asArray;
|
|
let promise = new Promise((resolve, reject) => {
|
|
let iterator = this.iterate(true);
|
|
let array = [];
|
|
let iterable = this;
|
|
Object.defineProperty(array, 'iterable', { value: iterable });
|
|
function next(result) {
|
|
while (result.done !== true) {
|
|
if (result.then) {
|
|
return result.then(next);
|
|
} else {
|
|
array.push(result.value);
|
|
}
|
|
result = iterator.next();
|
|
}
|
|
resolve((iterable._asArray = array));
|
|
}
|
|
next(iterator.next());
|
|
});
|
|
promise.iterable = this;
|
|
return this._asArray || (this._asArray = promise);
|
|
}
|
|
resolveData() {
|
|
return this.asArray;
|
|
}
|
|
at(index) {
|
|
for (let entry of this) {
|
|
if (index-- === 0) return entry;
|
|
}
|
|
}
|
|
}
|
|
RangeIterable.prototype.DONE = DONE;
|