Quellcodebibliothek Statistik Leitseite products/sources/formale Sprachen/C/Firefox/dom/notification/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 11 kB image not shown  

Quelle  NotificationDB.sys.mjs   Sprache: unbekannt

 
/* 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, {
  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "console", () => {
  return console.createInstance({
    prefix: "NotificationDB",
    maxLogLevelPref: "dom.webnotifications.loglevel",
  });
});

const NOTIFICATION_STORE_DIR = PathUtils.profileDir;
const NOTIFICATION_STORE_PATH = PathUtils.join(
  NOTIFICATION_STORE_DIR,
  "notificationstore.json"
);

export class NotificationDB {
  // Ensure we won't call init() while xpcom-shutdown is performed
  #shutdownInProgress = false;

  // A promise that resolves once the ongoing task queue has been drained.
  // The value will be reset when the queue starts again.
  #queueDrainedPromise = null;
  #queueDrainedPromiseResolve = null;

  #byTag = Object.create(null);
  #notifications = Object.create(null);
  #loaded = false;
  #tasks = [];
  #runningTask = null;

  storageQualifier() {
    return "Notification";
  }

  prefixStorageQualifier(message) {
    return `${this.storageQualifier()}:${message}`;
  }

  formatMessageType(message) {
    return this.prefixStorageQualifier(message);
  }

  supportedMessageTypes() {
    return [
      this.formatMessageType("Save"),
      this.formatMessageType("Delete"),
      this.formatMessageType("GetAll"),
    ];
  }

  constructor() {
    if (this.#shutdownInProgress) {
      return;
    }

    this.#notifications = Object.create(null);
    this.#byTag = Object.create(null);
    this.#loaded = false;

    this.#tasks = []; // read/write operation queue
    this.#runningTask = null;

    Services.obs.addObserver(this, "xpcom-shutdown");
    this.registerListeners();

    // This assumes that nothing will queue a new task at profile-change-teardown phase,
    // potentially replacing the #queueDrainedPromise if there was no existing task run.
    lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
      "NotificationDB: Need to make sure that all notification messages are processed",
      () => this.#queueDrainedPromise
    );
  }

  registerListeners() {
    for (let message of this.supportedMessageTypes()) {
      Services.ppmm.addMessageListener(message, this);
    }
  }

  unregisterListeners() {
    for (let message of this.supportedMessageTypes()) {
      Services.ppmm.removeMessageListener(message, this);
    }
  }

  observe(aSubject, aTopic) {
    lazy.console.debug(`Topic: ${aTopic}`);
    if (aTopic == "xpcom-shutdown") {
      this.#shutdownInProgress = true;
      Services.obs.removeObserver(this, "xpcom-shutdown");
      this.unregisterListeners();
    }
  }

  filterNonAppNotifications(notifications) {
    let result = Object.create(null);
    for (let origin in notifications) {
      result[origin] = Object.create(null);
      let persistentNotificationCount = 0;
      for (let id in notifications[origin]) {
        if (notifications[origin][id].serviceWorkerRegistrationScope) {
          persistentNotificationCount++;
          result[origin][id] = notifications[origin][id];
        }
      }
      if (persistentNotificationCount == 0) {
        lazy.console.debug(
          `Origin ${origin} is not linked to an app manifest, deleting.`
        );
        delete result[origin];
      }
    }

    return result;
  }

  // Attempt to read notification file, if it's not there we will create it.
  load() {
    var promise = IOUtils.readUTF8(NOTIFICATION_STORE_PATH);
    return promise.then(
      data => {
        if (data.length) {
          // Preprocessing phase intends to cleanly separate any migration-related
          // tasks.
          this.#notifications = this.filterNonAppNotifications(
            JSON.parse(data)
          );
        }

        // populate the list of notifications by tag
        if (this.#notifications) {
          for (var origin in this.#notifications) {
            this.#byTag[origin] = Object.create(null);
            for (var id in this.#notifications[origin]) {
              var curNotification = this.#notifications[origin][id];
              if (curNotification.tag) {
                this.#byTag[origin][curNotification.tag] = curNotification;
              }
            }
          }
        }

        this.#loaded = true;
      },

      // If read failed, we assume we have no notifications to load.
      () => {
        this.#loaded = true;
        return this.createStore();
      }
    );
  }

  // Creates the notification directory.
  createStore() {
    var promise = IOUtils.makeDirectory(NOTIFICATION_STORE_DIR, {
      ignoreExisting: true,
    });
    return promise.then(this.createFile.bind(this));
  }

  // Creates the notification file once the directory is created.
  createFile() {
    return IOUtils.writeUTF8(NOTIFICATION_STORE_PATH, "", {
      tmpPath: NOTIFICATION_STORE_PATH + ".tmp",
    });
  }

  // Save current notifications to the file.
  save() {
    var data = JSON.stringify(this.#notifications);
    return IOUtils.writeUTF8(NOTIFICATION_STORE_PATH, data, {
      tmpPath: NOTIFICATION_STORE_PATH + ".tmp",
    });
  }

  // Helper function: promise will be resolved once file exists and/or is loaded.
  #ensureLoaded() {
    if (!this.#loaded) {
      return this.load();
    }
    return Promise.resolve();
  }

  receiveMessage(message) {
    lazy.console.debug(`Received message: ${message.name}`);

    // sendAsyncMessage can fail if the child process exits during a
    // notification storage operation, so always wrap it in a try/catch.
    function returnMessage(name, data) {
      try {
        message.target.sendAsyncMessage(name, data);
      } catch (e) {
        lazy.console.debug(`Return message failed, ${name}`);
      }
    }

    switch (message.name) {
      case this.formatMessageType("GetAll"):
        this.queueTask("getall", message.data)
          .then(notifications => {
            returnMessage(this.formatMessageType("GetAll:Return:OK"), {
              requestID: message.data.requestID,
              origin: message.data.origin,
              notifications,
            });
          })
          .catch(error => {
            returnMessage(this.formatMessageType("GetAll:Return:KO"), {
              requestID: message.data.requestID,
              origin: message.data.origin,
              errorMsg: error,
            });
          });
        break;

      case this.formatMessageType("Save"):
        this.queueTask("save", message.data)
          .then(() => {
            returnMessage(this.formatMessageType("Save:Return:OK"), {
              requestID: message.data.requestID,
            });
          })
          .catch(error => {
            returnMessage(this.formatMessageType("Save:Return:KO"), {
              requestID: message.data.requestID,
              errorMsg: error,
            });
          });
        break;

      case this.formatMessageType("Delete"):
        this.queueTask("delete", message.data)
          .then(() => {
            returnMessage(this.formatMessageType("Delete:Return:OK"), {
              requestID: message.data.requestID,
            });
          })
          .catch(error => {
            returnMessage(this.formatMessageType("Delete:Return:KO"), {
              requestID: message.data.requestID,
              errorMsg: error,
            });
          });
        break;

      default:
        lazy.console.debug(`Invalid message name ${message.name}`);
    }
  }

  // We need to make sure any read/write operations are atomic,
  // so use a queue to run each operation sequentially.
  queueTask(operation, data) {
    lazy.console.debug(`Queueing task: ${operation}`);

    var defer = {};

    this.#tasks.push({
      operation,
      data,
      defer,
    });

    var promise = new Promise((resolve, reject) => {
      defer.resolve = resolve;
      defer.reject = reject;
    });

    // Only run immediately if we aren't currently running another task.
    if (!this.#runningTask) {
      lazy.console.debug("Task queue was not running, starting now...");
      this.runNextTask();
      this.#queueDrainedPromise = new Promise(resolve => {
        this.#queueDrainedPromiseResolve = resolve;
      });
    }

    return promise;
  }

  runNextTask() {
    if (this.#tasks.length === 0) {
      lazy.console.debug("No more tasks to run, queue depleted");
      this.#runningTask = null;
      if (this.#queueDrainedPromiseResolve) {
        this.#queueDrainedPromiseResolve();
      } else {
        lazy.console.debug(
          "#queueDrainedPromiseResolve was null somehow, no promise to resolve"
        );
      }
      return;
    }
    this.#runningTask = this.#tasks.shift();

    // Always make sure we are loaded before performing any read/write tasks.
    this.#ensureLoaded()
      .then(() => {
        var task = this.#runningTask;

        switch (task.operation) {
          case "getall":
            return this.taskGetAll(task.data);

          case "save":
            return this.taskSave(task.data);

          case "delete":
            return this.taskDelete(task.data);

          default:
            return Promise.reject(
              new Error(`Found a task with unknown operation ${task.operation}`)
            );
        }
      })
      .then(payload => {
        lazy.console.debug(`Finishing task: ${this.#runningTask.operation}`);
        this.#runningTask.defer.resolve(payload);
      })
      .catch(err => {
        lazy.console.debug(
          `Error while running ${this.#runningTask.operation}: ${err}`
        );
        this.#runningTask.defer.reject(err);
      })
      .then(() => {
        this.runNextTask();
      });
  }

  taskGetAll(data) {
    lazy.console.debug("Task, getting all");
    var origin = data.origin;
    var notifications = [];
    // Grab only the notifications for specified origin.
    if (this.#notifications[origin]) {
      if (data.tag) {
        let n;
        if ((n = this.#byTag[origin][data.tag])) {
          notifications.push(n);
        }
      } else {
        for (var i in this.#notifications[origin]) {
          notifications.push(this.#notifications[origin][i]);
        }
      }
    }
    return Promise.resolve(notifications);
  }

  taskSave(data) {
    lazy.console.debug("Task, saving");
    var origin = data.origin;
    var notification = data.notification;
    if (!this.#notifications[origin]) {
      this.#notifications[origin] = Object.create(null);
      this.#byTag[origin] = Object.create(null);
    }

    // We might have existing notification with this tag,
    // if so we need to remove it before saving the new one.
    if (notification.tag) {
      var oldNotification = this.#byTag[origin][notification.tag];
      if (oldNotification) {
        delete this.#notifications[origin][oldNotification.id];
      }
      this.#byTag[origin][notification.tag] = notification;
    }

    this.#notifications[origin][notification.id] = notification;
    return this.save();
  }

  taskDelete(data) {
    lazy.console.debug("Task, deleting");
    var origin = data.origin;
    var id = data.id;
    if (!this.#notifications[origin]) {
      lazy.console.debug(`No notifications found for origin: ${origin}`);
      return Promise.resolve();
    }

    // Make sure we can find the notification to delete.
    var oldNotification = this.#notifications[origin][id];
    if (!oldNotification) {
      lazy.console.debug(`No notification found with id: ${id}`);
      return Promise.resolve();
    }

    if (oldNotification.tag) {
      delete this.#byTag[origin][oldNotification.tag];
    }
    delete this.#notifications[origin][id];
    return this.save();
  }
}

new NotificationDB();

[ Dauer der Verarbeitung: 0.9 Sekunden  (vorverarbeitet)  ]