Source code

Revision control

Copy as Markdown

Other Tools

/* 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 <http://mozilla.org/MPL/2.0/>. */
/* 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: [],
shouldBreakpointsPaneOpenOnPause: false,
});
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;