Event demultiplexer
Start a timer, file read, and DNS lookup together, then observe ready callbacks returning to JavaScript.
Timeline
Understanding: Event demultiplexer
You can read this chapter before running the experiment. Then return to the live trace and match each concept to a real event.
Think of a hotel dispatcher. They do not stand at every door and repeatedly ask whether a room is ready. Services report readiness, and the dispatcher forwards those notifications to one receptionist: the JavaScript thread.
Node registers async work with libuv and returns control to the application. libuv uses OS readiness for sockets, usually delegates regular files and dns.lookup to its native thread pool, and tracks timers as time thresholds. These internal paths converge when callbacks return to JavaScript.
Terms used in this experiment
Understand the words first, then the execution order.
Event source
A readiness source such as a socket, timer, file operation, DNS request, or worker message.
Demultiplexer
A mechanism that waits for many sources and returns the subset that is ready.
libuv
The native Node library that unifies the Event Loop and async operations across operating systems.
Poll
The phase that handles many ready I/O callbacks and may wait for additional events.
What happens step by step
Each step maps to an observable runtime state.
- 01Registration
JavaScript starts a timer, readFile, and DNS without waiting in place.
- 02Delegation
libuv uses a timer structure, an OS facility, or its native thread pool.
- 03Free stack
Node can process other requests while the sources are not ready.
- 04Readiness signal
The OS/libuv reports which source has completed.
- 05Callback
The ready function enters the JavaScript stack when it is free.
Where the result needs context
These details explain why similar code can sometimes produce a different trace.
Not every source follows one path
A network socket usually uses epoll, kqueue, or IOCP; a regular file often uses the thread pool; a timer is a time-threshold check. “Demultiplexer” is a useful shared model, not one universal object behind every API.
dns.lookup differs from dns.resolve
dns.lookup uses the system resolver and usually the shared libuv pool. dns.resolve* methods issue DNS requests through another path. “DNS” alone does not identify the mechanism.
Promise.all starts nothing
readFile, lookup, and the timer start while the expressions above are evaluated. Promise.all receives already-created Promises, preserves result order, and coordinates waiting.
Fast rejection is not cancellation
On the success path Promise.all waits for all three results. If one Promise rejects, the aggregate rejects early, but the remaining operations are not automatically canceled.
First understand which parts of Node participate in execution.
Then remove instrumentation and focus on the central mechanism.
Finally match the model to the code that produces the live trace.
A minimal model without instrumentation
const file = fs.promises.readFile('package.json');
const dns = dns.promises.lookup('localhost');
const timer = new Promise(r => setTimeout(r, 180));
await Promise.all([file, dns, timer]);The complete code executed by the scenario
This is not an alternative example: these are the functions and files used by the Run button.
The source is generated from the real server function. Scenarios using a child process or Worker include every participating file.
import { readFile } from 'node:fs';
import { lookup } from 'node:dns';
import { fileURLToPath } from 'node:url';
const packageJsonPath = fileURLToPath(
new URL('../package.json', import.meta.url),
);
async function eventDemultiplexer(emit) {
emit('call-stack', 'sync', 'Регистрируем три независимые операции');
const timerTask = new Promise((resolve) => {
emit('timers', 'schedule', 'Таймер 180 мс передан подсистеме таймеров');
setTimeout(() => {
emit('timers', 'callback', 'Таймер готов: callback вернулся в JavaScript');
resolve('timer');
}, 180);
});
const fileTask = new Promise((resolve, reject) => {
emit('libuv', 'schedule', 'Чтение package.json делегировано libuv');
readFile(packageJsonPath, 'utf8', (error, content) => {
if (error) {
reject(error);
return;
}
emit(
'poll',
'callback',
`Файл готов: получено ${Buffer.byteLength(content)} байт`,
);
resolve('file');
});
});
const dnsTask = new Promise((resolve, reject) => {
emit('libuv', 'schedule', 'DNS lookup localhost делегирован libuv');
lookup('localhost', (error, address) => {
if (error) {
reject(error);
return;
}
emit('poll', 'callback', `DNS готов: localhost → ${address}`);
resolve('dns');
});
});
emit(
'demultiplexer',
'info',
'JS-стек свободен; Event Loop ожидает сигналы готовности, а не опрашивает каждую функцию вручную',
);
const completed = await Promise.all([timerTask, fileTask, dnsTask]);
emit('result', 'result', `Все источники готовы: ${completed.join(', ')}`);
}The emit(...) calls become live-trace rows. await and Promise keep the HTTP stream open until the scenario completes.
Common misconceptions
The myth is on the left; the accurate model is on the right.
Node repeatedly calls every function to ask whether it is ready.
Waiting is performed by efficient OS and libuv mechanisms.
Every async operation creates a new thread.
Sockets usually use OS readiness; only some APIs use a limited thread pool.
Parallel completion means parallel JavaScript callbacks.
Callbacks still enter a single JavaScript environment one at a time.
Promise.all starts and cancels its operations.
The operations are normally already running. Cancellation needs a separate mechanism such as AbortSignal when the specific API supports it.
Explain it in your own words
If you can explain the answer without quoting documentation, your mental model is starting to take shape.
- Why does a fast DNS lookup not have to wait for a slow timer?
- Which part differs between a network socket and a regular file?