/* 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/. */

/**
 * FinalizationRegistry callback, see
 * https://searchfox.org/firefox-main/source/js/src/builtin/FinalizationRegistryObject.h
 *
 * Will be invoked when the channel corresponding to the weak reference is
 * "destroyed", at which point we can cleanup the corresponding entry in our
 * regular map.
 */
function deleteIdFromRefMap({ refMap, id }) {
  refMap.delete(id);
}

/**
 * This object implements iterable weak map for HTTP channels tracked by
 * the network observer.
 *
 * We can't use Map() for storing HTTP channel references since we don't
 * know when we should remove the entry in it (it's wrong to do it in
 * 'onTransactionClose' since it doesn't have to be the last platform
 * notification for a given channel). We want the map to auto update
 * when the channel is garbage collected.
 *
 * We can't use WeakMap() since searching for a value by the channel object
 * isn't reliable (there might be different objects representing the same
 * channel). We need to search by channel ID, but ID can't be used as key
 * in WeakMap().
 *
 * So, this custom map solves aforementioned issues.
 */
export class ChannelMap {
  #finalizationRegistry;
  #refMap;
  #weakMap;

  constructor() {
    // See https://searchfox.org/firefox-main/source/js/src/builtin/FinalizationRegistryObject.h
    this.#finalizationRegistry = new FinalizationRegistry(deleteIdFromRefMap);

    // Map of channel id to a channel weak reference.
    this.#refMap = new Map();

    /**
     * WeakMap from nsIChannel instances to objects which encapsulate ChannelMap
     * values with the following structure:
     *
     * @property {object} value
     *     The actual value stored in this ChannelMap entry, which should relate
     *     to this channel.
     * @property {WeakRef} ref
     *     Weak reference for the channel object which is the key of the entry.
     */
    this.#weakMap = new WeakMap();
  }

  /**
   * Remove all entries from the ChannelMap.
   */
  clear() {
    this.#refMap.clear();
  }

  /**
   * Delete the entry for the provided channel from the underlying maps, if any.
   * Note that this will only delete entries which were set for the exact same
   * nsIChannel object, and will not attempt to look up entries by channel id.
   *
   * @param {nsIChannel} channel
   *     The key to delete from the ChannelMap.
   *
   * @return {boolean}
   *     True if an entry was deleted, false otherwise.
   */
  delete(channel) {
    const entry = this.#weakMap.get(channel);
    if (!entry) {
      return false;
    }

    this.#weakMap.delete(channel);
    this.#refMap.delete(channel.channelId);
    this.#finalizationRegistry.unregister(entry.ref);
    return true;
  }

  /**
   * Retrieve a value stored in the ChannelMap by the provided channel.
   *
   * @param {nsIChannel} channel
   *     The key to delete from the ChannelMap.
   *
   * @return {object | null}
   *     The value held for the provided channel.
   *     Null if the channel did not match any known key.
   */
  get(channel) {
    const ref = this.#refMap.get(channel.channelId);
    const key = ref ? ref.deref() : null;
    if (!key) {
      return null;
    }
    const channelInfo = this.#weakMap.get(key);
    return channelInfo ? channelInfo.value : null;
  }

  /**
   * Adds or updates an entry in the ChannelMap for the provided channel.
   *
   * @param {nsIChannel} channel
   *     The key of the entry to add or update.
   * @param {object} value
   *     The value to add or update.
   */
  set(channel, value) {
    const ref = new WeakRef(channel);
    this.#weakMap.set(channel, { value, ref });
    this.#refMap.set(channel.channelId, ref);
    this.#finalizationRegistry.register(
      channel,
      {
        refMap: this.#refMap,
        id: channel.channelId,
      },
      ref
    );
  }
}
