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/. */
"use strict";
const {
processNetworkUpdates,
} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
const {
ADD_REQUEST,
SET_EVENT_STREAM_FLAG,
CLEAR_REQUESTS,
CLONE_REQUEST,
CLONE_SELECTED_REQUEST,
OPEN_NETWORK_DETAILS,
REMOVE_SELECTED_CUSTOM_REQUEST,
RIGHT_CLICK_REQUEST,
SELECT_REQUEST,
PRESELECT_REQUEST,
SEND_CUSTOM_REQUEST,
SET_RECORDING_STATE,
UPDATE_REQUEST,
} = require("resource://devtools/client/netmonitor/src/constants.js");
/**
* This structure stores list of all HTTP requests received
* from the backend. It's using plain JS structures to store
* data instead of ImmutableJS, which is performance expensive.
*/
function Requests() {
return {
// Map with all requests (key = actor ID, value = request object)
requests: [],
// Selected request ID
selectedId: null,
// Right click request represents the last request that was clicked
clickedRequestId: null,
// @backward-compact { version 85 } The preselectedId can either be
// the actor id on old servers, or the resourceId on new ones.
preselectedId: null,
// True if the monitor is recording HTTP traffic
recording: true,
// Auxiliary fields to hold requests stats
firstStartedMs: +Infinity,
lastEndedMs: -Infinity,
};
}
/**
* This reducer is responsible for maintaining list of request
* within the Network panel.
*/
function requestsReducer(state = Requests(), action) {
switch (action.type) {
// Appending new request into the list/map.
case ADD_REQUEST: {
return addRequest(state, action);
}
// Update an existing request (with received data).
case UPDATE_REQUEST: {
return updateRequest(state, action);
}
// Add isEventStream flag to a request.
case SET_EVENT_STREAM_FLAG: {
return setEventStreamFlag(state, action);
}
// Remove all requests in the list. Create fresh new state
// object, but keep value of the `recording` field.
case CLEAR_REQUESTS: {
return {
...Requests(),
recording: state.recording,
};
}
// Select specific request.
case SELECT_REQUEST: {
return {
...state,
clickedRequestId: action.id,
selectedId: action.id,
};
}
// Clone selected request for re-send.
case CLONE_REQUEST: {
return cloneRequest(state, action.id);
}
case CLONE_SELECTED_REQUEST: {
return cloneRequest(state, state.selectedId);
}
case RIGHT_CLICK_REQUEST: {
return {
...state,
clickedRequestId: action.id,
};
}
case PRESELECT_REQUEST: {
return {
...state,
preselectedId: action.id,
};
}
// Removing temporary cloned request (created for re-send, but canceled).
case REMOVE_SELECTED_CUSTOM_REQUEST: {
return closeCustomRequest(state);
}
// Re-sending an existing request.
case SEND_CUSTOM_REQUEST: {
// When a new request with a given id is added in future, select it immediately.
// where we know in advance the ID of the request, at a time when it
// wasn't sent yet.
return closeCustomRequest({ ...state, preselectedId: action.id });
}
// Pause/resume button clicked.
case SET_RECORDING_STATE: {
return {
...state,
recording: action.recording,
};
}
// Side bar with request details opened.
case OPEN_NETWORK_DETAILS: {
const nextState = { ...state };
if (!action.open) {
nextState.selectedId = null;
return nextState;
}
if (!state.selectedId && action.defaultSelectedId) {
nextState.selectedId = action.defaultSelectedId;
return nextState;
}
return state;
}
default:
return state;
}
}
// Helpers
function addRequest(state, action) {
const nextState = { ...state };
// The target front is not used and cannot be serialized by redux
// eslint-disable-next-line no-unused-vars
const { targetFront, ...requestData } = action.data;
const newRequest = {
id: action.id,
...requestData,
};
nextState.requests = [...state.requests, newRequest];
// Update the started/ended timestamps.
const { startedMs } = action.data;
if (startedMs < state.firstStartedMs) {
nextState.firstStartedMs = startedMs;
}
if (startedMs > state.lastEndedMs) {
nextState.lastEndedMs = startedMs;
}
// Select the request if it was preselected and there is no other selection.
if (state.preselectedId) {
if (state.preselectedId === action.id) {
nextState.selectedId = state.selectedId || state.preselectedId;
}
// @backward-compact { version 85 } The preselectedId can be resourceId
// instead of actor id when a custom request is created, and could not be
// selected immediately because it was not yet in the request map.
else if (state.preselectedId === newRequest.resourceId) {
nextState.selectedId = action.id;
}
nextState.preselectedId = null;
}
return nextState;
}
function updateRequest(state, action) {
const { requests, lastEndedMs } = state;
const { id } = action;
const index = requests.findIndex(needle => needle.id === id);
if (index === -1) {
return state;
}
const request = requests[index];
const nextRequest = {
...request,
...processNetworkUpdates(action.data),
};
const requestEndTime =
nextRequest.startedMs +
(nextRequest.eventTimings ? nextRequest.eventTimings.totalTime : 0);
const nextRequests = [...requests];
nextRequests[index] = nextRequest;
return {
...state,
requests: nextRequests,
lastEndedMs: requestEndTime > lastEndedMs ? requestEndTime : lastEndedMs,
};
}
function setEventStreamFlag(state, action) {
const { requests } = state;
const { id } = action;
const index = requests.findIndex(needle => needle.id === id);
if (index === -1) {
return state;
}
const request = requests[index];
const nextRequest = {
...request,
isEventStream: true,
};
const nextRequests = [...requests];
nextRequests[index] = nextRequest;
return {
...state,
requests: nextRequests,
};
}
function cloneRequest(state, id) {
const { requests } = state;
if (!id) {
return state;
}
const clonedRequest = requests.find(needle => needle.id === id);
if (!clonedRequest) {
return state;
}
const newRequest = {
id: clonedRequest.id + "-clone",
method: clonedRequest.method,
cause: clonedRequest.cause,
url: clonedRequest.url,
urlDetails: clonedRequest.urlDetails,
requestHeaders: clonedRequest.requestHeaders,
requestPostData: clonedRequest.requestPostData,
requestPostDataAvailable: clonedRequest.requestPostDataAvailable,
requestHeadersAvailable: clonedRequest.requestHeadersAvailable,
isCustom: true,
};
return {
...state,
requests: [...requests, newRequest],
selectedId: newRequest.id,
preselectedId: id,
};
}
/**
* Remove the currently selected custom request.
*/
function closeCustomRequest(state) {
const { requests, selectedId, preselectedId } = state;
if (!selectedId) {
return state;
}
// Find the cloned requests to be removed
const removedRequest = requests.find(needle => needle.id === selectedId);
// If the custom request is already in the Map, select it immediately,
// and reset `preselectedId` attribute.
// @backward-compact { version 85 } The preselectId can also be a resourceId
// or an actor id.
const customRequest = requests.find(
needle => needle.id === preselectedId || needle.resourceId === preselectedId
);
const hasPreselectedId = preselectedId && customRequest;
return {
...state,
// Only custom requests can be removed
[removedRequest?.isCustom && "requests"]: requests.filter(
item => item.id !== selectedId
),
preselectedId: hasPreselectedId ? null : preselectedId,
selectedId: hasPreselectedId ? customRequest.id : null,
};
}
module.exports = {
Requests,
requestsReducer,
};