/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at . */ /* eslint complexity: ["error", 36]*/ /** * Pause reducer * @module reducers/pause */ import { prefs } from "../utils/prefs"; // Pause state associated with an individual thread. // Pause state describing all threads. export function initialPauseState(thread = "UnknownThread") { return { cx: { navigateCounter: 0, }, // This `threadcx` is the `cx` variable we pass around in components and actions. // This is pulled via getThreadContext(). // This stores information about the currently selected thread and its paused state. threadcx: { navigateCounter: 0, thread, pauseCounter: 0, }, threads: {}, skipPausing: prefs.skipPausing, mapScopes: prefs.mapScopes, shouldPauseOnDebuggerStatement: prefs.pauseOnDebuggerStatement, shouldPauseOnExceptions: prefs.pauseOnExceptions, shouldPauseOnCaughtExceptions: prefs.pauseOnCaughtExceptions, }; } const resumedPauseState = { isPaused: false, frames: null, framesLoading: false, frameScopes: { generated: {}, original: {}, mappings: {}, }, selectedFrameId: null, why: null, inlinePreview: {}, }; const createInitialPauseState = () => ({ ...resumedPauseState, isWaitingOnBreak: false, command: null, previousLocation: null, expandedScopes: new Set(), lastExpandedScopes: [], }); export function getThreadPauseState(state, thread) { // Thread state is lazily initialized so that we don't have to keep track of // the current set of worker threads. return state.threads[thread] || createInitialPauseState(); } function update(state = initialPauseState(), action) { // All the actions updating pause state must pass an object which designate // the related thread. const getActionThread = () => { const thread = action.thread || action.selectedFrame?.thread || action.frame?.thread; if (!thread) { throw new Error(`Missing thread in action ${action.type}`); } return thread; }; // `threadState` and `updateThreadState` help easily get and update // the pause state for a given thread. const threadState = () => { return getThreadPauseState(state, getActionThread()); }; const updateThreadState = newThreadState => { return { ...state, threads: { ...state.threads, [getActionThread()]: { ...threadState(), ...newThreadState }, }, }; }; switch (action.type) { case "SELECT_THREAD": { // Ignore the action if the related thread doesn't exist. if (!state.threads[action.thread]) { console.warn( `Trying to select a destroyed or non-existent thread '${action.thread}'` ); return state; } return { ...state, threadcx: { ...state.threadcx, thread: action.thread, pauseCounter: state.threadcx.pauseCounter + 1, }, }; } case "INSERT_THREAD": { // When navigating to a new location, // we receive NAVIGATE early, which clear things // then we have REMOVE_THREAD of the previous thread. // INSERT_THREAD will be the very first event with the new thread actor ID. // Automatically select the new top level thread. if (action.newThread.isTopLevel) { return { ...state, threadcx: { ...state.threadcx, thread: action.newThread.actor, pauseCounter: state.threadcx.pauseCounter + 1, }, threads: { ...state.threads, [action.newThread.actor]: createInitialPauseState(), }, }; } return { ...state, threads: { ...state.threads, [action.newThread.actor]: createInitialPauseState(), }, }; } case "REMOVE_THREAD": { if ( action.threadActorID in state.threads || action.threadActorID == state.threadcx.thread ) { // Remove the thread from the cached list const threads = { ...state.threads }; delete threads[action.threadActorID]; let threadcx = state.threadcx; // And also switch to another thread if this was the currently selected one. // As we don't store thread objects in this reducer, and only store thread actor IDs, // we can't try to find the top level thread. So we pick the first available thread, // and hope that's the top level one. if (state.threadcx.thread == action.threadActorID) { threadcx = { ...threadcx, thread: Object.keys(threads)[0], pauseCounter: threadcx.pauseCounter + 1, }; } return { ...state, threadcx, threads, }; } break; } case "PAUSED": { const { thread, topFrame, why } = action; state = { ...state, threadcx: { ...state.threadcx, pauseCounter: state.threadcx.pauseCounter + 1, thread, }, }; return updateThreadState({ isWaitingOnBreak: false, selectedFrameId: topFrame.id, isPaused: true, // On pause, we only receive the top frame, all subsequent ones // will be asynchronously populated via `fetchFrames` action frames: [topFrame], framesLoading: true, frameScopes: { ...resumedPauseState.frameScopes }, why, shouldBreakpointsPaneOpenOnPause: why.type === "breakpoint", }); } case "FETCHED_FRAMES": { const { frames } = action; // We typically receive a PAUSED action before this one, // with only the first frame. Here, we avoid replacing it // with a copy of it in order to avoid triggerring selectors // uncessarily // (note that in jest, action's frames might be empty) // (and if we resume in between PAUSED and FETCHED_FRAMES // threadState().frames might be null) if (threadState().frames) { const previousFirstFrame = threadState().frames[0]; if (previousFirstFrame.id == frames[0]?.id) { frames.splice(0, 1, previousFirstFrame); } } return updateThreadState({ frames, framesLoading: false }); } case "MAP_FRAMES": { const { selectedFrameId, frames } = action; return updateThreadState({ frames, selectedFrameId }); } case "ADD_SCOPES": { const { status, value } = action; const selectedFrameId = action.selectedFrame.id; const generated = { ...threadState().frameScopes.generated, [selectedFrameId]: { pending: status !== "done", scope: value, }, }; return updateThreadState({ frameScopes: { ...threadState().frameScopes, generated, }, }); } case "MAP_SCOPES": { const { status, value } = action; const selectedFrameId = action.selectedFrame.id; const original = { ...threadState().frameScopes.original, [selectedFrameId]: { pending: status !== "done", scope: value?.scope, }, }; const mappings = { ...threadState().frameScopes.mappings, [selectedFrameId]: value?.mappings, }; return updateThreadState({ frameScopes: { ...threadState().frameScopes, original, mappings, }, }); } case "BREAK_ON_NEXT": return updateThreadState({ isWaitingOnBreak: true }); case "SELECT_FRAME": return updateThreadState({ selectedFrameId: action.frame.id }); case "PAUSE_ON_DEBUGGER_STATEMENT": { const { shouldPauseOnDebuggerStatement } = action; prefs.pauseOnDebuggerStatement = shouldPauseOnDebuggerStatement; return { ...state, shouldPauseOnDebuggerStatement, }; } case "PAUSE_ON_EXCEPTIONS": { const { shouldPauseOnExceptions, shouldPauseOnCaughtExceptions } = action; prefs.pauseOnExceptions = shouldPauseOnExceptions; prefs.pauseOnCaughtExceptions = shouldPauseOnCaughtExceptions; // Preserving for the old debugger prefs.ignoreCaughtExceptions = !shouldPauseOnCaughtExceptions; return { ...state, shouldPauseOnExceptions, shouldPauseOnCaughtExceptions, }; } case "COMMAND": if (action.status === "start") { return updateThreadState({ ...resumedPauseState, command: action.command, previousLocation: getPauseLocation(threadState(), action), }); } return updateThreadState({ command: null }); case "RESUME": { if (action.thread == state.threadcx.thread) { state = { ...state, threadcx: { ...state.threadcx, pauseCounter: state.threadcx.pauseCounter + 1, }, }; } return updateThreadState({ ...resumedPauseState, expandedScopes: new Set(), lastExpandedScopes: [...threadState().expandedScopes], shouldBreakpointsPaneOpenOnPause: false, }); } case "EVALUATE_EXPRESSION": return updateThreadState({ command: action.status === "start" ? "expression" : null, }); case "NAVIGATE": { const navigateCounter = state.cx.navigateCounter + 1; return { ...state, cx: { navigateCounter, }, threadcx: { navigateCounter, thread: action.mainThread.actor, pauseCounter: 0, }, threads: { ...state.threads, [action.mainThread.actor]: { ...getThreadPauseState(state, action.mainThread.actor), ...resumedPauseState, }, }, }; } case "TOGGLE_SKIP_PAUSING": { const { skipPausing } = action; prefs.skipPausing = skipPausing; return { ...state, skipPausing }; } case "TOGGLE_MAP_SCOPES": { const { mapScopes } = action; prefs.mapScopes = mapScopes; return { ...state, mapScopes }; } case "SET_EXPANDED_SCOPE": { const { path, expanded } = action; const expandedScopes = new Set(threadState().expandedScopes); if (expanded) { expandedScopes.add(path); } else { expandedScopes.delete(path); } return updateThreadState({ expandedScopes }); } case "ADD_INLINE_PREVIEW": { const { selectedFrame, previews } = action; const selectedFrameId = selectedFrame.id; return updateThreadState({ inlinePreview: { ...threadState().inlinePreview, [selectedFrameId]: previews, }, }); } case "RESET_BREAKPOINTS_PANE_STATE": { return updateThreadState({ ...threadState(), shouldBreakpointsPaneOpenOnPause: false, }); } } return state; } function getPauseLocation(state, action) { const { frames, previousLocation } = state; // NOTE: We store the previous location so that we ensure that we // do not stop at the same location twice when we step over. if (action.command !== "stepOver") { return null; } const frame = frames?.[0]; if (!frame) { return previousLocation; } return { location: frame.location, generatedLocation: frame.generatedLocation, }; } export default update;