-
Notifications
You must be signed in to change notification settings - Fork 74
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
fix(asynciterable): use more yield
#379
base: master
Are you sure you want to change the base?
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
Wow that TS issue is concerning. Is the addition of the |
The bug just means that we can't use I'm not fully certain about what the best play with returning iterators is, but (thinking out loud here), if you look at for example try {
while (1) {
const { type, value } = await safeRace<TimeoutOperation<TSource>>([
it.next().then((val) => {
return { type: VALUE_TYPE, value: val };
}),
sleep(this._dueTime, signal).then(() => {
return { type: ERROR_TYPE };
}),
]);
if (type === ERROR_TYPE) {
throw new TimeoutError();
}
if (!value || value.done) {
break;
}
yield value.value;
}
} finally {
await it?.return?.();
} What we want in test('AsyncIterable#timeout with never', async () => {
const xs = never().pipe(timeout(1000));
const it = xs[Symbol.asyncIterator]();
await noNext(it);
}); The following snippet hopefully demonstrates why we can't await (async () => {
setTimeout(() => { }, 10e3); // Keep event loop running
async function* a() {
await new Promise(() => { });
}
const it = a();
void it.next();
await it.return();
console.log("Done")
})() If you return a generator that's currently running (the unresolved To be fully correct when we race iterators, we should actually abort whichever don't finish the race (if you instead return them, you get the above scenario), and only the one that yielded first should eventually return. What such an implementation, we would not need #378 at all, since The following test currently passes on master, even though it technically should not (with the current test('canceled', async () => {
let canceled = false;
async function* generate() {
try {
for (let i = 0; ; i++) {
await delay(100);
yield i;
}
} finally {
canceled = true;
}
}
const it = batch()(generate())[Symbol.asyncIterator]();
await delay(150);
expect(await it.next()).toEqual({ done: false, value: [0] });
expect(await it.return!()).toEqual({ done: true });
expect(canceled).toBe(true);
}); The test only succeeds because when you test('canceled', async () => {
let canceled = false;
async function* generate() {
try {
for (let i = 0; ; i++) {
await delay(10000);
yield i;
}
} finally {
canceled = true;
}
}
const it = batch()(generate())[Symbol.asyncIterator]();
expect(await it.return!()).toEqual({ done: true });
expect(canceled).toBe(true);
}); This test would pass if and only if we instead aborted the delay when the buffered iterator is returned. For that reason, I have removed tests like these that rely on In conclusion, I think, there is no scenario in which we can blindly |
I went through the async-iterable functions and swapped out custom while loops with
yield
where I could. Also took the liberty to clean up some of the code while at it. The benefit ofyield
over custom code is that it takes care of returning the wrapped iterator, etc.As a consequence of microsoft/TypeScript#61022, the following pattern fails for operators (
concat
) implemented usingyield*
on targets that use <= ES2017 (passes for higher targets):So even though
yield*
should be able to be used in most places, because of this, I went with loops andyield
.