SessionData.sys.mjs (16023B)
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 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 ContextDescriptorType: 9 "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs", 10 Log: "chrome://remote/content/shared/Log.sys.mjs", 11 RootMessageHandler: 12 "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs", 13 WindowGlobalMessageHandler: 14 "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs", 15 }); 16 17 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); 18 19 /** 20 * @typedef {string} SessionDataCategory 21 */ 22 23 /** 24 * Enum of session data categories. 25 * 26 * @readonly 27 * @enum {SessionDataCategory} 28 */ 29 export const SessionDataCategory = { 30 Event: "event", 31 PreloadScript: "preload-script", 32 }; 33 34 /** 35 * @typedef {string} SessionDataMethod 36 */ 37 38 /** 39 * Enum of session data methods. 40 * 41 * @readonly 42 * @enum {SessionDataMethod} 43 */ 44 export const SessionDataMethod = { 45 Add: "add", 46 Remove: "remove", 47 }; 48 49 export const SESSION_DATA_SHARED_DATA_KEY = "MessageHandlerSessionData"; 50 51 // This is a map from session id to session data, which will be persisted and 52 // propagated to all processes using Services' sharedData. 53 // We have to store this as a unique object under a unique shared data key 54 // because new MessageHandlers in other processes will need to access this data 55 // without any notion of a specific session. 56 // This is a singleton. 57 const sessionDataMap = new Map(); 58 59 /** 60 * @typedef {object} SessionDataItem 61 * @property {string} moduleName 62 * The name of the module responsible for this data item. 63 * @property {SessionDataCategory} category 64 * The category of data. The supported categories depend on the module. 65 * @property {(string|number|boolean)} value 66 * Value of the session data item. 67 * @property {ContextDescriptor} contextDescriptor 68 * ContextDescriptor to which this session data applies. 69 */ 70 71 /** 72 * @typedef SessionDataItemUpdate 73 * @property {SessionDataMethod} method 74 * The way sessionData is updated. 75 * @property {string} moduleName 76 * The name of the module responsible for this data item. 77 * @property {SessionDataCategory} category 78 * The category of data. The supported categories depend on the module. 79 * @property {Array<(string|number|boolean)>} values 80 * Values of the session data item update. 81 * @property {ContextDescriptor} contextDescriptor 82 * ContextDescriptor to which this session data applies. 83 */ 84 85 /** 86 * SessionData provides APIs to read and write the session data for a specific 87 * ROOT message handler. It holds the session data as a property and acts as the 88 * source of truth for this session data. 89 * 90 * The session data of a given message handler network should contain all the 91 * information that might be needed to setup new contexts, for instance a list 92 * of subscribed events, a list of breakpoints etc. 93 * 94 * The actual session data is an array of SessionDataItems. Example below: 95 * ``` 96 * data: [ 97 * { 98 * moduleName: "log", 99 * category: "event", 100 * value: "log.entryAdded", 101 * contextDescriptor: { type: "all" } 102 * }, 103 * { 104 * moduleName: "browsingContext", 105 * category: "event", 106 * value: "browsingContext.contextCreated", 107 * contextDescriptor: { type: "browser-element", id: "7"} 108 * }, 109 * { 110 * moduleName: "browsingContext", 111 * category: "event", 112 * value: "browsingContext.contextCreated", 113 * contextDescriptor: { type: "browser-element", id: "12"} 114 * }, 115 * ] 116 * ``` 117 * 118 * The session data will be persisted using Services.ppmm.sharedData, so that 119 * new contexts living in different processes can also access the information 120 * during their startup. 121 * 122 * This class should only be used from a ROOT MessageHandler, or from modules 123 * owned by a ROOT MessageHandler. Other MessageHandlers should rely on 124 * SessionDataReader's readSessionData to get read-only access to session data. 125 * 126 */ 127 export class SessionData { 128 #data; 129 #messageHandler; 130 131 constructor(messageHandler) { 132 if (messageHandler.constructor.type != lazy.RootMessageHandler.type) { 133 throw new Error( 134 "SessionData should only be used from a ROOT MessageHandler" 135 ); 136 } 137 138 this.#messageHandler = messageHandler; 139 140 /* 141 * The actual data for this session. This is an array of SessionDataItems. 142 */ 143 this.#data = []; 144 } 145 146 destroy() { 147 // Update the sessionDataMap singleton. 148 sessionDataMap.delete(this.#messageHandler.sessionId); 149 150 // Update sharedData and flush to force consistency. 151 Services.ppmm.sharedData.set(SESSION_DATA_SHARED_DATA_KEY, sessionDataMap); 152 Services.ppmm.sharedData.flush(); 153 } 154 155 /** 156 * Update session data items of a given module, category and 157 * contextDescriptor. 158 * 159 * A SessionDataItem will be added or removed for each value of each update 160 * in the provided array. 161 * 162 * Attempting to add a duplicate SessionDataItem or to remove an unknown 163 * SessionDataItem will be silently skipped (no-op). 164 * 165 * The data will be persisted across processes at the end of this method. 166 * 167 * @param {Array<SessionDataItemUpdate>} sessionDataItemUpdates 168 * Array of session data item updates. 169 * 170 * @returns {Array<SessionDataItemUpdate>} 171 * The subset of session data item updates which want to be applied. 172 */ 173 applySessionData(sessionDataItemUpdates = []) { 174 // The subset of session data item updates, which are cleaned up from 175 // duplicates and unknown items. 176 const updates = []; 177 for (const sessionDataItemUpdate of sessionDataItemUpdates) { 178 const { category, contextDescriptor, method, moduleName, values } = 179 sessionDataItemUpdate; 180 const updatedValues = []; 181 for (const value of values) { 182 const item = { moduleName, category, contextDescriptor, value }; 183 184 if (method === SessionDataMethod.Add) { 185 const hasItem = this.#findIndex(item) != -1; 186 187 if (!hasItem) { 188 this.#data.push(item); 189 updatedValues.push(value); 190 } else { 191 lazy.logger.warn( 192 `Duplicated session data item was not added: ${JSON.stringify( 193 item 194 )}` 195 ); 196 } 197 } else { 198 const itemIndex = this.#findIndex(item); 199 200 if (itemIndex != -1) { 201 // The item was found in the session data, remove it. 202 this.#data.splice(itemIndex, 1); 203 updatedValues.push(value); 204 } else { 205 lazy.logger.warn( 206 `Missing session data item was not removed: ${JSON.stringify( 207 item 208 )}` 209 ); 210 } 211 } 212 } 213 214 if (updatedValues.length) { 215 updates.push({ 216 ...sessionDataItemUpdate, 217 values: updatedValues, 218 }); 219 } 220 } 221 // Persist the sessionDataMap. 222 this.#persist(); 223 224 return updates; 225 } 226 227 /** 228 * Generate session data item update (remove existing items and add new) 229 * for a given module, category, contextDescriptor and new value. 230 * 231 * @param {string} moduleName 232 * The name of the module. 233 * @param {string} category 234 * The session data category. 235 * @param {ContextDescriptor=} contextDescriptor 236 * The context descriptor. 237 * @param {boolean} onlyRemove 238 * If it's set to "true" do not add a new session data item. 239 * @param {(string|number|boolean)} newValue 240 * The new value of the session data item. 241 * 242 * @returns {Array<SessionDataItemUpdate>} sessionDataItemUpdates 243 * Array of session data item updates. 244 */ 245 generateSessionDataItemUpdate( 246 moduleName, 247 category, 248 contextDescriptor, 249 onlyRemove, 250 newValue 251 ) { 252 const sessionDataUpdate = []; 253 const sessionData = this.getSessionData( 254 moduleName, 255 category, 256 contextDescriptor 257 ); 258 259 if (sessionData.length) { 260 for (const item of sessionData) { 261 sessionDataUpdate.push({ 262 category, 263 moduleName, 264 values: [item.value], 265 contextDescriptor, 266 method: SessionDataMethod.Remove, 267 }); 268 } 269 } 270 271 if (!onlyRemove) { 272 sessionDataUpdate.push({ 273 category, 274 moduleName, 275 values: [newValue], 276 contextDescriptor, 277 method: SessionDataMethod.Add, 278 }); 279 } 280 281 return sessionDataUpdate; 282 } 283 284 /** 285 * Retrieve the SessionDataItems for a given module and type. 286 * 287 * @param {string} moduleName 288 * The name of the module responsible for this data item. 289 * @param {string=} category 290 * Optional session data category. 291 * @param {ContextDescriptor=} contextDescriptor 292 * Optional context descriptor, to retrieve only session data items added 293 * for a specific context descriptor. 294 * @returns {Array<SessionDataItem>} 295 * Array of SessionDataItems for the provided module and type. 296 */ 297 getSessionData(moduleName, category, contextDescriptor) { 298 return this.#data.filter(item => 299 this.#matchItem(item, moduleName, category, contextDescriptor) 300 ); 301 } 302 303 /** 304 * Retrieve the SessionDataItems for a given module, type and 305 * with context descriptors which would match the provided 306 * browsing context. 307 * 308 * @param {string} moduleName 309 * The name of the module. 310 * @param {string} category 311 * The session data category. 312 * @param {BrowsingContext} context 313 * The browsing context. 314 * @returns {Array<SessionDataItem>} 315 * Array of SessionDataItems for the provided module, type 316 * and browsing context. 317 */ 318 getSessionDataForContext(moduleName, category, context) { 319 return this.#data.filter( 320 item => 321 this.#matchItem(item, moduleName, category) && 322 this.#messageHandler.contextMatchesDescriptor( 323 context, 324 item.contextDescriptor 325 ) 326 ); 327 } 328 329 /** 330 * Checks if any session data exists for a provided module name. 331 * 332 * @param {string} moduleName 333 * The name of the module responsible for this data item. 334 * @param {string=} category 335 * Optional session data category. 336 * @param {ContextDescriptor=} contextDescriptor 337 * Optional context descriptor, to retrieve only session data items added 338 * for a specific context descriptor. 339 * @returns {boolean} 340 * Returns `true` if matching session data is found, `false` otherwise. 341 */ 342 hasSessionData(moduleName, category, contextDescriptor) { 343 return this.#data.some(item => 344 this.#matchItem(item, moduleName, category, contextDescriptor) 345 ); 346 } 347 348 /** 349 * Update session data items of a given module, category and 350 * contextDescriptor and propagate the information 351 * via a command to existing MessageHandlers. 352 * 353 * @param {Array<SessionDataItemUpdate>} sessionDataItemUpdates 354 * Array of session data item updates. 355 */ 356 async updateSessionData(sessionDataItemUpdates = []) { 357 const updates = this.applySessionData(sessionDataItemUpdates); 358 359 if (!updates.length) { 360 // Avoid unnecessary broadcast if no items were updated. 361 return; 362 } 363 364 // Create a Map with the structure moduleName -> category -> list of descriptors. 365 const structuredUpdates = new Map(); 366 for (const { moduleName, category, contextDescriptor } of updates) { 367 if (!structuredUpdates.has(moduleName)) { 368 structuredUpdates.set(moduleName, new Map()); 369 } 370 if (!structuredUpdates.get(moduleName).has(category)) { 371 structuredUpdates.get(moduleName).set(category, new Set()); 372 } 373 const descriptors = structuredUpdates.get(moduleName).get(category); 374 // If there is at least one update for all contexts, 375 // keep only this descriptor in the list of descriptors 376 if (contextDescriptor.type === lazy.ContextDescriptorType.All) { 377 structuredUpdates 378 .get(moduleName) 379 .set(category, new Set([contextDescriptor])); 380 } 381 // Add an individual descriptor if there is no descriptor for all contexts. 382 else if ( 383 descriptors.size !== 1 || 384 Array.from(descriptors)[0]?.type !== lazy.ContextDescriptorType.All 385 ) { 386 descriptors.add(contextDescriptor); 387 } 388 } 389 390 const rootDestination = { 391 type: lazy.RootMessageHandler.type, 392 }; 393 const sessionDataPromises = []; 394 395 for (const [moduleName, categories] of structuredUpdates.entries()) { 396 for (const [category, contextDescriptors] of categories.entries()) { 397 // Find sessionData for the category and the moduleName. 398 const relevantSessionData = this.#data.filter( 399 item => item.category == category && item.moduleName === moduleName 400 ); 401 for (const contextDescriptor of contextDescriptors.values()) { 402 const windowGlobalDestination = { 403 type: lazy.WindowGlobalMessageHandler.type, 404 contextDescriptor, 405 }; 406 407 for (const destination of [ 408 windowGlobalDestination, 409 rootDestination, 410 ]) { 411 // Only apply session data if the module is present for the destination. 412 if ( 413 this.#messageHandler.supportsCommand( 414 moduleName, 415 "_applySessionData", 416 destination 417 ) 418 ) { 419 sessionDataPromises.push( 420 this.#messageHandler 421 .handleCommand({ 422 moduleName, 423 commandName: "_applySessionData", 424 params: { 425 sessionData: relevantSessionData, 426 category, 427 contextDescriptor, 428 initial: false, 429 }, 430 destination, 431 }) 432 ?.catch(reason => 433 lazy.logger.error( 434 `_applySessionData for module: ${moduleName} failed, reason: ${reason}` 435 ) 436 ) 437 ); 438 } 439 } 440 } 441 } 442 } 443 444 await Promise.allSettled(sessionDataPromises); 445 } 446 447 #isSameItem(item1, item2) { 448 const descriptor1 = item1.contextDescriptor; 449 const descriptor2 = item2.contextDescriptor; 450 451 return ( 452 item1.moduleName === item2.moduleName && 453 item1.category === item2.category && 454 this.#isSameContextDescriptor(descriptor1, descriptor2) && 455 this.#isSameValue(item1.category, item1.value, item2.value) 456 ); 457 } 458 459 #isSameContextDescriptor(contextDescriptor1, contextDescriptor2) { 460 if (contextDescriptor1.type === lazy.ContextDescriptorType.All) { 461 // Ignore the id for type "all" since we made the id optional for this type. 462 return contextDescriptor1.type === contextDescriptor2.type; 463 } 464 465 return ( 466 contextDescriptor1.type === contextDescriptor2.type && 467 contextDescriptor1.id === contextDescriptor2.id 468 ); 469 } 470 471 #isSameValue(category, value1, value2) { 472 if (category === SessionDataCategory.PreloadScript) { 473 return value1.script === value2.script; 474 } 475 476 return value1 === value2; 477 } 478 479 #findIndex(item) { 480 return this.#data.findIndex(_item => this.#isSameItem(item, _item)); 481 } 482 483 #matchItem(item, moduleName, category, contextDescriptor) { 484 return ( 485 item.moduleName === moduleName && 486 (!category || item.category === category) && 487 (!contextDescriptor || 488 this.#isSameContextDescriptor( 489 item.contextDescriptor, 490 contextDescriptor 491 )) 492 ); 493 } 494 495 #persist() { 496 // Update the sessionDataMap singleton. 497 sessionDataMap.set(this.#messageHandler.sessionId, this.#data); 498 499 // Update sharedData and flush to force consistency. 500 Services.ppmm.sharedData.set(SESSION_DATA_SHARED_DATA_KEY, sessionDataMap); 501 Services.ppmm.sharedData.flush(); 502 } 503 }