Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/scheduler/src/__tests__/Scheduler-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,41 @@ describe('SchedulerBrowser', () => {
runtime.assertLog(['Message Event', 'B']);
});

it('re-entrant message event is a no-op (Firefox alert/debugger regression)', () => {
// Regression: https://github.com/facebook/react/issues/17355
//
// Firefox and IE allow alert() or a debugger statement to pause the JS
// call stack while still letting the event loop run. If a MessageChannel
// message is already queued (e.g. because the scheduler yielded), Firefox
// delivers it immediately when execution resumes — re-entering
// performWorkUntilDeadline while the first invocation is still on the
// stack. Without the isPerformingWork guard, this caused
// "Should not already be working." in react-reconciler.
//
// We simulate this by directly invoking port1.onmessage (which IS
// performWorkUntilDeadline) from inside a running task, mimicking the
// browser delivering a new MessageChannel event during a debugger pause.

// port1 is the same shared object for all mock MessageChannel instances.
// After scheduler initialisation, port1.onmessage === performWorkUntilDeadline.
const performWorkUntilDeadline = new global.MessageChannel().port1.onmessage;

let taskRunCount = 0;
scheduleCallback(NormalPriority, () => {
taskRunCount++;
runtime.log('Task');
// Re-enter performWorkUntilDeadline while isPerformingWork is true.
// Should be a no-op — must not throw "Should not already be working."
expect(() => performWorkUntilDeadline()).not.toThrow();
});

runtime.assertLog(['Post Message']);
runtime.fireMessageEvent();
// Task ran exactly once; the re-entrant invocation was silently ignored.
expect(taskRunCount).toBe(1);
runtime.assertLog(['Message Event', 'Task']);
});

it('yielding continues in a new task regardless of how much time is remaining', () => {
scheduleCallback(NormalPriority, () => {
runtime.log('Original Task');
Expand Down
7 changes: 7 additions & 0 deletions packages/scheduler/src/forks/Scheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,13 @@ function forceFrameRate(fps: number) {
}

const performWorkUntilDeadline = () => {
// If we're already performing work, a browser-level pause (e.g., alert() or
// a debugger statement in Firefox or IE) allowed another event loop task to
// fire while the current call stack is still executing. Skip this invocation;
// the ongoing work loop will resume and handle any remaining tasks.
if (isPerformingWork) {
return;
}
if (enableRequestPaint) {
needsPaint = false;
}
Expand Down