-
Notifications
You must be signed in to change notification settings - Fork 331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: Gracefully shutdown the embedder #9693
Changes from all commits
489d7fe
099e61f
c7a75b7
c860c91
fc8b5cf
2e15053
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = { | ||
'*.{ts,tsx}': ['eslint --fix', 'prettier --config ../../.prettierrc --ignore-path ./.eslintignore --write'], | ||
'*.graphql': ['prettier --config ../../.prettierrc --ignore-path ./.eslintignore --write'], | ||
'**/*.{ts,tsx}': () => 'tsc --noEmit -p tsconfig.json' | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,85 +12,80 @@ type Result<T extends AsyncIterator<any>> = UnYield<Awaited<ReturnType<T['next'] | |
|
||
// Promise.race has a memory leak | ||
// To avoid: https://github.com/tc39/proposal-async-iterator-helpers/issues/15#issuecomment-1937011820 | ||
export function mergeAsyncIterators<T extends AsyncIterator<any>[] | []>( | ||
iterators: T | ||
): AsyncIterableIterator<{[P in keyof T]: [ParseInt<`${P}`>, Result<T[P]>]}[number]> { | ||
return (async function* () { | ||
type ResultThunk = () => [number, Result<T[number]>] | ||
let count = iterators.length as number | ||
let capability: PromiseCapability<ResultThunk | null> | undefined | ||
const queuedResults: ResultThunk[] = [] | ||
const getNext = async (idx: number, iterator: T[number]) => { | ||
try { | ||
const next = await iterator.next() | ||
if (next.done) { | ||
if (--count === 0 && capability !== undefined) { | ||
capability.resolve(null) | ||
} | ||
} else { | ||
resolveResult(() => { | ||
void getNext(idx, iterator) | ||
return [idx, next.value] | ||
}) | ||
export function mergeAsyncIterators<T extends AsyncIterator<any>[] | []>(iterators: T) { | ||
type ResultThunk = () => [number, Result<T[number]>] | ||
let count = iterators.length as number | ||
let capability: PromiseCapability<ResultThunk | null> | undefined | ||
const queuedResults: ResultThunk[] = [] | ||
const getNext = async (idx: number, iterator: T[number]) => { | ||
try { | ||
const next = await iterator.next() | ||
if (next.done) { | ||
if (--count === 0 && capability !== undefined) { | ||
capability.resolve(null) | ||
} | ||
} catch (error) { | ||
} else { | ||
resolveResult(() => { | ||
throw error | ||
void getNext(idx, iterator) | ||
return [idx, next.value] | ||
}) | ||
} | ||
} catch (error) { | ||
resolveResult(() => { | ||
throw error | ||
}) | ||
} | ||
const resolveResult = (resultThunk: ResultThunk) => { | ||
if (capability === undefined) { | ||
queuedResults.push(resultThunk) | ||
} else { | ||
capability.resolve(resultThunk) | ||
} | ||
} | ||
const resolveResult = (resultThunk: ResultThunk) => { | ||
if (capability === undefined) { | ||
queuedResults.push(resultThunk) | ||
} else { | ||
capability.resolve(resultThunk) | ||
} | ||
} | ||
|
||
try { | ||
// Begin all iterators | ||
for (const [idx, iterable] of iterators.entries()) { | ||
void getNext(idx, iterable) | ||
// Begin all iterators | ||
for (const [idx, iterable] of iterators.entries()) { | ||
void getNext(idx, iterable) | ||
} | ||
|
||
const it: AsyncIterableIterator<{[P in keyof T]: [ParseInt<`${P}`>, Result<T[P]>]}[number]> = { | ||
[Symbol.asyncIterator]: () => it, | ||
next: async () => { | ||
const nextQueuedResult = queuedResults.shift() | ||
if (nextQueuedResult !== undefined) { | ||
return {done: false as const, value: nextQueuedResult()} | ||
} | ||
if (count === 0) { | ||
return {done: true as const, value: undefined} | ||
} | ||
|
||
// Delegate to iterables as results complete | ||
while (true) { | ||
while (true) { | ||
const nextQueuedResult = queuedResults.shift() | ||
if (nextQueuedResult === undefined) { | ||
break | ||
} else { | ||
yield nextQueuedResult() | ||
} | ||
} | ||
if (count === 0) { | ||
break | ||
} else { | ||
// Promise.withResolvers() is not yet implemented in node | ||
capability = { | ||
resolve: undefined as any, | ||
reject: undefined as any, | ||
promise: undefined as any | ||
} | ||
capability.promise = new Promise((res, rej) => { | ||
capability!.resolve = res | ||
capability!.reject = rej | ||
}) | ||
const nextResult = await capability.promise | ||
if (nextResult === null) { | ||
break | ||
} else { | ||
capability = undefined | ||
yield nextResult() | ||
} | ||
} | ||
// Promise.withResolvers() is not yet implemented in node | ||
capability = { | ||
resolve: undefined as any, | ||
reject: undefined as any, | ||
promise: undefined as any | ||
} | ||
capability.promise = new Promise((res, rej) => { | ||
capability!.resolve = res | ||
capability!.reject = rej | ||
}) | ||
const nextResult = await capability.promise | ||
if (nextResult === null) { | ||
return {done: true as const, value: undefined} | ||
} else { | ||
capability = undefined | ||
return {done: false as const, value: nextResult()} | ||
} | ||
} catch (err) { | ||
// Unwind remaining iterators on failure | ||
try { | ||
await Promise.all(iterators.map((iterator) => iterator.return?.())) | ||
} catch {} | ||
throw err | ||
}, | ||
return: async () => { | ||
await Promise.allSettled(iterators.map((iterator) => iterator.return?.())) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This refactoring was done so we can define this return function. This is necessary so all streams are notified of the return. The async generator function we had previously would hang in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. interesting! really good find having to define our own |
||
return {done: true as const, value: undefined} | ||
}, | ||
throw: async () => { | ||
await Promise.allSettled(iterators.map((iterator) => iterator.return?.())) | ||
return {done: true as const, value: undefined} | ||
} | ||
})() | ||
} | ||
return it | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was an attempt to regularly end the event loop. It will continue if there are still timers running unless these are
unref
ed. This did not allow proper shutdown as there are many things in dependencies still running, but it's good practice nonetheless if we're not keeping a reference to the timer to allow cleanup.