messages.js (8155B)
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 SELECT_REQUEST, 9 MSG_ADD, 10 MSG_SELECT, 11 MSG_OPEN_DETAILS, 12 MSG_CLEAR, 13 MSG_TOGGLE_FILTER_TYPE, 14 MSG_TOGGLE_CONTROL, 15 MSG_SET_FILTER_TEXT, 16 MSG_TOGGLE_COLUMN, 17 MSG_RESET_COLUMNS, 18 MSG_CLOSE_CONNECTION, 19 CHANNEL_TYPE, 20 SET_EVENT_STREAM_FLAG, 21 } = require("resource://devtools/client/netmonitor/src/constants.js"); 22 23 /** 24 * The default column states for the MessageListItem component. 25 */ 26 const defaultColumnsState = { 27 data: true, 28 size: false, 29 time: true, 30 }; 31 32 const defaultWSColumnsState = { 33 ...defaultColumnsState, 34 opCode: false, 35 maskBit: false, 36 finBit: false, 37 }; 38 39 const defaultSSEColumnsState = { 40 ...defaultColumnsState, 41 eventName: false, 42 lastEventId: false, 43 retry: false, 44 }; 45 46 /** 47 * Returns a new object of default cols. 48 */ 49 function getMessageDefaultColumnsState(channelType) { 50 let columnsState = defaultColumnsState; 51 const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE; 52 53 if (channelType === WEB_SOCKET) { 54 columnsState = defaultWSColumnsState; 55 } else if (channelType === EVENT_STREAM) { 56 columnsState = defaultSSEColumnsState; 57 } 58 59 return Object.assign({}, columnsState); 60 } 61 62 /** 63 * This structure stores list of all WebSocket and EventSource messages received 64 * from the backend. 65 */ 66 function Messages(initialState = {}) { 67 const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE; 68 69 return { 70 // Map with all requests (key = resourceId, value = array of message objects) 71 messages: new Map(), 72 messageFilterText: "", 73 // Default filter type is "all", 74 messageFilterType: "all", 75 showControlFrames: false, 76 selectedMessage: null, 77 messageDetailsOpen: false, 78 currentChannelId: null, 79 currentChannelType: null, 80 currentRequestId: null, 81 closedConnections: new Map(), 82 columns: null, 83 sseColumns: getMessageDefaultColumnsState(EVENT_STREAM), 84 wsColumns: getMessageDefaultColumnsState(WEB_SOCKET), 85 ...initialState, 86 }; 87 } 88 89 /** 90 * When a network request is selected, 91 * set the current resourceId affiliated with the connection. 92 */ 93 function setCurrentChannel(state, action) { 94 if (!action.request) { 95 return state; 96 } 97 98 const { id, cause, resourceId, isEventStream } = action.request; 99 const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE; 100 let currentChannelType = null; 101 let columnsKey = "columns"; 102 if (cause.type === "websocket") { 103 currentChannelType = WEB_SOCKET; 104 columnsKey = "wsColumns"; 105 } else if (isEventStream) { 106 currentChannelType = EVENT_STREAM; 107 columnsKey = "sseColumns"; 108 } 109 110 return { 111 ...state, 112 columns: 113 currentChannelType === state.currentChannelType 114 ? { ...state.columns } 115 : { ...state[columnsKey] }, 116 currentChannelId: resourceId, 117 currentChannelType, 118 currentRequestId: id, 119 // Default filter text is empty string for a new connection 120 messageFilterText: "", 121 }; 122 } 123 124 /** 125 * If the request is already selected and isEventStream flag 126 * is added later, we need to update currentChannelType & columns. 127 */ 128 function updateCurrentChannel(state, action) { 129 if (state.currentRequestId === action.id) { 130 const currentChannelType = CHANNEL_TYPE.EVENT_STREAM; 131 return { 132 ...state, 133 columns: { ...state.sseColumns }, 134 currentChannelType, 135 }; 136 } 137 return state; 138 } 139 140 /** 141 * Appending new message into the map. 142 */ 143 function addMessage(state, action) { 144 const { httpChannelId } = action; 145 const nextState = { ...state }; 146 147 const newMessage = { 148 httpChannelId, 149 ...action.data, 150 }; 151 152 nextState.messages = mapSet( 153 nextState.messages, 154 newMessage.httpChannelId, 155 newMessage 156 ); 157 158 return nextState; 159 } 160 161 /** 162 * Select specific message. 163 */ 164 function selectMessage(state, action) { 165 if ( 166 state.selectedMessage == action.message && 167 state.messageDetailsOpen == action.open 168 ) { 169 return state; 170 } 171 return { 172 ...state, 173 selectedMessage: action.message, 174 messageDetailsOpen: action.open, 175 }; 176 } 177 178 /** 179 * Shows/Hides the MessagePayload component. 180 */ 181 function openMessageDetails(state, action) { 182 if (state.messageDetailsOpen == action.open) { 183 return state; 184 } 185 return { 186 ...state, 187 messageDetailsOpen: action.open, 188 }; 189 } 190 191 /** 192 * Clear messages of the request from the state. 193 */ 194 function clearMessages(state) { 195 const nextState = { ...state }; 196 const defaultState = Messages(); 197 nextState.messages = new Map(state.messages); 198 nextState.messages.delete(nextState.currentChannelId); 199 200 // Reset fields to default state. 201 nextState.selectedMessage = defaultState.selectedMessage; 202 nextState.messageDetailsOpen = defaultState.messageDetailsOpen; 203 204 return nextState; 205 } 206 207 /** 208 * Toggle the message filter type of the connection. 209 */ 210 function toggleMessageFilterType(state, action) { 211 if (state.messageFilterType == action.filter) { 212 return state; 213 } 214 return { 215 ...state, 216 messageFilterType: action.filter, 217 }; 218 } 219 220 /** 221 * Toggle control frames for the WebSocket connection. 222 */ 223 function toggleControlFrames(state) { 224 return { 225 ...state, 226 showControlFrames: !state.showControlFrames, 227 }; 228 } 229 230 /** 231 * Set the filter text of the current channelId. 232 */ 233 function setMessageFilterText(state, action) { 234 if (state.messageFilterText == action.text) { 235 return state; 236 } 237 return { 238 ...state, 239 messageFilterText: action.text, 240 }; 241 } 242 243 /** 244 * Toggle the user specified column view state. 245 */ 246 function toggleColumn(state, action) { 247 const { column } = action; 248 let columnsKey = null; 249 if (state.currentChannelType === CHANNEL_TYPE.WEB_SOCKET) { 250 columnsKey = "wsColumns"; 251 } else { 252 columnsKey = "sseColumns"; 253 } 254 const newColumnsState = { 255 ...state[columnsKey], 256 [column]: !state[columnsKey][column], 257 }; 258 return { 259 ...state, 260 columns: newColumnsState, 261 [columnsKey]: newColumnsState, 262 }; 263 } 264 265 /** 266 * Reset back to default columns view state. 267 */ 268 function resetColumns(state) { 269 let columnsKey = null; 270 if (state.currentChannelType === CHANNEL_TYPE.WEB_SOCKET) { 271 columnsKey = "wsColumns"; 272 } else { 273 columnsKey = "sseColumns"; 274 } 275 const newColumnsState = getMessageDefaultColumnsState( 276 state.currentChannelType 277 ); 278 return { 279 ...state, 280 [columnsKey]: newColumnsState, 281 columns: newColumnsState, 282 }; 283 } 284 285 function closeConnection(state, action) { 286 const { httpChannelId, code, reason } = action; 287 const nextState = { ...state }; 288 289 nextState.closedConnections.set(httpChannelId, { 290 code, 291 reason, 292 }); 293 294 return nextState; 295 } 296 297 /** 298 * Append new item into existing map and return new map. 299 */ 300 function mapSet(map, key, value) { 301 const newMap = new Map(map); 302 if (newMap.has(key)) { 303 const messagesArray = [...newMap.get(key)]; 304 messagesArray.push(value); 305 newMap.set(key, messagesArray); 306 return newMap; 307 } 308 return newMap.set(key, [value]); 309 } 310 311 /** 312 * This reducer is responsible for maintaining list of 313 * messages within the Network panel. 314 */ 315 function messages(state = Messages(), action) { 316 switch (action.type) { 317 case SELECT_REQUEST: 318 return setCurrentChannel(state, action); 319 case SET_EVENT_STREAM_FLAG: 320 return updateCurrentChannel(state, action); 321 case MSG_ADD: 322 return addMessage(state, action); 323 case MSG_SELECT: 324 return selectMessage(state, action); 325 case MSG_OPEN_DETAILS: 326 return openMessageDetails(state, action); 327 case MSG_CLEAR: 328 return clearMessages(state); 329 case MSG_TOGGLE_FILTER_TYPE: 330 return toggleMessageFilterType(state, action); 331 case MSG_TOGGLE_CONTROL: 332 return toggleControlFrames(state, action); 333 case MSG_SET_FILTER_TEXT: 334 return setMessageFilterText(state, action); 335 case MSG_TOGGLE_COLUMN: 336 return toggleColumn(state, action); 337 case MSG_RESET_COLUMNS: 338 return resetColumns(state); 339 case MSG_CLOSE_CONNECTION: 340 return closeConnection(state, action); 341 default: 342 return state; 343 } 344 } 345 346 module.exports = { 347 Messages, 348 messages, 349 getMessageDefaultColumnsState, 350 };