Prompt and context
This question tests browser concurrency, shared memory, and lifecycle control. Atomics.waitAsync() returns a Promise while a shared integer location still has the expected value, so the caller can continue handling UI work. It only accepts an Int32Array or BigInt64Array view backed by a SharedArrayBuffer. A strong answer connects the waiting protocol to ownership, cancellation, and a usable fallback.
What the interviewer evaluates
- Whether you distinguish non-blocking main-thread waiting from potentially blocking
Atomics.wait()in a Worker. - Whether state values, notification timing, timeouts, and duplicate notifications have explicit invariants.
- Whether cancellation, page hiding, Worker crashes, and buffer release are safe.
- Whether you understand cross-origin isolation, capability detection, and Transferable fallbacks.
Clarifying questions
Confirm whether data may be dropped, the latency target, producer and consumer counts, and whether memory must be shared across Workers. Ask about the browser support matrix and whether the page can use a secure context and cross-origin isolation. Check whether third-party scripts, iframes, or login popups are affected by COOP/COEP. Define whether cancellation means user stop, timeout, or page teardown.
30-second answer outline
I would split the shared region into control words and data slots, using versioned states such as 0=waiting, 1=ready, 2=cancelled, and 3=closed. A consumer atomically reads the state and calls Atomics.waitAsync only while it is waiting. A producer writes the data, publishes the state with Atomics.store, and calls Atomics.notify. The Promise is only a wake-up result; the consumer must reread state before processing. Timeout and cancellation are state transitions, and the buffer is released only after every Worker exits. Unsupported browsers use Transferable chunks with the same cancellation and error semantics.
Step-by-step solution
1. Define shared state and ownership
The control area should include a protocol version, state, sequence, length, and closed flag. A producer writes only slots it owns and publishes readiness after the write completes. A consumer reads the state and sequence before processing; it cannot treat one notification as one durable message. Every wake-up rechecks state because notifications can merge, race, or refer to an earlier batch.
2. Use waitAsync and notify correctly
Atomics.waitAsync(view, index, expected, timeout) first compares the location. A different value returns not-equal; a timeout returns timed-out; otherwise it returns an awaitable Promise. After changing the state, the producer calls Atomics.notify(view, index, count). The protocol can be represented as:
const control = new Int32Array(new SharedArrayBuffer(16));
const WAITING = 0;
const READY = 1;
const CANCELLED = 2;
async function waitForData(timeout = 1000) {
const result = Atomics.waitAsync(control, 0, WAITING, timeout);
const outcome = result.async ? await result.value : result.value;
const state = Atomics.load(control, 0);
return { outcome, state };
}
function publishData() {
Atomics.store(control, 0, READY);
Atomics.notify(control, 0, 1);
}3. Add cancellation, timeout, and close states
Do not try to interrupt an existing Promise. Write a cancellation state and notify waiters instead. After receiving ok, timed-out, or not-equal, the waiter rereads state and chooses to continue, retry, or exit. Closing first stops production, then marks the region closed and notifies every waiter; only after Worker exit is confirmed should the buffer be released.
4. Handle races and backpressure
Multiple producers need CAS or an allocator to claim slots; they must not write the same sequence concurrently. When the queue is full, choose dropping, overwriting, or backpressure according to the product contract, and record depth and drops. A consumer should drain all published sequence numbers in a loop rather than interpreting one wake-up as one message.
5. Manage crashes and page lifecycle
The main thread owns the Worker state machine and heartbeat. On an error or deadline, stop new writes, mark the task failed, and ask remaining Workers to exit. On page hiding or component unmount, send cancellation, wait for a bounded period, then terminate. A restart creates a new control-region version instead of reusing stale sequence and state values.
6. Detect capabilities and degrade safely
At startup check crossOriginIsolated, SharedArrayBuffer, Atomics.waitAsync, and Worker support. Cross-origin isolation changes third-party resource and popup behavior, so performance is not a reason to weaken security policy. Without the required capabilities, use Transferable ArrayBuffer chunks or ordinary postMessage, preserving cancellation, timeout, progress, and error fields. Measure path share and tail latency.
Model high-quality answer
I would detect capabilities and verify the secure-context and cross-origin-isolation requirements before creating a versioned control region. A producer publishes state atomically after writing data and notifies waiters. A consumer calls Atomics.waitAsync only while the state is waiting, then rereads state and sequence regardless of the Promise result. Cancellation, timeout, and close are explicit states. Shutdown stops production, wakes waiters, confirms Worker exit, and only then releases shared memory. Ownership and sequence numbers prevent duplicate consumption, while queue-full behavior is a measured product choice. If shared memory or the API is unavailable, Transferable chunks provide the same lifecycle and error contract.
Common mistakes
- Assuming
notifyguarantees one wake-up per message. - Calling a blocking wait on the main thread and freezing the UI.
- Trusting the Promise result without rereading shared state and sequence.
- Releasing the buffer immediately on cancellation while Workers still run.
- Ignoring cross-origin-isolation and third-party-resource constraints.
- Switching to
postMessagebut dropping timeout, cancellation, or error semantics.
Follow-up questions
When would you choose waitAsync over wait?
waitAsync returns a Promise without blocking the calling thread, which suits the main thread and event-driven code. wait can block a Worker, but only when that Worker’s blocked time cannot harm UI or other critical work. Both require state checks, timeouts, and a close protocol.
Why reread state after notification?
A notification only says that a condition may have changed. Notifications can merge, wake a competitor, or correspond to a previous batch. Reading state, sequence, and length establishes whether data is actually available.
How do you prevent work after cancellation?
Publish cancellation in shared state and check it when claiming a slot, before processing, and before committing the result. Include a task version in result messages; the main thread rejects results from revoked versions.
When should shared memory be abandoned?
Use Transferable chunks when isolation headers break critical dependencies, browser coverage is insufficient, debugging cost exceeds the gain, or throughput is modest. Let capability and telemetry select the path instead of assuming shared memory is always the optimization.