summaryrefslogtreecommitdiffstats
path: root/remote/shared/messagehandler/sessiondata/SessionData.sys.mjs
blob: 4a85585a909c59950ca877c1fb4981c7a7424277 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
/* 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/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

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",
});

XPCOMUtils.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();
  }
}