pause.js (12019B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 /* eslint complexity: ["error", 38]*/ 6 7 /** 8 * Pause reducer 9 * 10 * @module reducers/pause 11 */ 12 13 import { prefs } from "../utils/prefs"; 14 15 // Pause state associated with an individual thread. 16 17 // Pause state describing all threads. 18 19 export function initialPauseState(thread = "UnknownThread") { 20 return { 21 cx: { 22 navigateCounter: 0, 23 }, 24 // This `threadcx` is the `cx` variable we pass around in components and actions. 25 // This is pulled via getThreadContext(). 26 // This stores information about the currently selected thread and its paused state. 27 threadcx: { 28 navigateCounter: 0, 29 thread, 30 pauseCounter: 0, 31 }, 32 threads: {}, 33 skipPausing: prefs.skipPausing, 34 mapScopes: prefs.mapScopes, 35 shouldPauseOnDebuggerStatement: prefs.pauseOnDebuggerStatement, 36 shouldPauseOnExceptions: prefs.pauseOnExceptions, 37 shouldPauseOnCaughtExceptions: prefs.pauseOnCaughtExceptions, 38 }; 39 } 40 41 const resumedPauseState = { 42 isPaused: false, 43 frames: null, 44 framesLoading: false, 45 frameScopes: { 46 generated: {}, 47 original: {}, 48 mappings: {}, 49 }, 50 selectedFrameId: null, 51 why: null, 52 inlinePreview: {}, 53 }; 54 55 const createInitialPauseState = () => ({ 56 ...resumedPauseState, 57 isWaitingOnBreak: false, 58 command: null, 59 previousLocation: null, 60 expandedScopes: new Set(), 61 lastExpandedScopes: [], 62 shouldBreakpointsPaneOpenOnPause: false, 63 }); 64 65 export function getThreadPauseState(state, thread) { 66 // Thread state is lazily initialized so that we don't have to keep track of 67 // the current set of worker threads. 68 return state.threads[thread] || createInitialPauseState(); 69 } 70 71 function update(state = initialPauseState(), action) { 72 // All the actions updating pause state must pass an object which designate 73 // the related thread. 74 const getActionThread = () => { 75 const thread = 76 action.thread || action.selectedFrame?.thread || action.frame?.thread; 77 if (!thread) { 78 throw new Error(`Missing thread in action ${action.type}`); 79 } 80 return thread; 81 }; 82 83 // `threadState` and `updateThreadState` help easily get and update 84 // the pause state for a given thread. 85 const threadState = () => { 86 return getThreadPauseState(state, getActionThread()); 87 }; 88 const updateThreadState = newThreadState => { 89 return { 90 ...state, 91 threads: { 92 ...state.threads, 93 [getActionThread()]: { ...threadState(), ...newThreadState }, 94 }, 95 }; 96 }; 97 98 switch (action.type) { 99 case "SELECT_THREAD": { 100 // Ignore the action if the related thread doesn't exist. 101 if (!state.threads[action.thread]) { 102 console.warn( 103 `Trying to select a destroyed or non-existent thread '${action.thread}'` 104 ); 105 return state; 106 } 107 108 return { 109 ...state, 110 threadcx: { 111 ...state.threadcx, 112 thread: action.thread, 113 pauseCounter: state.threadcx.pauseCounter + 1, 114 }, 115 }; 116 } 117 118 case "INSERT_THREAD": { 119 // When navigating to a new location, 120 // we receive NAVIGATE early, which clear things 121 // then we have REMOVE_THREAD of the previous thread. 122 // INSERT_THREAD will be the very first event with the new thread actor ID. 123 // Automatically select the new top level thread. 124 if (action.newThread.isTopLevel) { 125 return { 126 ...state, 127 threadcx: { 128 ...state.threadcx, 129 thread: action.newThread.actor, 130 pauseCounter: state.threadcx.pauseCounter + 1, 131 }, 132 threads: { 133 ...state.threads, 134 [action.newThread.actor]: createInitialPauseState(), 135 }, 136 }; 137 } 138 139 return { 140 ...state, 141 threads: { 142 ...state.threads, 143 [action.newThread.actor]: createInitialPauseState(), 144 }, 145 }; 146 } 147 148 case "REMOVE_THREAD": { 149 const { threadActorID } = action; 150 if ( 151 threadActorID in state.threads || 152 threadActorID == state.threadcx.thread 153 ) { 154 // Remove the thread from the cached list 155 const threads = { ...state.threads }; 156 delete threads[threadActorID]; 157 let threadcx = state.threadcx; 158 159 // And also switch to another thread if this was the currently selected one. 160 // As we don't store thread objects in this reducer, and only store thread actor IDs, 161 // we can't try to find the top level thread. So we pick the first available thread, 162 // and hope that's the top level one. 163 if (state.threadcx.thread == threadActorID) { 164 threadcx = { 165 ...threadcx, 166 thread: Object.keys(threads)[0], 167 pauseCounter: threadcx.pauseCounter + 1, 168 }; 169 } 170 return { 171 ...state, 172 threadcx, 173 threads, 174 }; 175 } 176 break; 177 } 178 179 case "PAUSED": { 180 const { thread, topFrame, why } = action; 181 state = { 182 ...state, 183 threadcx: { 184 ...state.threadcx, 185 pauseCounter: state.threadcx.pauseCounter + 1, 186 thread, 187 }, 188 }; 189 190 return updateThreadState({ 191 isWaitingOnBreak: false, 192 selectedFrameId: topFrame.id, 193 isPaused: true, 194 // On pause, we only receive the top frame, all subsequent ones 195 // will be asynchronously populated via `fetchFrames` action 196 frames: [topFrame], 197 framesLoading: true, 198 frameScopes: { ...resumedPauseState.frameScopes }, 199 why, 200 shouldBreakpointsPaneOpenOnPause: why.type === "breakpoint", 201 }); 202 } 203 204 case "FETCHED_FRAMES": { 205 const { frames } = action; 206 207 // We typically receive a PAUSED action before this one, 208 // with only the first frame. Here, we avoid replacing it 209 // with a copy of it in order to avoid triggerring selectors 210 // uncessarily 211 // (note that in jest, action's frames might be empty) 212 // (and if we resume in between PAUSED and FETCHED_FRAMES 213 // threadState().frames might be null) 214 if (threadState().frames) { 215 const previousFirstFrame = threadState().frames[0]; 216 if (previousFirstFrame.id == frames[0]?.id) { 217 frames.splice(0, 1, previousFirstFrame); 218 } 219 } 220 return updateThreadState({ frames, framesLoading: false }); 221 } 222 223 case "MAP_FRAMES": { 224 const { selectedFrameId, frames } = action; 225 return updateThreadState({ frames, selectedFrameId }); 226 } 227 228 case "UPDATE_FRAMES": { 229 const { frames } = action; 230 return updateThreadState({ frames }); 231 } 232 233 case "ADD_SCOPES": { 234 const { status, value } = action; 235 const selectedFrameId = action.selectedFrame.id; 236 237 const generated = { 238 ...threadState().frameScopes.generated, 239 [selectedFrameId]: { 240 pending: status !== "done", 241 // Environment Scope information from the platform. 242 // See https://searchfox.org/mozilla-central/rev/b0e8e4ceb46cb3339cdcb90310fcc161ef4b9e3e/devtools/server/actors/environment.js#42-81 243 scope: value, 244 }, 245 }; 246 247 return updateThreadState({ 248 frameScopes: { 249 ...threadState().frameScopes, 250 generated, 251 }, 252 }); 253 } 254 255 case "MAP_SCOPES": { 256 const { status, value } = action; 257 const selectedFrameId = action.selectedFrame.id; 258 259 const original = { 260 ...threadState().frameScopes.original, 261 [selectedFrameId]: { 262 pending: status !== "done", 263 scope: value?.scope, 264 }, 265 }; 266 267 const mappings = { 268 ...threadState().frameScopes.mappings, 269 [selectedFrameId]: value?.mappings, 270 }; 271 272 return updateThreadState({ 273 frameScopes: { 274 ...threadState().frameScopes, 275 original, 276 mappings, 277 }, 278 }); 279 } 280 281 case "BREAK_ON_NEXT": 282 return updateThreadState({ isWaitingOnBreak: true }); 283 284 case "SELECT_FRAME": 285 return updateThreadState({ selectedFrameId: action.frame.id }); 286 287 case "PAUSE_ON_DEBUGGER_STATEMENT": { 288 const { shouldPauseOnDebuggerStatement } = action; 289 290 prefs.pauseOnDebuggerStatement = shouldPauseOnDebuggerStatement; 291 292 return { 293 ...state, 294 shouldPauseOnDebuggerStatement, 295 }; 296 } 297 298 case "PAUSE_ON_EXCEPTIONS": { 299 const { shouldPauseOnExceptions, shouldPauseOnCaughtExceptions } = action; 300 301 prefs.pauseOnExceptions = shouldPauseOnExceptions; 302 prefs.pauseOnCaughtExceptions = shouldPauseOnCaughtExceptions; 303 304 // Preserving for the old debugger 305 prefs.ignoreCaughtExceptions = !shouldPauseOnCaughtExceptions; 306 307 return { 308 ...state, 309 shouldPauseOnExceptions, 310 shouldPauseOnCaughtExceptions, 311 }; 312 } 313 314 case "COMMAND": 315 if (action.status === "start") { 316 return updateThreadState({ 317 ...resumedPauseState, 318 command: action.command, 319 previousLocation: getPauseLocation(threadState(), action), 320 }); 321 } 322 return updateThreadState({ command: null }); 323 324 case "RESUME": { 325 if (action.thread == state.threadcx.thread) { 326 state = { 327 ...state, 328 threadcx: { 329 ...state.threadcx, 330 pauseCounter: state.threadcx.pauseCounter + 1, 331 }, 332 }; 333 } 334 335 return updateThreadState({ 336 ...resumedPauseState, 337 expandedScopes: new Set(), 338 lastExpandedScopes: [...threadState().expandedScopes], 339 shouldBreakpointsPaneOpenOnPause: false, 340 }); 341 } 342 343 case "EVALUATE_EXPRESSION": 344 return updateThreadState({ 345 command: action.status === "start" ? "expression" : null, 346 }); 347 348 case "NAVIGATE": { 349 const navigateCounter = state.cx.navigateCounter + 1; 350 return { 351 ...state, 352 cx: { 353 navigateCounter, 354 }, 355 threadcx: { 356 navigateCounter, 357 thread: action.mainThread.actor, 358 pauseCounter: 0, 359 }, 360 threads: { 361 ...state.threads, 362 [action.mainThread.actor]: { 363 ...getThreadPauseState(state, action.mainThread.actor), 364 ...resumedPauseState, 365 }, 366 }, 367 }; 368 } 369 370 case "TOGGLE_SKIP_PAUSING": { 371 const { skipPausing } = action; 372 prefs.skipPausing = skipPausing; 373 374 return { ...state, skipPausing }; 375 } 376 377 case "TOGGLE_MAP_SCOPES": { 378 const { mapScopes } = action; 379 prefs.mapScopes = mapScopes; 380 return { ...state, mapScopes }; 381 } 382 383 case "SET_EXPANDED_SCOPE": { 384 const { path, expanded } = action; 385 const expandedScopes = new Set(threadState().expandedScopes); 386 if (expanded) { 387 expandedScopes.add(path); 388 } else { 389 expandedScopes.delete(path); 390 } 391 return updateThreadState({ expandedScopes }); 392 } 393 394 case "ADD_INLINE_PREVIEW": { 395 const { selectedFrame, previews } = action; 396 const selectedFrameId = selectedFrame.id; 397 398 return updateThreadState({ 399 inlinePreview: { 400 ...threadState().inlinePreview, 401 [selectedFrameId]: previews, 402 }, 403 }); 404 } 405 406 case "RESET_BREAKPOINTS_PANE_STATE": { 407 return updateThreadState({ 408 ...threadState(), 409 shouldBreakpointsPaneOpenOnPause: false, 410 }); 411 } 412 } 413 414 return state; 415 } 416 417 function getPauseLocation(state, action) { 418 const { frames, previousLocation } = state; 419 420 // NOTE: We store the previous location so that we ensure that we 421 // do not stop at the same location twice when we step over. 422 if (action.command !== "stepOver") { 423 return null; 424 } 425 426 const frame = frames?.[0]; 427 if (!frame) { 428 return previousLocation; 429 } 430 431 return { 432 location: frame.location, 433 generatedLocation: frame.generatedLocation, 434 }; 435 } 436 437 export default update;