You can't clone a generator--it's just a function with no state. What could have state, and therefore what could be cloned, is the iterator resulting from invoking the generator function.
This approach caches intermediate results, so that cloned iterators can access them if necessary until they "catch up". It returns an object which is both an iterator and an iterable, so you can either call next
on it or for...of
over it. Any iterator may be passed in, so you could in theory have cloned iterators over an array by passing in array.values()
. Whichever clone calls next
first at a given point in the iteration will have the argument passed to next
, if any, reflected in the value of the yield
in the underlying generator.
function clonableIterator(it) {
var vals = [];
return function make(n) {
return {
next(arg) {
const len = vals.length;
if (n >= len) vals[len] = it.next(arg);
return vals[n++];
},
clone() { return make(n); },
throw(e) { if (it.throw) it.throw(e); },
return(v) { if (it.return) it.return(v); },
[Symbol.iterator]() { return this; }
};
}(0);
}
function *gen() {
yield 1;
yield 2;
yield 3;
}
var it = clonableIterator(gen());
console.log(it.next());
var clone = it.clone();
console.log(clone.next());
console.log(it.next());
Obviously this approach has the problem that it keeps the entire history of the iterator. One optimization would be to keep a WeakMap
of all the cloned iterators and how far they have progressed, and then clean up the history to eliminate all the past values that have already been consumed by all clones.