summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/utils/context.js
blob: 8c7311008dd3a5bd6e538786a98f58ee82ea3350 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/* 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/>. */

import {
  getThreadContext,
  getSelectedFrame,
  getCurrentThread,
  hasSource,
  hasSourceActor,
  getCurrentlyFetchedTopFrame,
  hasFrame,
} from "../selectors/index";

// Context encapsulates the main parameters of the current redux state, which
// impact most other information tracked by the debugger.
//
// The main use of Context is to control when asynchronous operations are
// allowed to make changes to the program state. Such operations might be
// invalidated as the state changes from the time the operation was originally
// initiated. For example, operations on pause state might still continue even
// after the thread unpauses.
//
// The methods below can be used to compare an old context with the current one
// and see if the operation is now invalid and should be abandoned. Actions can
// also include a 'cx' Context property, which will be checked by the context
// middleware. If the action fails validateContextAction() then it will not be
// dispatched.
//
// Context can additionally be used as a shortcut to access the main properties
// of the pause state.

// A normal Context is invalidated if the target navigates.

// A ThreadContext is invalidated if the target navigates, or if the current
// thread changes, pauses, or resumes.

export class ContextError extends Error {
  constructor(msg) {
    // Use a prefix string to help `PromiseTestUtils.allowMatchingRejectionsGlobally`
    // ignore all these exceptions as this is based on error strings.
    super(`DebuggerContextError: ${msg}`);
  }
}

export function validateNavigateContext(state, cx) {
  const newcx = getThreadContext(state);

  if (newcx.navigateCounter != cx.navigateCounter) {
    throw new ContextError("Page has navigated");
  }
}

export function validateThreadContext(state, cx) {
  const newcx = getThreadContext(state);

  if (cx.thread != newcx.thread) {
    throw new ContextError("Current thread has changed");
  }

  if (cx.pauseCounter != newcx.pauseCounter) {
    throw new ContextError("Current thread has paused or resumed");
  }
}

export function validateContext(state, cx) {
  validateNavigateContext(state, cx);

  if ("thread" in cx) {
    validateThreadContext(state, cx);
  }
}

export function validateSelectedFrame(state, selectedFrame) {
  const newThread = getCurrentThread(state);
  if (selectedFrame.thread != newThread) {
    throw new ContextError("Selected thread has changed");
  }

  const newSelectedFrame = getSelectedFrame(state, newThread);
  // Compare frame's IDs as frame objects are cloned during mapping
  if (selectedFrame.id != newSelectedFrame?.id) {
    throw new ContextError("Selected frame changed");
  }
}

export function validateBreakpoint(state, breakpoint) {
  // XHR breakpoint don't use any location and are always valid
  if (!breakpoint.location) {
    return;
  }

  if (!hasSource(state, breakpoint.location.source.id)) {
    throw new ContextError(
      `Breakpoint's location is obsolete (source '${breakpoint.location.source.id}' no longer exists)`
    );
  }
  if (!hasSource(state, breakpoint.generatedLocation.source.id)) {
    throw new ContextError(
      `Breakpoint's generated location is obsolete (source '${breakpoint.generatedLocation.source.id}' no longer exists)`
    );
  }
}

export function validateSource(state, source) {
  if (!hasSource(state, source.id)) {
    throw new ContextError(
      `Obsolete source (source '${source.id}' no longer exists)`
    );
  }
}

export function validateSourceActor(state, sourceActor) {
  if (!hasSourceActor(state, sourceActor.id)) {
    throw new ContextError(
      `Obsolete source actor (source '${sourceActor.id}' no longer exists)`
    );
  }
}

export function validateThreadFrames(state, thread, frames) {
  const newThread = getCurrentThread(state);
  if (thread != newThread) {
    throw new ContextError("Selected thread has changed");
  }
  const newTopFrame = getCurrentlyFetchedTopFrame(state, newThread);
  if (newTopFrame?.id != frames[0].id) {
    throw new ContextError("Thread moved to another location");
  }
}

export function validateFrame(state, frame) {
  if (!hasFrame(state, frame)) {
    throw new ContextError(
      `Obsolete frame (frame '${frame.id}' no longer exists)`
    );
  }
}

export function isValidThreadContext(state, cx) {
  const newcx = getThreadContext(state);
  return cx.thread == newcx.thread && cx.pauseCounter == newcx.pauseCounter;
}