| | import { AsyncLocalStorage } from 'node:async_hooks' |
| | import { createPromiseWithResolvers } from '../../shared/lib/promise-with-resolvers' |
| | import { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask, |
| | expectNoPendingImmediates, |
| | } from './fast-set-immediate.external' |
| | import { createAtomicTimerGroup } from '../app-render/app-render-scheduling' |
| |
|
| | function createLogger() { |
| | const logs: string[] = [] |
| |
|
| | const log = (...args: any[]) => { |
| | const { inspect } = require('node:util') as typeof import('node:util') |
| | const { writeFileSync } = require('node:fs') as typeof import('node:fs') |
| |
|
| | let logLine = args |
| | .map((arg) => |
| | typeof arg === 'string' ? arg : inspect(arg, { colors: true }) |
| | ) |
| | .join(' ') |
| |
|
| | logs.push(logLine) |
| | writeFileSync(process.stdout.fd, logLine + '\n') |
| | } |
| | return { logs, log } |
| | } |
| |
|
| | it('runs immediates after each task', async () => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 1') |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1') |
| | process.nextTick(() => { |
| | log('timeout 1 -> immediate 1 -> nextTick 1') |
| | queueMicrotask(() => { |
| | log('timeout 1 -> immediate 1 -> nextTick 1 -> microtask 1') |
| | }) |
| | queueMicrotask(() => { |
| | process.nextTick(() => { |
| | log( |
| | 'timeout 1 -> immediate 1 -> nextTick 1 -> microtask 2 -> nextTick' |
| | ) |
| | }) |
| | }) |
| | }) |
| | }) |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 2') |
| | }) |
| | process.nextTick(() => { |
| | log('timeout 1 -> nextTick 1') |
| | queueMicrotask(() => { |
| | log('timeout 1 -> nextTick 1 -> microtask 1') |
| | }) |
| | queueMicrotask(() => { |
| | process.nextTick(() => { |
| | log('timeout 1 -> nextTick 1 -> microtask 2 -> nextTick') |
| | }) |
| | }) |
| | process.nextTick(() => { |
| | log('timeout 1 -> nextTick 1 -> nextTick 1') |
| | }) |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 2') |
| | setImmediate(() => { |
| | log('timeout 2 -> immediate 1') |
| | setImmediate(() => { |
| | log('timeout 2 -> immediate 1 -> immediate 1') |
| | }) |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | log('timeout 3') |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | 'timeout 1 -> nextTick 1', |
| | 'timeout 1 -> nextTick 1 -> nextTick 1', |
| | 'timeout 1 -> nextTick 1 -> microtask 1', |
| | 'timeout 1 -> nextTick 1 -> microtask 2 -> nextTick', |
| | |
| | 'timeout 1 -> immediate 1', |
| | 'timeout 1 -> immediate 1 -> nextTick 1', |
| | 'timeout 1 -> immediate 1 -> nextTick 1 -> microtask 1', |
| | 'timeout 1 -> immediate 1 -> nextTick 1 -> microtask 2 -> nextTick', |
| | |
| | 'timeout 1 -> immediate 2', |
| | |
| | 'timeout 2', |
| | |
| | 'timeout 2 -> immediate 1', |
| | |
| | 'timeout 2 -> immediate 1 -> immediate 1', |
| | |
| | 'timeout 3', |
| | ]) |
| | }) |
| |
|
| | it('only affects the task it is called in', async () => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | |
| | |
| | const scheduleTimeout = createAtomicTimerGroup() |
| |
|
| | scheduleTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 1') |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1 (fast)') |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1 (fast) -> immediate 1 (fast)') |
| | }) |
| | }) |
| | }) |
| |
|
| | scheduleTimeout(() => { |
| | log('timeout 2') |
| | try { |
| | expectNoPendingImmediates() |
| | |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| |
|
| | |
| | setImmediate(() => { |
| | log('timeout 2 -> immediate 1 (native)') |
| | done.resolve() |
| | }) |
| | }) |
| |
|
| | scheduleTimeout(() => { |
| | log('timeout 3') |
| | try { |
| | expectNoPendingImmediates() |
| | |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1 (fast)', |
| | |
| | 'timeout 1 -> immediate 1 (fast) -> immediate 1 (fast)', |
| | |
| | 'timeout 2', |
| | |
| | 'timeout 3', |
| | |
| | 'timeout 2 -> immediate 1 (native)', |
| | ]) |
| | }) |
| |
|
| | it('does not run immediates scheduled before it was called', async () => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | |
| | |
| | const scheduleTimeout = createAtomicTimerGroup() |
| |
|
| | scheduleTimeout(() => { |
| | log('timeout 1') |
| |
|
| | |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1 (native)') |
| | done.resolve() |
| | }) |
| |
|
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 2 (fast)') |
| | }) |
| | }) |
| |
|
| | scheduleTimeout(() => { |
| | log('timeout 2') |
| | try { |
| | expectNoPendingImmediates() |
| | |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 2 (fast)', |
| | |
| | 'timeout 2', |
| | |
| | 'timeout 1 -> immediate 1 (native)', |
| | ]) |
| | }) |
| |
|
| | it('runs immediates scheduled in nextTick', async () => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 1') |
| | process.nextTick(() => { |
| | setImmediate(() => { |
| | log('timeout 1 -> nextTick -> immediate 1') |
| | process.nextTick(() => { |
| | setImmediate(() => { |
| | log( |
| | 'timeout 1 -> nextTick -> immediate 1 -> nextTick -> immediate 1' |
| | ) |
| | }) |
| | }) |
| | }) |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | log('timeout 2') |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> nextTick -> immediate 1', |
| | |
| | 'timeout 1 -> nextTick -> immediate 1 -> nextTick -> immediate 1', |
| | |
| | 'timeout 2', |
| | ]) |
| | }) |
| |
|
| | it('runs ticks and microtasks from immediates before moving onto the next task', async () => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 1') |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1') |
| | queueMicrotask(() => { |
| | log('timeout 1 -> immediate 1 -> microtask 1') |
| | queueMicrotask(() => { |
| | log('timeout 1 -> immediate 1 -> microtask 1 -> microtask 1') |
| | }) |
| | process.nextTick(() => { |
| | log('timeout 1 -> immediate 1 -> microtask 1 -> nextTick') |
| | }) |
| | }) |
| | process.nextTick(() => { |
| | log('timeout 1 -> immediate 1 -> nextTick') |
| | }) |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | log('timeout 2') |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1', |
| | 'timeout 1 -> immediate 1 -> nextTick', |
| | 'timeout 1 -> immediate 1 -> microtask 1', |
| | 'timeout 1 -> immediate 1 -> microtask 1 -> microtask 1', |
| | 'timeout 1 -> immediate 1 -> microtask 1 -> nextTick', |
| | |
| | 'timeout 2', |
| | ]) |
| | }) |
| |
|
| | describe('alternate sources of immediates', () => { |
| | it('promisify(setImmediate)', async () => { |
| | |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | const { promisify } = require('node:util') as typeof import('node:util') |
| | const promisifiedSetImmediate = promisify(setImmediate) |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 1') |
| | promisifiedSetImmediate().then(() => { |
| | log('timeout 1 -> immediate 1') |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | log('timeout 2') |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1', |
| | |
| | 'timeout 2', |
| | ]) |
| | }) |
| |
|
| | it('require("node:timers").setImmediate', async () => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | const timers = require('node:timers') as typeof import('node:timers') |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 1') |
| | timers.setImmediate(() => { |
| | log('timeout 1 -> immediate 1') |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | log('timeout 2') |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1', |
| | |
| | 'timeout 2', |
| | ]) |
| | }) |
| |
|
| | it('require("node:timers/promises").setImmediate', async () => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | const timersPromises = |
| | require('node:timers/promises') as typeof import('node:timers/promises') |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 1') |
| | timersPromises.setImmediate().then(() => { |
| | log('timeout 1 -> immediate 1') |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | log('timeout 2') |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1', |
| | |
| | 'timeout 2', |
| | ]) |
| | }) |
| | }) |
| |
|
| | describe('patched function behavior', () => { |
| | describe('setImmediate', () => { |
| | it('extra arguments are passed to callback', async () => { |
| | const done = createPromiseWithResolvers<void>() |
| | const passedArgs = [1, 2, 3] |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | setImmediate( |
| | (...receivedArgs) => { |
| | try { |
| | expect(passedArgs).toEqual(receivedArgs) |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }, |
| | ...passedArgs |
| | ) |
| | }) |
| |
|
| | await done.promise |
| | }) |
| |
|
| | it('validates the first argument', async () => { |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | try { |
| | expect(() => setImmediate(undefined as any)).toThrow( |
| | /The "callback" argument must be of type function. Received undefined/ |
| | ) |
| | expect(() => setImmediate('not a callback' as any)).toThrow( |
| | /The "callback" argument must be of type function. Received type string/ |
| | ) |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| | }) |
| | }) |
| |
|
| | describe('process.nextTick', () => { |
| | it('extra arguments are passed to callback', async () => { |
| | const done = createPromiseWithResolvers<void>() |
| | const passedArgs = [1, 2, 3] |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | process.nextTick( |
| | (...receivedArgs: unknown[]) => { |
| | try { |
| | expect(passedArgs).toEqual(receivedArgs) |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }, |
| | ...passedArgs |
| | ) |
| | }) |
| |
|
| | await done.promise |
| | }) |
| |
|
| | it('validates the first argument', async () => { |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | try { |
| | expect(() => process.nextTick(undefined as any)).toThrow( |
| | /The "callback" argument must be of type function. Received undefined/ |
| | ) |
| | expect(() => process.nextTick('not a callback' as any)).toThrow( |
| | /The "callback" argument must be of type function. Received type string/ |
| | ) |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| | }) |
| | }) |
| | }) |
| |
|
| | describe('async context propagation', () => { |
| | it('propagates AsyncLocalStorage to setImmediate', async () => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| | const Ctx = new AsyncLocalStorage<string>() |
| |
|
| | Ctx.run('outer', () => { |
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| | log(`timeout 1 :: ${Ctx.getStore()}`) |
| | setImmediate(() => { |
| | |
| | log(`timeout 1 -> immediate 1 :: ${Ctx.getStore()}`) |
| | |
| | Ctx.run('inner', () => { |
| | setImmediate(() => { |
| | |
| | log( |
| | `timeout 1 -> immediate 1 -> immediate 1 :: ${Ctx.getStore()}` |
| | ) |
| | }) |
| | }) |
| | }) |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | |
| | log(`timeout 2 :: ${Ctx.getStore()}`) |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1 :: outer', |
| | |
| | 'timeout 1 -> immediate 1 :: outer', |
| | |
| | 'timeout 1 -> immediate 1 -> immediate 1 :: inner', |
| | |
| | 'timeout 2 :: undefined', |
| | ]) |
| | }) |
| |
|
| | it('does not break AsyncLocalStorage propagation in process.nextTick', async () => { |
| | |
| | |
| | |
| |
|
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| | const Ctx = new AsyncLocalStorage<string>() |
| |
|
| | Ctx.run('hello', () => { |
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log(`timeout 1 :: ${Ctx.getStore()}`) |
| | process.nextTick(() => { |
| | |
| | log(`timeout 1 -> nextTick :: ${Ctx.getStore()}`) |
| | }) |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | |
| | log(`timeout 2 :: ${Ctx.getStore()}`) |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1 :: hello', |
| | |
| | 'timeout 1 -> nextTick :: hello', |
| | |
| | 'timeout 2 :: undefined', |
| | ]) |
| | }) |
| | }) |
| |
|
| | describe('allows cancelling immediates', () => { |
| | it('synchronously', async () => { |
| | const { log, logs } = createLogger() |
| |
|
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 1') |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1') |
| | }) |
| | const immediate2 = setImmediate(() => { |
| | log('timeout 1 -> immediate 2') |
| | }) |
| | clearImmediate(immediate2) |
| | }) |
| |
|
| | setTimeout(() => { |
| | log('timeout 2') |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1', |
| | |
| | 'timeout 2', |
| | ]) |
| | }) |
| |
|
| | it('from a nextTick', async () => { |
| | const { log, logs } = createLogger() |
| |
|
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 1') |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1') |
| | }) |
| | const immediate2 = setImmediate(() => { |
| | log('timeout 1 -> immediate 2') |
| | }) |
| | process.nextTick(() => { |
| | clearImmediate(immediate2) |
| | }) |
| | }) |
| | setTimeout(() => { |
| | log('timeout 2') |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1', |
| | |
| | 'timeout 2', |
| | ]) |
| | }) |
| |
|
| | it('from another immediate', async () => { |
| | const { log, logs } = createLogger() |
| |
|
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 1') |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1') |
| | clearImmediate(immediate2) |
| | }) |
| | const immediate2 = setImmediate(() => { |
| | log('timeout 1 -> immediate 2') |
| | }) |
| | }) |
| | setTimeout(() => { |
| | log('timeout 2') |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1', |
| | |
| | 'timeout 2', |
| | ]) |
| | }) |
| |
|
| | it('promisified - with an AbortSignal after creating', async () => { |
| | const { log, logs } = createLogger() |
| |
|
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | const { promisify } = require('node:util') as typeof import('node:util') |
| | const promisifiedSetImmediate = promisify(setImmediate) |
| |
|
| | const abortError = new Error('Stop right there') |
| | let thrownOnAbort: unknown |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 1') |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1') |
| | }) |
| |
|
| | const abortController = new AbortController() |
| |
|
| | promisifiedSetImmediate(undefined, { |
| | signal: abortController.signal, |
| | }).then( |
| | () => { |
| | log('timeout 1 -> immediate 2') |
| | }, |
| | (err) => { |
| | thrownOnAbort = err |
| | } |
| | ) |
| |
|
| | abortController.abort(abortError) |
| | }) |
| | setTimeout(() => { |
| | log('timeout 2') |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1', |
| | |
| | 'timeout 2', |
| | ]) |
| | expect(thrownOnAbort).toBe(abortError) |
| | }) |
| |
|
| | it('promisified - with an AbortSignal that was already aborted', async () => { |
| | const { log, logs } = createLogger() |
| |
|
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | const { promisify } = require('node:util') as typeof import('node:util') |
| | const promisifiedSetImmediate = promisify(setImmediate) |
| |
|
| | const abortError = new Error('Stop right there') |
| | let thrownOnAbort: unknown |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| |
|
| | log('timeout 1') |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1') |
| | }) |
| |
|
| | const abortController = new AbortController() |
| | abortController.abort(abortError) |
| |
|
| | promisifiedSetImmediate(undefined, { |
| | signal: abortController.signal, |
| | }).then( |
| | () => { |
| | log('timeout 1 -> immediate 2') |
| | }, |
| | (err) => { |
| | thrownOnAbort = err |
| | } |
| | ) |
| | }) |
| | setTimeout(() => { |
| | log('timeout 2') |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1', |
| | |
| | 'timeout 2', |
| | ]) |
| | expect(thrownOnAbort).toBe(abortError) |
| | }) |
| | }) |
| |
|
| | describe('uncaught errors in setImmediate do not affect surrounding tasks or other immediates', () => { |
| | type TriggeredUncaught = { |
| | error: unknown |
| | kind: 'uncaughtException' | 'unhandledRejection' |
| | } |
| |
|
| | const trackUncaughtErrors = ( |
| | handler: ( |
| | error: unknown, |
| | kind: 'uncaughtException' | 'unhandledRejection' |
| | ) => void |
| | ) => { |
| | |
| | |
| | const onUncaughtException = (err: unknown) => { |
| | handler(err, 'uncaughtException') |
| | } |
| | process.setUncaughtExceptionCaptureCallback(onUncaughtException) |
| |
|
| | |
| | |
| | |
| | |
| | const prevListeners = process.rawListeners('unhandledRejection') |
| | process.removeAllListeners('unhandledRejection') |
| | const onUnhandledRejection = (err: unknown) => { |
| | handler(err, 'unhandledRejection') |
| | } |
| | process.on('unhandledRejection', onUnhandledRejection) |
| |
|
| | return { |
| | [Symbol.dispose]() { |
| | process.setUncaughtExceptionCaptureCallback(null) |
| |
|
| | process.off('unhandledRejection', onUnhandledRejection) |
| | for (const listener of prevListeners) { |
| | process.on( |
| | 'unhandledRejection', |
| | listener as NodeJS.UnhandledRejectionListener |
| | ) |
| | } |
| | }, |
| | } |
| | } |
| |
|
| | it('sync errors trigger uncaughtException', async () => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | const Ctx = new AsyncLocalStorage<string>() |
| | const contextValue = 'hello' |
| |
|
| | let triggeredError: TriggeredUncaught | undefined = undefined |
| | using _ = trackUncaughtErrors((error, kind) => { |
| | log(`${kind} - ${Ctx.getStore()}`) |
| | triggeredError = { error, kind } |
| | }) |
| |
|
| | const error = new Error('kaboom') |
| |
|
| | Ctx.run(contextValue, () => { |
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| | log('timeout 1') |
| |
|
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1') |
| |
|
| | |
| | |
| | process.nextTick(() => { |
| | log('timeout 1 -> immediate 1 -> nextTick') |
| | }) |
| |
|
| | throw error |
| | }) |
| |
|
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 2') |
| | }) |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | log('timeout 2') |
| | |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(triggeredError).toEqual({ error, kind: 'uncaughtException' }) |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1', |
| | 'timeout 1 -> immediate 1 -> nextTick', |
| |
|
| | |
| | |
| | `uncaughtException - undefined`, |
| |
|
| | |
| | 'timeout 1 -> immediate 2', |
| | |
| | 'timeout 2', |
| | ]) |
| | }) |
| |
|
| | it('sync errors in nextTick trigger uncaughtException', async () => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | const Ctx = new AsyncLocalStorage<string>() |
| | const contextValue = 'hello' |
| |
|
| | let triggeredError: TriggeredUncaught | undefined = undefined |
| | using _ = trackUncaughtErrors((error, kind) => { |
| | |
| | log(`${kind} - ${Ctx.getStore()}`) |
| | triggeredError = { error, kind } |
| | }) |
| |
|
| | const error = new Error('kaboom') |
| |
|
| | Ctx.run(contextValue, () => { |
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| | log('timeout 1') |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1') |
| | process.nextTick(() => { |
| | log(`timeout 1 -> immediate 1 -> nextTick - ${Ctx.getStore()}`) |
| | throw error |
| | }) |
| | }) |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 2') |
| | }) |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | log('timeout 2') |
| | |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(triggeredError).toEqual({ error, kind: 'uncaughtException' }) |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1', |
| | `timeout 1 -> immediate 1 -> nextTick - ${contextValue}`, |
| |
|
| | |
| | |
| | `uncaughtException - undefined`, |
| |
|
| | |
| | 'timeout 1 -> immediate 2', |
| | |
| | 'timeout 2', |
| | ]) |
| | }) |
| |
|
| | it('sync errors in nextTick before immediate', async () => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | let triggeredError: TriggeredUncaught | undefined = undefined |
| | using _ = trackUncaughtErrors((error, kind) => { |
| | log(kind) |
| | triggeredError = { error, kind } |
| | }) |
| |
|
| | const error = new Error('kaboom') |
| |
|
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| | log('timeout 1') |
| | process.nextTick(() => { |
| | log('timeout 1 -> nextTick') |
| | throw error |
| | }) |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1') |
| | }) |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 2') |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | log('timeout 2') |
| | |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(triggeredError).toEqual({ error, kind: 'uncaughtException' }) |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | 'timeout 1 -> nextTick', |
| | 'uncaughtException', |
| | |
| | 'timeout 1 -> immediate 1', |
| | |
| | 'timeout 1 -> immediate 2', |
| | |
| | 'timeout 2', |
| | ]) |
| | }) |
| |
|
| | describe('unhandled rejections', () => { |
| | type Case = { |
| | name: string |
| | immediate: ( |
| | name: string, |
| | error: Error, |
| | log: (...args: any[]) => void |
| | ) => void |
| | } |
| |
|
| | const unhandledRejectionCases: Case[] = [ |
| | { |
| | name: 'Promise.resolve().then(...)', |
| | immediate: (name, error, log) => { |
| | log(name) |
| | void Promise.resolve().then(() => { |
| | log(`${name} :: erroring`) |
| | throw error |
| | }) |
| | }, |
| | }, |
| | { |
| | name: 'throw in unawaited async IIFE', |
| | immediate: (name, error, log) => { |
| | log(name) |
| | void (async () => { |
| | await Promise.resolve() |
| | log(`${name} :: erroring`) |
| | throw error |
| | })() |
| | }, |
| | }, |
| | { |
| | name: 'Promise.reject(...)', |
| | immediate: (name, error, log) => { |
| | log(name) |
| | log(`${name} :: erroring`) |
| | Promise.reject(error) |
| | }, |
| | }, |
| | { |
| | name: 'throw in async immediate', |
| | immediate: async (name, error, log) => { |
| | log(name) |
| | await Promise.resolve() |
| | log(`${name} :: erroring`) |
| | throw error |
| | }, |
| | }, |
| | ] |
| |
|
| | it.each(unhandledRejectionCases)('$name', async ({ immediate }) => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | const Ctx = new AsyncLocalStorage<string>() |
| | const contextValue = 'hello' |
| |
|
| | let triggeredError: TriggeredUncaught | undefined = undefined |
| | using _ = trackUncaughtErrors((error, kind) => { |
| | |
| | log(`${kind} - ${Ctx.getStore()}`) |
| | triggeredError = { error, kind } |
| | }) |
| |
|
| | const error = new Error('kaboom') |
| |
|
| | Ctx.run(contextValue, () => { |
| | setTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| | log('timeout 1') |
| | setImmediate(() => { |
| | return immediate('timeout 1 -> immediate 1', error, log) |
| | }) |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 2') |
| | }) |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | log('timeout 2') |
| | |
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(triggeredError).toEqual({ error, kind: 'unhandledRejection' }) |
| |
|
| | expect(logs).toEqual([ |
| | |
| | 'timeout 1', |
| | |
| | 'timeout 1 -> immediate 1', |
| | 'timeout 1 -> immediate 1 :: erroring', |
| |
|
| | |
| | |
| |
|
| | |
| | 'timeout 1 -> immediate 2', |
| |
|
| | |
| | `unhandledRejection - ${contextValue}`, |
| | |
| | |
| | |
| |
|
| | |
| | 'timeout 2', |
| | ]) |
| | }) |
| | }) |
| | }) |
| |
|
| | describe('error recovery', () => { |
| | describe('when crashing, it bails out to native setImmediate and does not break subsequent calls', () => { |
| | const expectCorrectRunToWork = async () => { |
| | const { log, logs } = createLogger() |
| | const done = createPromiseWithResolvers<void>() |
| |
|
| | setTimeout(() => { |
| | try { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| | } catch (err) { |
| | return done.reject(err) |
| | } |
| |
|
| | log('timeout 1') |
| |
|
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1') |
| | }) |
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 2') |
| | }) |
| | }) |
| |
|
| | setTimeout(() => { |
| | log('timeout 2') |
| |
|
| | try { |
| | expectNoPendingImmediates() |
| | done.resolve() |
| | } catch (err) { |
| | done.reject(err) |
| | } |
| | }) |
| |
|
| | await done.promise |
| |
|
| | expect(logs).toEqual([ |
| | 'timeout 1', |
| | 'timeout 1 -> immediate 1', |
| | 'timeout 1 -> immediate 2', |
| | 'timeout 2', |
| | ]) |
| | } |
| |
|
| | const schedulingCases = [ |
| | { |
| | description: 'in sync code', |
| | scheduleCrash: (cb: () => void) => { |
| | cb() |
| | }, |
| | }, |
| | { |
| | description: 'in nextTick', |
| | scheduleCrash: (cb: () => void) => { |
| | process.nextTick(() => { |
| | cb() |
| | }) |
| | }, |
| | }, |
| | { |
| | description: 'in microtask', |
| | scheduleCrash: (cb: () => void) => { |
| | queueMicrotask(() => { |
| | cb() |
| | }) |
| | }, |
| | }, |
| | { |
| | description: 'after microtasks', |
| | scheduleCrash: (cb: () => void) => { |
| | queueMicrotask(() => { |
| | process.nextTick(() => { |
| | cb() |
| | }) |
| | }) |
| | }, |
| | }, |
| | ] |
| |
|
| | describe.each([ |
| | { |
| | description: 'starting capture twice in the same task', |
| | invalidCall: () => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| | }, |
| | }, |
| | { |
| | description: 'expectNoPendingImmediates in the same task as capture', |
| | invalidCall: () => { |
| | expectNoPendingImmediates() |
| | }, |
| | }, |
| | ])('crash reason - $description', ({ invalidCall }) => { |
| | it.each(schedulingCases)( |
| | 'after a crash - $description', |
| | async ({ scheduleCrash }) => { |
| | |
| |
|
| | const { log, logs } = createLogger() |
| | const dones = [ |
| | createPromiseWithResolvers<void>(), |
| | createPromiseWithResolvers<void>(), |
| | createPromiseWithResolvers<void>(), |
| | ] |
| |
|
| | |
| | |
| | const scheduleTimeout = createAtomicTimerGroup() |
| |
|
| | scheduleTimeout(() => { |
| | |
| | setImmediate(() => { |
| | log('immediate 1 (native)') |
| | dones[0].resolve() |
| | }) |
| | }) |
| | scheduleTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| | log('timeout 1') |
| |
|
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1 (patched)') |
| | dones[1].resolve() |
| | }) |
| |
|
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 2 (patched)') |
| | dones[2].resolve() |
| | }) |
| |
|
| | scheduleCrash(() => { |
| | expect(() => invalidCall()).toThrow() |
| | }) |
| | }) |
| |
|
| | await Promise.all(dones.map((d) => d.promise)) |
| |
|
| | expect(logs).toEqual([ |
| | 'timeout 1', |
| | |
| | |
| | 'immediate 1 (native)', |
| | 'timeout 1 -> immediate 1 (patched)', |
| | 'timeout 1 -> immediate 2 (patched)', |
| | ]) |
| |
|
| | |
| | await expectCorrectRunToWork() |
| | } |
| | ) |
| |
|
| | it.each(schedulingCases)( |
| | 'after a crash in a patched immediate - $description', |
| | async ({ scheduleCrash }) => { |
| | |
| |
|
| | const { log, logs } = createLogger() |
| | const dones = [ |
| | createPromiseWithResolvers<void>(), |
| | createPromiseWithResolvers<void>(), |
| | createPromiseWithResolvers<void>(), |
| | ] |
| |
|
| | |
| | |
| | const scheduleTimeout = createAtomicTimerGroup() |
| |
|
| | scheduleTimeout(() => { |
| | |
| | setImmediate(() => { |
| | log('immediate 1 (native)') |
| | dones[0].resolve() |
| | }) |
| | }) |
| | scheduleTimeout(() => { |
| | DANGEROUSLY_runPendingImmediatesAfterCurrentTask() |
| | log('timeout 1') |
| |
|
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 1 (patched)') |
| | dones[1].resolve() |
| | }) |
| |
|
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 2 (patched)') |
| | scheduleCrash(() => { |
| | expect(() => expectNoPendingImmediates()).toThrow() |
| | }) |
| | }) |
| |
|
| | setImmediate(() => { |
| | log('timeout 1 -> immediate 3 (patched)') |
| | dones[2].resolve() |
| | }) |
| | }) |
| |
|
| | await Promise.all(dones.map((d) => d.promise)) |
| |
|
| | expect(logs).toEqual([ |
| | 'timeout 1', |
| | 'timeout 1 -> immediate 1 (patched)', |
| | 'timeout 1 -> immediate 2 (patched)', |
| | |
| | |
| | 'immediate 1 (native)', |
| | 'timeout 1 -> immediate 3 (patched)', |
| | ]) |
| |
|
| | |
| | await expectCorrectRunToWork() |
| | } |
| | ) |
| | }) |
| | }) |
| | }) |
| |
|