packages/scheduler/src/forks/Scheduler.js JAVASCRIPT 614 lines View on github.com → Search inside
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow8 */910/* eslint-disable no-var */1112import type {PriorityLevel} from '../SchedulerPriorities';1314import {15  enableProfiling,16  frameYieldMs,17  userBlockingPriorityTimeout,18  lowPriorityTimeout,19  normalPriorityTimeout,20  enableRequestPaint,21  enableAlwaysYieldScheduler,22} from '../SchedulerFeatureFlags';2324import {push, pop, peek} from '../SchedulerMinHeap';2526// TODO: Use symbols?27import {28  ImmediatePriority,29  UserBlockingPriority,30  NormalPriority,31  LowPriority,32  IdlePriority,33} from '../SchedulerPriorities';34import {35  markTaskRun,36  markTaskYield,37  markTaskCompleted,38  markTaskCanceled,39  markTaskErrored,40  markSchedulerSuspended,41  markSchedulerUnsuspended,42  markTaskStart,43  stopLoggingProfilingEvents,44  startLoggingProfilingEvents,45} from '../SchedulerProfiling';4647export type Callback = boolean => ?Callback;4849export opaque type Task = {50  id: number,51  callback: Callback | null,52  priorityLevel: PriorityLevel,53  startTime: number,54  expirationTime: number,55  sortIndex: number,56  isQueued?: boolean,57};5859let getCurrentTime: () => number | DOMHighResTimeStamp;60const hasPerformanceNow =61  // $FlowFixMe[method-unbinding]62  typeof performance === 'object' && typeof performance.now === 'function';6364if (hasPerformanceNow) {65  const localPerformance = performance;66  getCurrentTime = () => localPerformance.now();67} else {68  const localDate = Date;69  const initialTime = localDate.now();70  getCurrentTime = () => localDate.now() - initialTime;71}7273// Max 31 bit integer. The max integer size in V8 for 32-bit systems.74// Math.pow(2, 30) - 175// 0b11111111111111111111111111111176var maxSigned31BitInt = 1073741823;7778// Tasks are stored on a min heap79var taskQueue: Array<Task> = [];80var timerQueue: Array<Task> = [];8182// Incrementing id counter. Used to maintain insertion order.83var taskIdCounter = 1;8485var currentTask = null;86var currentPriorityLevel: PriorityLevel = NormalPriority;8788// This is set while performing work, to prevent re-entrance.89var isPerformingWork = false;9091var isHostCallbackScheduled = false;92var isHostTimeoutScheduled = false;9394var needsPaint = false;9596// Capture local references to native APIs, in case a polyfill overrides them.97const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;98const localClearTimeout =99  typeof clearTimeout === 'function' ? clearTimeout : null;100const localSetImmediate =101  typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom102103function advanceTimers(currentTime: number) {104  // Check for tasks that are no longer delayed and add them to the queue.105  let timer = peek(timerQueue);106  while (timer !== null) {107    if (timer.callback === null) {108      // Timer was cancelled.109      pop(timerQueue);110    } else if (timer.startTime <= currentTime) {111      // Timer fired. Transfer to the task queue.112      pop(timerQueue);113      timer.sortIndex = timer.expirationTime;114      push(taskQueue, timer);115      // $FlowFixMe[constant-condition]116      if (enableProfiling) {117        markTaskStart(timer, currentTime);118        timer.isQueued = true;119      }120    } else {121      // Remaining timers are pending.122      return;123    }124    timer = peek(timerQueue);125  }126}127128function handleTimeout(currentTime: number) {129  isHostTimeoutScheduled = false;130  advanceTimers(currentTime);131132  if (!isHostCallbackScheduled) {133    if (peek(taskQueue) !== null) {134      isHostCallbackScheduled = true;135      requestHostCallback();136    } else {137      const firstTimer = peek(timerQueue);138      if (firstTimer !== null) {139        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);140      }141    }142  }143}144145function flushWork(initialTime: number) {146  // $FlowFixMe[constant-condition]147  if (enableProfiling) {148    markSchedulerUnsuspended(initialTime);149  }150151  // We'll need a host callback the next time work is scheduled.152  isHostCallbackScheduled = false;153  if (isHostTimeoutScheduled) {154    // We scheduled a timeout but it's no longer needed. Cancel it.155    isHostTimeoutScheduled = false;156    cancelHostTimeout();157  }158159  isPerformingWork = true;160  const previousPriorityLevel = currentPriorityLevel;161  try {162    // $FlowFixMe[constant-condition]163    if (enableProfiling) {164      try {165        return workLoop(initialTime);166      } catch (error) {167        if (currentTask !== null) {168          const currentTime = getCurrentTime();169          // $FlowFixMe[incompatible-call] found when upgrading Flow170          // $FlowFixMe[incompatible-type]171          markTaskErrored(currentTask, currentTime);172          // $FlowFixMe[incompatible-use] found when upgrading Flow173          currentTask.isQueued = false;174        }175        throw error;176      }177    } else {178      // No catch in prod code path.179      return workLoop(initialTime);180    }181  } finally {182    currentTask = null;183    currentPriorityLevel = previousPriorityLevel;184    isPerformingWork = false;185    // $FlowFixMe[constant-condition]186    if (enableProfiling) {187      const currentTime = getCurrentTime();188      markSchedulerSuspended(currentTime);189    }190  }191}192193function workLoop(initialTime: number) {194  let currentTime = initialTime;195  advanceTimers(currentTime);196  currentTask = peek(taskQueue);197  while (currentTask !== null) {198    if (!enableAlwaysYieldScheduler) {199      if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {200        // This currentTask hasn't expired, and we've reached the deadline.201        break;202      }203    }204    // $FlowFixMe[incompatible-use] found when upgrading Flow205    const callback = currentTask.callback;206    if (typeof callback === 'function') {207      // $FlowFixMe[incompatible-use] found when upgrading Flow208      currentTask.callback = null;209      // $FlowFixMe[incompatible-use] found when upgrading Flow210      currentPriorityLevel = currentTask.priorityLevel;211      // $FlowFixMe[incompatible-use] found when upgrading Flow212      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;213      // $FlowFixMe[constant-condition]214      if (enableProfiling) {215        // $FlowFixMe[incompatible-type] found when upgrading Flow216        markTaskRun(currentTask, currentTime);217      }218      const continuationCallback = callback(didUserCallbackTimeout);219      currentTime = getCurrentTime();220      if (typeof continuationCallback === 'function') {221        // If a continuation is returned, immediately yield to the main thread222        // regardless of how much time is left in the current time slice.223        // $FlowFixMe[incompatible-use] found when upgrading Flow224        currentTask.callback = continuationCallback;225        // $FlowFixMe[constant-condition]226        if (enableProfiling) {227          // $FlowFixMe[incompatible-type] found when upgrading Flow228          markTaskYield(currentTask, currentTime);229        }230        advanceTimers(currentTime);231        return true;232      } else {233        // $FlowFixMe[constant-condition]234        if (enableProfiling) {235          // $FlowFixMe[incompatible-type] found when upgrading Flow236          markTaskCompleted(currentTask, currentTime);237          // $FlowFixMe[incompatible-use] found when upgrading Flow238          currentTask.isQueued = false;239        }240        if (currentTask === peek(taskQueue)) {241          pop(taskQueue);242        }243        advanceTimers(currentTime);244      }245    } else {246      pop(taskQueue);247    }248    currentTask = peek(taskQueue);249    if (enableAlwaysYieldScheduler) {250      if (currentTask === null || currentTask.expirationTime > currentTime) {251        // This currentTask hasn't expired we yield to the browser task.252        break;253      }254    }255  }256  // Return whether there's additional work257  if (currentTask !== null) {258    return true;259  } else {260    const firstTimer = peek(timerQueue);261    if (firstTimer !== null) {262      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);263    }264    return false;265  }266}267268function unstable_runWithPriority<T>(269  priorityLevel: PriorityLevel,270  eventHandler: () => T,271): T {272  switch (priorityLevel) {273    case ImmediatePriority:274    case UserBlockingPriority:275    case NormalPriority:276    case LowPriority:277    case IdlePriority:278      break;279    default:280      priorityLevel = NormalPriority;281  }282283  var previousPriorityLevel = currentPriorityLevel;284  currentPriorityLevel = priorityLevel;285286  try {287    return eventHandler();288  } finally {289    currentPriorityLevel = previousPriorityLevel;290  }291}292293function unstable_next<T>(eventHandler: () => T): T {294  var priorityLevel: PriorityLevel;295  switch (currentPriorityLevel) {296    case ImmediatePriority:297    case UserBlockingPriority:298    case NormalPriority:299      // Shift down to normal priority300      priorityLevel = NormalPriority;301      break;302    default:303      // Anything lower than normal priority should remain at the current level.304      priorityLevel = currentPriorityLevel;305      break;306  }307308  var previousPriorityLevel = currentPriorityLevel;309  currentPriorityLevel = priorityLevel;310311  try {312    return eventHandler();313  } finally {314    currentPriorityLevel = previousPriorityLevel;315  }316}317318function unstable_wrapCallback<T: (...Array<mixed>) => mixed>(callback: T): T {319  var parentPriorityLevel = currentPriorityLevel;320  // $FlowFixMe[incompatible-type]321  // $FlowFixMe[missing-this-annot]322  return function () {323    // This is a fork of runWithPriority, inlined for performance.324    var previousPriorityLevel = currentPriorityLevel;325    currentPriorityLevel = parentPriorityLevel;326327    try {328      return callback.apply(this, arguments);329    } finally {330      currentPriorityLevel = previousPriorityLevel;331    }332  };333}334335function unstable_scheduleCallback(336  priorityLevel: PriorityLevel,337  callback: Callback,338  options?: {delay: number},339): Task {340  var currentTime = getCurrentTime();341342  var startTime;343  // $FlowFixMe[invalid-compare]344  if (typeof options === 'object' && options !== null) {345    var delay = options.delay;346    if (typeof delay === 'number' && delay > 0) {347      startTime = currentTime + delay;348    } else {349      startTime = currentTime;350    }351  } else {352    startTime = currentTime;353  }354355  var timeout;356  switch (priorityLevel) {357    case ImmediatePriority:358      // Times out immediately359      timeout = -1;360      break;361    case UserBlockingPriority:362      // Eventually times out363      timeout = userBlockingPriorityTimeout;364      break;365    case IdlePriority:366      // Never times out367      timeout = maxSigned31BitInt;368      break;369    case LowPriority:370      // Eventually times out371      timeout = lowPriorityTimeout;372      break;373    case NormalPriority:374    default:375      // Eventually times out376      timeout = normalPriorityTimeout;377      break;378  }379380  var expirationTime = startTime + timeout;381382  var newTask: Task = {383    id: taskIdCounter++,384    callback,385    priorityLevel,386    startTime,387    expirationTime,388    sortIndex: -1,389  };390  // $FlowFixMe[constant-condition]391  if (enableProfiling) {392    newTask.isQueued = false;393  }394395  if (startTime > currentTime) {396    // This is a delayed task.397    newTask.sortIndex = startTime;398    push(timerQueue, newTask);399    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {400      // All tasks are delayed, and this is the task with the earliest delay.401      if (isHostTimeoutScheduled) {402        // Cancel an existing timeout.403        cancelHostTimeout();404      } else {405        isHostTimeoutScheduled = true;406      }407      // Schedule a timeout.408      requestHostTimeout(handleTimeout, startTime - currentTime);409    }410  } else {411    newTask.sortIndex = expirationTime;412    push(taskQueue, newTask);413    // $FlowFixMe[constant-condition]414    if (enableProfiling) {415      markTaskStart(newTask, currentTime);416      newTask.isQueued = true;417    }418    // Schedule a host callback, if needed. If we're already performing work,419    // wait until the next time we yield.420    if (!isHostCallbackScheduled && !isPerformingWork) {421      isHostCallbackScheduled = true;422      requestHostCallback();423    }424  }425426  return newTask;427}428429function unstable_cancelCallback(task: Task) {430  // $FlowFixMe[constant-condition]431  if (enableProfiling) {432    if (task.isQueued) {433      const currentTime = getCurrentTime();434      markTaskCanceled(task, currentTime);435      task.isQueued = false;436    }437  }438439  // Null out the callback to indicate the task has been canceled. (Can't440  // remove from the queue because you can't remove arbitrary nodes from an441  // array based heap, only the first one.)442  task.callback = null;443}444445function unstable_getCurrentPriorityLevel(): PriorityLevel {446  return currentPriorityLevel;447}448449let isMessageLoopRunning = false;450let taskTimeoutID: TimeoutID = -1 as any;451452// Scheduler periodically yields in case there is other work on the main453// thread, like user events. By default, it yields multiple times per frame.454// It does not attempt to align with frame boundaries, since most tasks don't455// need to be frame aligned; for those that do, use requestAnimationFrame.456let frameInterval: number = frameYieldMs;457let startTime = -1;458459function shouldYieldToHost(): boolean {460  if (!enableAlwaysYieldScheduler && enableRequestPaint && needsPaint) {461    // Yield now.462    return true;463  }464  const timeElapsed = getCurrentTime() - startTime;465  if (timeElapsed < frameInterval) {466    // The main thread has only been blocked for a really short amount of time;467    // smaller than a single frame. Don't yield yet.468    return false;469  }470  // Yield now.471  return true;472}473474function requestPaint() {475  // $FlowFixMe[constant-condition]476  if (enableRequestPaint) {477    needsPaint = true;478  }479}480481function forceFrameRate(fps: number) {482  if (fps < 0 || fps > 125) {483    // Using console['error'] to evade Babel and ESLint484    console['error'](485      'forceFrameRate takes a positive int between 0 and 125, ' +486        'forcing frame rates higher than 125 fps is not supported',487    );488    return;489  }490  if (fps > 0) {491    frameInterval = Math.floor(1000 / fps);492  } else {493    // reset the framerate494    frameInterval = frameYieldMs;495  }496}497498const performWorkUntilDeadline = () => {499  // $FlowFixMe[constant-condition]500  if (enableRequestPaint) {501    needsPaint = false;502  }503  if (isMessageLoopRunning) {504    const currentTime = getCurrentTime();505    // Keep track of the start time so we can measure how long the main thread506    // has been blocked.507    startTime = currentTime;508509    // If a scheduler task throws, exit the current browser task so the510    // error can be observed.511    //512    // Intentionally not using a try-catch, since that makes some debugging513    // techniques harder. Instead, if `flushWork` errors, then `hasMoreWork` will514    // remain true, and we'll continue the work loop.515    let hasMoreWork = true;516    try {517      hasMoreWork = flushWork(currentTime);518    } finally {519      if (hasMoreWork) {520        // If there's more work, schedule the next message event at the end521        // of the preceding one.522        schedulePerformWorkUntilDeadline();523      } else {524        isMessageLoopRunning = false;525      }526    }527  }528};529530let schedulePerformWorkUntilDeadline;531if (typeof localSetImmediate === 'function') {532  // Node.js and old IE.533  // There's a few reasons for why we prefer setImmediate.534  //535  // Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.536  // (Even though this is a DOM fork of the Scheduler, you could get here537  // with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)538  // https://github.com/facebook/react/issues/20756539  //540  // But also, it runs earlier which is the semantic we want.541  // If other browsers ever implement it, it's better to use it.542  // Although both of these would be inferior to native scheduling.543  schedulePerformWorkUntilDeadline = () => {544    localSetImmediate(performWorkUntilDeadline);545  };546} else if (typeof MessageChannel !== 'undefined') {547  // DOM and Worker environments.548  // We prefer MessageChannel because of the 4ms setTimeout clamping.549  const channel = new MessageChannel();550  const port = channel.port2;551  channel.port1.onmessage = performWorkUntilDeadline;552  schedulePerformWorkUntilDeadline = () => {553    port.postMessage(null);554  };555} else {556  // We should only fallback here in non-browser environments.557  schedulePerformWorkUntilDeadline = () => {558    // $FlowFixMe[not-a-function] nullable value559    localSetTimeout(performWorkUntilDeadline, 0);560  };561}562563function requestHostCallback() {564  if (!isMessageLoopRunning) {565    isMessageLoopRunning = true;566    schedulePerformWorkUntilDeadline();567  }568}569570function requestHostTimeout(571  callback: (currentTime: number) => void,572  ms: number,573) {574  // $FlowFixMe[not-a-function] nullable value575  taskTimeoutID = localSetTimeout(() => {576    callback(getCurrentTime());577  }, ms);578}579580function cancelHostTimeout() {581  // $FlowFixMe[not-a-function] nullable value582  localClearTimeout(taskTimeoutID);583  taskTimeoutID = -1 as any as TimeoutID;584}585586export {587  ImmediatePriority as unstable_ImmediatePriority,588  UserBlockingPriority as unstable_UserBlockingPriority,589  NormalPriority as unstable_NormalPriority,590  IdlePriority as unstable_IdlePriority,591  LowPriority as unstable_LowPriority,592  unstable_runWithPriority,593  unstable_next,594  unstable_scheduleCallback,595  unstable_cancelCallback,596  unstable_wrapCallback,597  unstable_getCurrentPriorityLevel,598  shouldYieldToHost as unstable_shouldYield,599  requestPaint as unstable_requestPaint,600  getCurrentTime as unstable_now,601  forceFrameRate as unstable_forceFrameRate,602};603604export const unstable_Profiling: {605  startLoggingProfilingEvents(): void,606  stopLoggingProfilingEvents(): ArrayBuffer | null,607  // $FlowFixMe[constant-condition]608} | null = enableProfiling609  ? {610      startLoggingProfilingEvents,611      stopLoggingProfilingEvents,612    }613  : null;

Code quality findings 54

Use let or const to avoid scope issues and hoisting
info correctness var-declaration
/* eslint-disable no-var */
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
typeof performance === 'object' && typeof performance.now === 'function';
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
typeof performance === 'object' && typeof performance.now === 'function';
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var maxSigned31BitInt = 1073741823;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var taskQueue: Array<Task> = [];
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var timerQueue: Array<Task> = [];
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var taskIdCounter = 1;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var currentTask = null;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var currentPriorityLevel: PriorityLevel = NormalPriority;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var isPerformingWork = false;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var isHostCallbackScheduled = false;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var isHostTimeoutScheduled = false;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var needsPaint = false;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
typeof clearTimeout === 'function' ? clearTimeout : null;
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
typeof clearTimeout === 'function' ? clearTimeout : null;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (timer !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (timer.callback === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (peek(taskQueue) !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (firstTimer !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (currentTask !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (currentTask !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (typeof callback === 'function') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (typeof callback === 'function') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (typeof continuationCallback === 'function') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (typeof continuationCallback === 'function') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (currentTask === peek(taskQueue)) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (currentTask === null || currentTask.expirationTime > currentTime) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (currentTask !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (firstTimer !== null) {
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var previousPriorityLevel = currentPriorityLevel;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var priorityLevel: PriorityLevel;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var previousPriorityLevel = currentPriorityLevel;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var parentPriorityLevel = currentPriorityLevel;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var previousPriorityLevel = currentPriorityLevel;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var currentTime = getCurrentTime();
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var startTime;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (typeof options === 'object' && options !== null) {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (typeof options === 'object' && options !== null) {
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var delay = options.delay;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (typeof delay === 'number' && delay > 0) {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (typeof delay === 'number' && delay > 0) {
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var timeout;
Ensure all cases are handled or a default case is present
info correctness switch-without-default
switch (priorityLevel) {
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var expirationTime = startTime + timeout;
Use let or const to avoid scope issues and hoisting
info correctness var-declaration
var newTask: Task = {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (typeof localSetImmediate === 'function') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (typeof localSetImmediate === 'function') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if (typeof MessageChannel !== 'undefined') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
} else if (typeof MessageChannel !== 'undefined') {

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.