forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			392 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			392 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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/. */
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   ContextDescriptorType:
 | |
|     "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
 | |
|   Log: "chrome://remote/content/shared/Log.sys.mjs",
 | |
|   RootMessageHandler:
 | |
|     "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
 | |
|   WindowGlobalMessageHandler:
 | |
|     "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
 | |
| });
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
 | |
| 
 | |
| /**
 | |
|  * @typedef {string} SessionDataCategory
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * Enum of session data categories.
 | |
|  *
 | |
|  * @readonly
 | |
|  * @enum {SessionDataCategory}
 | |
|  */
 | |
| export const SessionDataCategory = {
 | |
|   Event: "event",
 | |
|   PreloadScript: "preload-script",
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * @typedef {string} SessionDataMethod
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * Enum of session data methods.
 | |
|  *
 | |
|  * @readonly
 | |
|  * @enum {SessionDataMethod}
 | |
|  */
 | |
| export const SessionDataMethod = {
 | |
|   Add: "add",
 | |
|   Remove: "remove",
 | |
| };
 | |
| 
 | |
| export const SESSION_DATA_SHARED_DATA_KEY = "MessageHandlerSessionData";
 | |
| 
 | |
| // This is a map from session id to session data, which will be persisted and
 | |
| // propagated to all processes using Services' sharedData.
 | |
| // We have to store this as a unique object under a unique shared data key
 | |
| // because new MessageHandlers in other processes will need to access this data
 | |
| // without any notion of a specific session.
 | |
| // This is a singleton.
 | |
| const sessionDataMap = new Map();
 | |
| 
 | |
| /**
 | |
|  * @typedef {object} SessionDataItem
 | |
|  * @property {string} moduleName
 | |
|  *     The name of the module responsible for this data item.
 | |
|  * @property {SessionDataCategory} category
 | |
|  *     The category of data. The supported categories depend on the module.
 | |
|  * @property {(string|number|boolean)} value
 | |
|  *     Value of the session data item.
 | |
|  * @property {ContextDescriptor} contextDescriptor
 | |
|  *     ContextDescriptor to which this session data applies.
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * @typedef SessionDataItemUpdate
 | |
|  * @property {SessionDataMethod} method
 | |
|  *     The way sessionData is updated.
 | |
|  * @property {string} moduleName
 | |
|  *     The name of the module responsible for this data item.
 | |
|  * @property {SessionDataCategory} category
 | |
|  *     The category of data. The supported categories depend on the module.
 | |
|  * @property {Array<(string|number|boolean)>} values
 | |
|  *     Values of the session data item update.
 | |
|  * @property {ContextDescriptor} contextDescriptor
 | |
|  *     ContextDescriptor to which this session data applies.
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * SessionData provides APIs to read and write the session data for a specific
 | |
|  * ROOT message handler. It holds the session data as a property and acts as the
 | |
|  * source of truth for this session data.
 | |
|  *
 | |
|  * The session data of a given message handler network should contain all the
 | |
|  * information that might be needed to setup new contexts, for instance a list
 | |
|  * of subscribed events, a list of breakpoints etc.
 | |
|  *
 | |
|  * The actual session data is an array of SessionDataItems. Example below:
 | |
|  * ```
 | |
|  * data: [
 | |
|  *   {
 | |
|  *     moduleName: "log",
 | |
|  *     category: "event",
 | |
|  *     value: "log.entryAdded",
 | |
|  *     contextDescriptor: { type: "all" }
 | |
|  *   },
 | |
|  *   {
 | |
|  *     moduleName: "browsingContext",
 | |
|  *     category: "event",
 | |
|  *     value: "browsingContext.contextCreated",
 | |
|  *     contextDescriptor: { type: "browser-element", id: "7"}
 | |
|  *   },
 | |
|  *   {
 | |
|  *     moduleName: "browsingContext",
 | |
|  *     category: "event",
 | |
|  *     value: "browsingContext.contextCreated",
 | |
|  *     contextDescriptor: { type: "browser-element", id: "12"}
 | |
|  *   },
 | |
|  * ]
 | |
|  * ```
 | |
|  *
 | |
|  * The session data will be persisted using Services.ppmm.sharedData, so that
 | |
|  * new contexts living in different processes can also access the information
 | |
|  * during their startup.
 | |
|  *
 | |
|  * This class should only be used from a ROOT MessageHandler, or from modules
 | |
|  * owned by a ROOT MessageHandler. Other MessageHandlers should rely on
 | |
|  * SessionDataReader's readSessionData to get read-only access to session data.
 | |
|  *
 | |
|  */
 | |
| export class SessionData {
 | |
|   constructor(messageHandler) {
 | |
|     if (messageHandler.constructor.type != lazy.RootMessageHandler.type) {
 | |
|       throw new Error(
 | |
|         "SessionData should only be used from a ROOT MessageHandler"
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this._messageHandler = messageHandler;
 | |
| 
 | |
|     /*
 | |
|      * The actual data for this session. This is an array of SessionDataItems.
 | |
|      */
 | |
|     this._data = [];
 | |
|   }
 | |
| 
 | |
|   destroy() {
 | |
|     // Update the sessionDataMap singleton.
 | |
|     sessionDataMap.delete(this._messageHandler.sessionId);
 | |
| 
 | |
|     // Update sharedData and flush to force consistency.
 | |
|     Services.ppmm.sharedData.set(SESSION_DATA_SHARED_DATA_KEY, sessionDataMap);
 | |
|     Services.ppmm.sharedData.flush();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Update session data items of a given module, category and
 | |
|    * contextDescriptor.
 | |
|    *
 | |
|    * A SessionDataItem will be added or removed for each value of each update
 | |
|    * in the provided array.
 | |
|    *
 | |
|    * Attempting to add a duplicate SessionDataItem or to remove an unknown
 | |
|    * SessionDataItem will be silently skipped (no-op).
 | |
|    *
 | |
|    * The data will be persisted across processes at the end of this method.
 | |
|    *
 | |
|    * @param {Array<SessionDataItemUpdate>} sessionDataItemUpdates
 | |
|    *     Array of session data item updates.
 | |
|    *
 | |
|    * @returns {Array<SessionDataItemUpdate>}
 | |
|    *     The subset of session data item updates which want to be applied.
 | |
|    */
 | |
|   applySessionData(sessionDataItemUpdates = []) {
 | |
|     // The subset of session data item updates, which are cleaned up from
 | |
|     // duplicates and unknown items.
 | |
|     let updates = [];
 | |
|     for (const sessionDataItemUpdate of sessionDataItemUpdates) {
 | |
|       const { category, contextDescriptor, method, moduleName, values } =
 | |
|         sessionDataItemUpdate;
 | |
|       const updatedValues = [];
 | |
|       for (const value of values) {
 | |
|         const item = { moduleName, category, contextDescriptor, value };
 | |
| 
 | |
|         if (method === SessionDataMethod.Add) {
 | |
|           const hasItem = this._findIndex(item) != -1;
 | |
| 
 | |
|           if (!hasItem) {
 | |
|             this._data.push(item);
 | |
|             updatedValues.push(value);
 | |
|           } else {
 | |
|             lazy.logger.warn(
 | |
|               `Duplicated session data item was not added: ${JSON.stringify(
 | |
|                 item
 | |
|               )}`
 | |
|             );
 | |
|           }
 | |
|         } else {
 | |
|           const itemIndex = this._findIndex(item);
 | |
| 
 | |
|           if (itemIndex != -1) {
 | |
|             // The item was found in the session data, remove it.
 | |
|             this._data.splice(itemIndex, 1);
 | |
|             updatedValues.push(value);
 | |
|           } else {
 | |
|             lazy.logger.warn(
 | |
|               `Missing session data item was not removed: ${JSON.stringify(
 | |
|                 item
 | |
|               )}`
 | |
|             );
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (updatedValues.length) {
 | |
|         updates.push({
 | |
|           ...sessionDataItemUpdate,
 | |
|           values: updatedValues,
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|     // Persist the sessionDataMap.
 | |
|     this._persist();
 | |
| 
 | |
|     return updates;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Retrieve the SessionDataItems for a given module and type.
 | |
|    *
 | |
|    * @param {string} moduleName
 | |
|    *     The name of the module responsible for this data item.
 | |
|    * @param {string} category
 | |
|    *     The session data category.
 | |
|    * @param {ContextDescriptor=} contextDescriptor
 | |
|    *     Optional context descriptor, to retrieve only session data items added
 | |
|    *     for a specific context descriptor.
 | |
|    * @returns {Array<SessionDataItem>}
 | |
|    *     Array of SessionDataItems for the provided module and type.
 | |
|    */
 | |
|   getSessionData(moduleName, category, contextDescriptor) {
 | |
|     return this._data.filter(
 | |
|       item =>
 | |
|         item.moduleName === moduleName &&
 | |
|         item.category === category &&
 | |
|         (!contextDescriptor ||
 | |
|           this._isSameContextDescriptor(
 | |
|             item.contextDescriptor,
 | |
|             contextDescriptor
 | |
|           ))
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Update session data items of a given module, category and
 | |
|    * contextDescriptor and propagate the information
 | |
|    * via a command to existing MessageHandlers.
 | |
|    *
 | |
|    * @param {Array<SessionDataItemUpdate>} sessionDataItemUpdates
 | |
|    *     Array of session data item updates.
 | |
|    */
 | |
|   async updateSessionData(sessionDataItemUpdates = []) {
 | |
|     const updates = this.applySessionData(sessionDataItemUpdates);
 | |
| 
 | |
|     if (!updates.length) {
 | |
|       // Avoid unnecessary broadcast if no items were updated.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Create a Map with the structure moduleName -> category -> list of descriptors.
 | |
|     const structuredUpdates = new Map();
 | |
|     for (const { moduleName, category, contextDescriptor } of updates) {
 | |
|       if (!structuredUpdates.has(moduleName)) {
 | |
|         structuredUpdates.set(moduleName, new Map());
 | |
|       }
 | |
|       if (!structuredUpdates.get(moduleName).has(category)) {
 | |
|         structuredUpdates.get(moduleName).set(category, new Set());
 | |
|       }
 | |
|       const descriptors = structuredUpdates.get(moduleName).get(category);
 | |
|       // If there is at least one update for all contexts,
 | |
|       // keep only this descriptor in the list of descriptors
 | |
|       if (contextDescriptor.type === lazy.ContextDescriptorType.All) {
 | |
|         structuredUpdates
 | |
|           .get(moduleName)
 | |
|           .set(category, new Set([contextDescriptor]));
 | |
|       }
 | |
|       // Add an individual descriptor if there is no descriptor for all contexts.
 | |
|       else if (
 | |
|         descriptors.size !== 1 ||
 | |
|         Array.from(descriptors)[0]?.type !== lazy.ContextDescriptorType.All
 | |
|       ) {
 | |
|         descriptors.add(contextDescriptor);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     const rootDestination = {
 | |
|       type: lazy.RootMessageHandler.type,
 | |
|     };
 | |
|     const sessionDataPromises = [];
 | |
| 
 | |
|     for (const [moduleName, categories] of structuredUpdates.entries()) {
 | |
|       for (const [category, contextDescriptors] of categories.entries()) {
 | |
|         // Find sessionData for the category and the moduleName.
 | |
|         const relevantSessionData = this._data.filter(
 | |
|           item => item.category == category && item.moduleName === moduleName
 | |
|         );
 | |
|         for (const contextDescriptor of contextDescriptors.values()) {
 | |
|           const windowGlobalDestination = {
 | |
|             type: lazy.WindowGlobalMessageHandler.type,
 | |
|             contextDescriptor,
 | |
|           };
 | |
| 
 | |
|           for (const destination of [
 | |
|             windowGlobalDestination,
 | |
|             rootDestination,
 | |
|           ]) {
 | |
|             // Only apply session data if the module is present for the destination.
 | |
|             if (
 | |
|               this._messageHandler.supportsCommand(
 | |
|                 moduleName,
 | |
|                 "_applySessionData",
 | |
|                 destination
 | |
|               )
 | |
|             ) {
 | |
|               sessionDataPromises.push(
 | |
|                 this._messageHandler
 | |
|                   .handleCommand({
 | |
|                     moduleName,
 | |
|                     commandName: "_applySessionData",
 | |
|                     params: {
 | |
|                       sessionData: relevantSessionData,
 | |
|                       category,
 | |
|                       contextDescriptor,
 | |
|                     },
 | |
|                     destination,
 | |
|                   })
 | |
|                   ?.catch(reason =>
 | |
|                     lazy.logger.error(
 | |
|                       `_applySessionData for module: ${moduleName} failed, reason: ${reason}`
 | |
|                     )
 | |
|                   )
 | |
|               );
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     await Promise.allSettled(sessionDataPromises);
 | |
|   }
 | |
| 
 | |
|   _isSameItem(item1, item2) {
 | |
|     const descriptor1 = item1.contextDescriptor;
 | |
|     const descriptor2 = item2.contextDescriptor;
 | |
| 
 | |
|     return (
 | |
|       item1.moduleName === item2.moduleName &&
 | |
|       item1.category === item2.category &&
 | |
|       this._isSameContextDescriptor(descriptor1, descriptor2) &&
 | |
|       this._isSameValue(item1.category, item1.value, item2.value)
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   _isSameContextDescriptor(contextDescriptor1, contextDescriptor2) {
 | |
|     if (contextDescriptor1.type === lazy.ContextDescriptorType.All) {
 | |
|       // Ignore the id for type "all" since we made the id optional for this type.
 | |
|       return contextDescriptor1.type === contextDescriptor2.type;
 | |
|     }
 | |
| 
 | |
|     return (
 | |
|       contextDescriptor1.type === contextDescriptor2.type &&
 | |
|       contextDescriptor1.id === contextDescriptor2.id
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   _isSameValue(category, value1, value2) {
 | |
|     if (category === SessionDataCategory.PreloadScript) {
 | |
|       return value1.script === value2.script;
 | |
|     }
 | |
| 
 | |
|     return value1 === value2;
 | |
|   }
 | |
| 
 | |
|   _findIndex(item) {
 | |
|     return this._data.findIndex(_item => this._isSameItem(item, _item));
 | |
|   }
 | |
| 
 | |
|   _persist() {
 | |
|     // Update the sessionDataMap singleton.
 | |
|     sessionDataMap.set(this._messageHandler.sessionId, this._data);
 | |
| 
 | |
|     // Update sharedData and flush to force consistency.
 | |
|     Services.ppmm.sharedData.set(SESSION_DATA_SHARED_DATA_KEY, sessionDataMap);
 | |
|     Services.ppmm.sharedData.flush();
 | |
|   }
 | |
| }
 | 
