requests.js (8787B)
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 "use strict"; 6 7 const { 8 processNetworkUpdates, 9 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); 10 const { 11 ADD_REQUEST, 12 SET_EVENT_STREAM_FLAG, 13 CLEAR_REQUESTS, 14 CLONE_REQUEST, 15 CLONE_SELECTED_REQUEST, 16 OPEN_NETWORK_DETAILS, 17 REMOVE_SELECTED_CUSTOM_REQUEST, 18 RIGHT_CLICK_REQUEST, 19 SELECT_REQUEST, 20 PRESELECT_REQUEST, 21 SEND_CUSTOM_REQUEST, 22 SET_RECORDING_STATE, 23 UPDATE_REQUEST, 24 } = require("resource://devtools/client/netmonitor/src/constants.js"); 25 26 /** 27 * This structure stores list of all HTTP requests received 28 * from the backend. It's using plain JS structures to store 29 * data instead of ImmutableJS, which is performance expensive. 30 */ 31 function Requests() { 32 return { 33 // Map with all requests (key = actor ID, value = request object) 34 requests: [], 35 // Selected request ID 36 selectedId: null, 37 // Right click request represents the last request that was clicked 38 clickedRequestId: null, 39 // @backward-compact { version 85 } The preselectedId can either be 40 // the actor id on old servers, or the resourceId on new ones. 41 preselectedId: null, 42 // True if the monitor is recording HTTP traffic 43 recording: true, 44 // Auxiliary fields to hold requests stats 45 firstStartedMs: +Infinity, 46 lastEndedMs: -Infinity, 47 }; 48 } 49 50 /** 51 * This reducer is responsible for maintaining list of request 52 * within the Network panel. 53 */ 54 // eslint-disable-next-line complexity 55 function requestsReducer(state = Requests(), action) { 56 switch (action.type) { 57 // Appending new request into the list/map. 58 case ADD_REQUEST: { 59 return addRequest(state, action); 60 } 61 62 // Update an existing request (with received data). 63 case UPDATE_REQUEST: { 64 return updateRequest(state, action); 65 } 66 67 // Add isEventStream flag to a request. 68 case SET_EVENT_STREAM_FLAG: { 69 return setEventStreamFlag(state, action); 70 } 71 72 // Remove all requests in the list. Create fresh new state 73 // object, but keep value of the `recording` field. 74 case CLEAR_REQUESTS: { 75 return { 76 ...Requests(), 77 recording: state.recording, 78 }; 79 } 80 81 // Select specific request. 82 case SELECT_REQUEST: { 83 if ( 84 state.clickedRequestId == action.id && 85 state.selectedId == action.id 86 ) { 87 return state; 88 } 89 return { 90 ...state, 91 clickedRequestId: action.id, 92 selectedId: action.id, 93 }; 94 } 95 96 // Clone selected request for re-send. 97 case CLONE_REQUEST: { 98 return cloneRequest(state, action.id); 99 } 100 101 case CLONE_SELECTED_REQUEST: { 102 return cloneRequest(state, state.selectedId); 103 } 104 105 case RIGHT_CLICK_REQUEST: { 106 if (state.clickedRequestId == action.id) { 107 return state; 108 } 109 return { 110 ...state, 111 clickedRequestId: action.id, 112 }; 113 } 114 115 case PRESELECT_REQUEST: { 116 if (state.preselectedId == action.id) { 117 return state; 118 } 119 return { 120 ...state, 121 preselectedId: action.id, 122 }; 123 } 124 125 // Removing temporary cloned request (created for re-send, but canceled). 126 case REMOVE_SELECTED_CUSTOM_REQUEST: { 127 return closeCustomRequest(state); 128 } 129 130 // Re-sending an existing request. 131 case SEND_CUSTOM_REQUEST: { 132 // When a new request with a given id is added in future, select it immediately. 133 // where we know in advance the ID of the request, at a time when it 134 // wasn't sent yet. 135 return closeCustomRequest({ ...state, preselectedId: action.id }); 136 } 137 138 // Pause/resume button clicked. 139 case SET_RECORDING_STATE: { 140 if (state.recording == action.recording) { 141 return state; 142 } 143 return { 144 ...state, 145 recording: action.recording, 146 }; 147 } 148 149 // Side bar with request details opened. 150 case OPEN_NETWORK_DETAILS: { 151 if (!action.open) { 152 if (state.selectedId == null) { 153 return state; 154 } 155 return { 156 ...state, 157 selectedId: null, 158 }; 159 } 160 161 if (!state.selectedId && action.defaultSelectedId != state.selectedId) { 162 return { 163 ...state, 164 selectedId: action.defaultSelectedId, 165 }; 166 } 167 168 return state; 169 } 170 171 default: 172 return state; 173 } 174 } 175 176 // Helpers 177 178 function addRequest(state, action) { 179 const nextState = { ...state }; 180 // The target front is not used and cannot be serialized by redux 181 // eslint-disable-next-line no-unused-vars 182 const { targetFront, ...requestData } = action.data; 183 const newRequest = { 184 id: action.id, 185 ...requestData, 186 }; 187 188 nextState.requests = [...state.requests, newRequest]; 189 190 // Update the started/ended timestamps. 191 const { startedMs } = action.data; 192 if (startedMs < state.firstStartedMs) { 193 nextState.firstStartedMs = startedMs; 194 } 195 if (startedMs > state.lastEndedMs) { 196 nextState.lastEndedMs = startedMs; 197 } 198 199 // Select the request if it was preselected and there is no other selection. 200 if (state.preselectedId) { 201 if (state.preselectedId === action.id) { 202 nextState.selectedId = state.selectedId || state.preselectedId; 203 } 204 // @backward-compact { version 85 } The preselectedId can be resourceId 205 // instead of actor id when a custom request is created, and could not be 206 // selected immediately because it was not yet in the request map. 207 else if (state.preselectedId === newRequest.resourceId) { 208 nextState.selectedId = action.id; 209 } 210 nextState.preselectedId = null; 211 } 212 213 return nextState; 214 } 215 216 function updateRequest(state, action) { 217 const { requests, lastEndedMs } = state; 218 219 const { id } = action; 220 const index = requests.findIndex(needle => needle.id === id); 221 if (index === -1) { 222 return state; 223 } 224 const request = requests[index]; 225 226 const nextRequest = { 227 ...request, 228 ...processNetworkUpdates(action.data), 229 }; 230 const requestEndTime = 231 nextRequest.startedMs + 232 (nextRequest.eventTimings ? nextRequest.eventTimings.totalTime : 0); 233 234 const nextRequests = [...requests]; 235 nextRequests[index] = nextRequest; 236 return { 237 ...state, 238 requests: nextRequests, 239 lastEndedMs: requestEndTime > lastEndedMs ? requestEndTime : lastEndedMs, 240 }; 241 } 242 243 function setEventStreamFlag(state, action) { 244 const { requests } = state; 245 const { id } = action; 246 const index = requests.findIndex(needle => needle.id === id); 247 if (index === -1) { 248 return state; 249 } 250 251 const request = requests[index]; 252 253 const nextRequest = { 254 ...request, 255 isEventStream: true, 256 }; 257 258 const nextRequests = [...requests]; 259 nextRequests[index] = nextRequest; 260 return { 261 ...state, 262 requests: nextRequests, 263 }; 264 } 265 266 function cloneRequest(state, id) { 267 const { requests } = state; 268 269 if (!id) { 270 return state; 271 } 272 273 const clonedRequest = requests.find(needle => needle.id === id); 274 if (!clonedRequest) { 275 return state; 276 } 277 278 const newRequest = { 279 id: clonedRequest.id + "-clone", 280 method: clonedRequest.method, 281 cause: clonedRequest.cause, 282 url: clonedRequest.url, 283 urlDetails: clonedRequest.urlDetails, 284 requestHeaders: clonedRequest.requestHeaders, 285 requestPostData: clonedRequest.requestPostData, 286 requestPostDataAvailable: clonedRequest.requestPostDataAvailable, 287 requestHeadersAvailable: clonedRequest.requestHeadersAvailable, 288 isCustom: true, 289 }; 290 291 return { 292 ...state, 293 requests: [...requests, newRequest], 294 selectedId: newRequest.id, 295 preselectedId: id, 296 }; 297 } 298 299 /** 300 * Remove the currently selected custom request. 301 */ 302 function closeCustomRequest(state) { 303 const { requests, selectedId, preselectedId } = state; 304 305 if (!selectedId) { 306 return state; 307 } 308 309 // Find the cloned requests to be removed 310 const removedRequest = requests.find(needle => needle.id === selectedId); 311 312 // If the custom request is already in the Map, select it immediately, 313 // and reset `preselectedId` attribute. 314 // @backward-compact { version 85 } The preselectId can also be a resourceId 315 // or an actor id. 316 const customRequest = requests.find( 317 needle => needle.id === preselectedId || needle.resourceId === preselectedId 318 ); 319 const hasPreselectedId = preselectedId && customRequest; 320 321 return { 322 ...state, 323 // Only custom requests can be removed 324 [removedRequest?.isCustom && "requests"]: requests.filter( 325 item => item.id !== selectedId 326 ), 327 preselectedId: hasPreselectedId ? null : preselectedId, 328 selectedId: hasPreselectedId ? customRequest.id : null, 329 }; 330 } 331 332 module.exports = { 333 Requests, 334 requestsReducer, 335 };