JSDoc: Source: transport.js

# Source: transport.js

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

    "use strict";

    /* global Pipe, ScriptableInputStream, uneval */

    const {Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu, results: Cr} =
        Components;

    Cu.import("resource://gre/modules/Services.jsm");
    Cu.import("resource://gre/modules/EventEmitter.jsm");
    const {StreamUtils} =
        Cu.import("chrome://marionette/content/stream-utils.js", {});
    const {Packet, JSONPacket, BulkPacket} =
        Cu.import("chrome://marionette/content/packets.js", {});

    const defer = function() {
      let deferred = {
        promise: new Promise((resolve, reject) => {
          deferred.resolve = resolve;
          deferred.reject = reject;
        }),
      };
      return deferred;
    };

    const executeSoon = function(func) {
      Services.tm.dispatchToMainThread(func);
    };

    const flags = {wantVerbose: false, wantLogging: false};

    const dumpv =
      flags.wantVerbose ?
      function(msg) { dump(msg + "\n"); } :
      function() {};

    const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init");

    const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
        "nsIScriptableInputStream", "init");

    this.EXPORTED_SYMBOLS = ["DebuggerTransport"];

    const PACKET_HEADER_MAX = 200;

    /**
     * An adapter that handles data transfers between the debugger client
     * and server. It can work with both nsIPipe and nsIServerSocket
     * transports so long as the properly created input and output streams
     * are specified.  (However, for intra-process connections,
     * LocalDebuggerTransport, below, is more efficient than using an nsIPipe
     * pair with DebuggerTransport.)
     *
     * @param {nsIAsyncInputStream} input
     *     The input stream.
     * @param {nsIAsyncOutputStream} output
     *     The output stream.
     *
     * Given a DebuggerTransport instance dt:
     * 1) Set dt.hooks to a packet handler object (described below).
     * 2) Call dt.ready() to begin watching for input packets.
     * 3) Call dt.send() / dt.startBulkSend() to send packets.
     * 4) Call dt.close() to close the connection, and disengage from
     *    the event loop.
     *
     * A packet handler is an object with the following methods:
     *
     * - onPacket(packet) - called when we have received a complete packet.
     *   |packet| is the parsed form of the packet --- a JavaScript value, not
     *   a JSON-syntax string.
     *
     * - onBulkPacket(packet) - called when we have switched to bulk packet
     *   receiving mode. |packet| is an object containing:
     *   * actor:  Name of actor that will receive the packet
     *   * type:   Name of actor's method that should be called on receipt
     *   * length: Size of the data to be read
     *   * stream: This input stream should only be used directly if you
     *             can ensure that you will read exactly |length| bytes and
     *             will not close the stream when reading is complete
     *   * done:   If you use the stream directly (instead of |copyTo|
     *             below), you must signal completion by resolving/rejecting
     *             this deferred.  If it's rejected, the transport will
     *             be closed.  If an Error is supplied as a rejection value,
     *             it will be logged via |dump|.  If you do use |copyTo|,
     *             resolving is taken care of for you when copying completes.
     *   * copyTo: A helper function for getting your data out of the
     *             stream that meets the stream handling requirements above,
     *             and has the following signature:
     *
     *             @param nsIAsyncOutputStream {output}
     *                 The stream to copy to.
     *
     *             @return {Promise}
     *                 The promise is resolved when copying completes or
     *                 rejected if any (unexpected) errors occur.  This object
     *                 also emits "progress" events for each chunk that is
     *                 copied.  See stream-utils.js.
     *
     * - onClosed(reason) - called when the connection is closed. |reason|
     *   is an optional nsresult or object, typically passed when the
     *   transport is closed due to some error in a underlying stream.
     *
     * See ./packets.js and the Remote Debugging Protocol specification for
     * more details on the format of these packets.
     *
     * @class
     */
    function DebuggerTransport(input, output) {
      EventEmitter.decorate(this);

      this._input = input;
      this._scriptableInput = new ScriptableInputStream(input);
      this._output = output;

      // The current incoming (possibly partial) header, which will determine
      // which type of Packet |_incoming| below will become.
      this._incomingHeader = "";
      // The current incoming Packet object
      this._incoming = null;
      // A queue of outgoing Packet objects
      this._outgoing = [];

      this.hooks = null;
      this.active = false;

      this._incomingEnabled = true;
      this._outgoingEnabled = true;

      this.close = this.close.bind(this);
    }

    DebuggerTransport.prototype = {
      /**
       * Transmit an object as a JSON packet.
       *
       * This method returns immediately, without waiting for the entire
       * packet to be transmitted, registering event handlers as needed to
       * transmit the entire packet. Packets are transmitted in the order they
       * are passed to this method.
       */
      send(object) {
        this.emit("send", object);

        let packet = new JSONPacket(this);
        packet.object = object;
        this._outgoing.push(packet);
        this._flushOutgoing();
      },

      /**
       * Transmit streaming data via a bulk packet.
       *
       * This method initiates the bulk send process by queuing up the header
       * data.  The caller receives eventual access to a stream for writing.
       *
       * N.B.: Do *not* attempt to close the stream handed to you, as it
       * will continue to be used by this transport afterwards.  Most users
       * should instead use the provided |copyFrom| function instead.
       *
       * @param {Object} header
       *     This is modeled after the format of JSON packets above, but does
       *     not actually contain the data, but is instead just a routing
       *     header:
       *
       *       - actor:  Name of actor that will receive the packet
       *       - type:   Name of actor's method that should be called on receipt
       *       - length: Size of the data to be sent
       *
       * @return {Promise}
       *     The promise will be resolved when you are allowed to write to
       *     the stream with an object containing:
       *
       *       - stream:   This output stream should only be used directly
       *                   if you can ensure that you will write exactly
       *                   |length| bytes and will not close the stream when
       *                    writing is complete.
       *       - done:     If you use the stream directly (instead of
       *                   |copyFrom| below), you must signal completion by
       *                   resolving/rejecting this deferred.  If it's
       *                   rejected, the transport will be closed.  If an
       *                   Error is supplied as a rejection value, it will
       *                   be logged via |dump|.  If you do use |copyFrom|,
       *                   resolving is taken care of for you when copying
       *                   completes.
       *       - copyFrom: A helper function for getting your data onto the
       *                   stream that meets the stream handling requirements
       *                   above, and has the following signature:
       *
       *                   @param {nsIAsyncInputStream} input
       *                       The stream to copy from.
       *
       *                   @return {Promise}
       *                       The promise is resolved when copying completes
       *                       or rejected if any (unexpected) errors occur.
       *                       This object also emits "progress" events for
       *                       each chunkthat is copied.  See stream-utils.js.
       */
      startBulkSend(header) {
        this.emit("startbulksend", header);

        let packet = new BulkPacket(this);
        packet.header = header;
        this._outgoing.push(packet);
        this._flushOutgoing();
        return packet.streamReadyForWriting;
      },

      /**
       * Close the transport.
       *
       * @param {(nsresult|object)=} reason
       *     The status code or error message that corresponds to the reason
       *     for closing the transport (likely because a stream closed
       *     or failed).
       */
      close(reason) {
        this.emit("close", reason);

        this.active = false;
        this._input.close();
        this._scriptableInput.close();
        this._output.close();
        this._destroyIncoming();
        this._destroyAllOutgoing();
        if (this.hooks) {
          this.hooks.onClosed(reason);
          this.hooks = null;
        }
        if (reason) {
          dumpv("Transport closed: " + reason);
        } else {
          dumpv("Transport closed.");
        }
      },

      /**
       * The currently outgoing packet (at the top of the queue).
       */
      get _currentOutgoing() {
        return this._outgoing[0];
      },

      /**
       * Flush data to the outgoing stream.  Waits until the output
       * stream notifies us that it is ready to be written to (via
       * onOutputStreamReady).
       */
      _flushOutgoing() {
        if (!this._outgoingEnabled || this._outgoing.length === 0) {
          return;
        }

        // If the top of the packet queue has nothing more to send, remove it.
        if (this._currentOutgoing.done) {
          this._finishCurrentOutgoing();
        }

        if (this._outgoing.length > 0) {
          let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
          this._output.asyncWait(this, 0, 0, threadManager.currentThread);
        }
      },

      /**
       * Pause this transport's attempts to write to the output stream.
       * This is used when we've temporarily handed off our output stream for
       * writing bulk data.
       */
      pauseOutgoing() {
        this._outgoingEnabled = false;
      },

      /**
       * Resume this transport's attempts to write to the output stream.
       */
      resumeOutgoing() {
        this._outgoingEnabled = true;
        this._flushOutgoing();
      },

      // nsIOutputStreamCallback
      /**
       * This is called when the output stream is ready for more data to
       * be written.  The current outgoing packet will attempt to write some
       * amount of data, but may not complete.
       */
      onOutputStreamReady(stream) {
        if (!this._outgoingEnabled || this._outgoing.length === 0) {
          return;
        }

        try {
          this._currentOutgoing.write(stream);
        } catch (e) {
          if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
            this.close(e.result);
            return;
          }
          throw e;
        }

        this._flushOutgoing();
      },

      /**
       * Remove the current outgoing packet from the queue upon completion.
       */
      _finishCurrentOutgoing() {
        if (this._currentOutgoing) {
          this._currentOutgoing.destroy();
          this._outgoing.shift();
        }
      },

      /**
       * Clear the entire outgoing queue.
       */
      _destroyAllOutgoing() {
        for (let packet of this._outgoing) {
          packet.destroy();
        }
        this._outgoing = [];
      },

      /**
       * Initialize the input stream for reading. Once this method has been
       * called, we watch for packets on the input stream, and pass them to
       * the appropriate handlers via this.hooks.
       */
      ready() {
        this.active = true;
        this._waitForIncoming();
      },

      /**
       * Asks the input stream to notify us (via onInputStreamReady) when it is
       * ready for reading.
       */
      _waitForIncoming() {
        if (this._incomingEnabled) {
          let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
          this._input.asyncWait(this, 0, 0, threadManager.currentThread);
        }
      },

      /**
       * Pause this transport's attempts to read from the input stream.
       * This is used when we've temporarily handed off our input stream for
       * reading bulk data.
       */
      pauseIncoming() {
        this._incomingEnabled = false;
      },

      /**
       * Resume this transport's attempts to read from the input stream.
       */
      resumeIncoming() {
        this._incomingEnabled = true;
        this._flushIncoming();
        this._waitForIncoming();
      },

      // nsIInputStreamCallback
      /**
       * Called when the stream is either readable or closed.
       */
      onInputStreamReady(stream) {
        try {
          while (stream.available() && this._incomingEnabled &&
                 this._processIncoming(stream, stream.available())) {
             // Loop until there is nothing more to process
          }
          this._waitForIncoming();
        } catch (e) {
          if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
            this.close(e.result);
          } else {
            throw e;
          }
        }
      },

      /**
       * Process the incoming data.  Will create a new currently incoming
       * Packet if needed.  Tells the incoming Packet to read as much data
       * as it can, but reading may not complete.  The Packet signals that
       * its data is ready for delivery by calling one of this transport's
       * _on*Ready methods (see ./packets.js and the _on*Ready methods below).
       *
       * @return {boolean}
       *     Whether incoming stream processing should continue for any
       *     remaining data.
       */
      _processIncoming(stream, count) {
        dumpv("Data available: " + count);

        if (!count) {
          dumpv("Nothing to read, skipping");
          return false;
        }

        try {
          if (!this._incoming) {
            dumpv("Creating a new packet from incoming");

            if (!this._readHeader(stream)) {
              // Not enough data to read packet type
              return false;
            }

            // Attempt to create a new Packet by trying to parse each possible
            // header pattern.
            this._incoming = Packet.fromHeader(this._incomingHeader, this);
            if (!this._incoming) {
              throw new Error("No packet types for header: " +
                            this._incomingHeader);
            }
          }

          if (!this._incoming.done) {
            // We have an incomplete packet, keep reading it.
            dumpv("Existing packet incomplete, keep reading");
            this._incoming.read(stream, this._scriptableInput);
          }
        } catch (e) {
          dump(`Error reading incoming packet: (${e} - ${e.stack})\n`);

          // Now in an invalid state, shut down the transport.
          this.close();
          return false;
        }

        if (!this._incoming.done) {
          // Still not complete, we'll wait for more data.
          dumpv("Packet not done, wait for more");
          return true;
        }

        // Ready for next packet
        this._flushIncoming();
        return true;
      },

      /**
       * Read as far as we can into the incoming data, attempting to build
       * up a complete packet header (which terminates with ":").  We'll only
       * read up to PACKET_HEADER_MAX characters.
       *
       * @return {boolean}
       *     True if we now have a complete header.
       */
      _readHeader() {
        let amountToRead = PACKET_HEADER_MAX - this._incomingHeader.length;
        this._incomingHeader +=
        StreamUtils.delimitedRead(this._scriptableInput, ":", amountToRead);
        if (flags.wantVerbose) {
          dumpv("Header read: " + this._incomingHeader);
        }

        if (this._incomingHeader.endsWith(":")) {
          if (flags.wantVerbose) {
            dumpv("Found packet header successfully: " + this._incomingHeader);
          }
          return true;
        }

        if (this._incomingHeader.length >= PACKET_HEADER_MAX) {
          throw new Error("Failed to parse packet header!");
        }

        // Not enough data yet.
        return false;
      },

      /**
       * If the incoming packet is done, log it as needed and clear the buffer.
       */
      _flushIncoming() {
        if (!this._incoming.done) {
          return;
        }
        if (flags.wantLogging) {
          dumpv("Got: " + this._incoming);
        }
        this._destroyIncoming();
      },

      /**
       * Handler triggered by an incoming JSONPacket completing it's |read|
       * method.  Delivers the packet to this.hooks.onPacket.
       */
      _onJSONObjectReady(object) {
        executeSoon(() => {
        // Ensure the transport is still alive by the time this runs.
          if (this.active) {
            this.emit("packet", object);
            this.hooks.onPacket(object);
          }
        });
      },

      /**
       * Handler triggered by an incoming BulkPacket entering the |read|
       * phase for the stream portion of the packet.  Delivers info about the
       * incoming streaming data to this.hooks.onBulkPacket.  See the main
       * comment on the transport at the top of this file for more details.
       */
      _onBulkReadReady(...args) {
        executeSoon(() => {
        // Ensure the transport is still alive by the time this runs.
          if (this.active) {
            this.emit("bulkpacket", ...args);
            this.hooks.onBulkPacket(...args);
          }
        });
      },

      /**
       * Remove all handlers and references related to the current incoming
       * packet, either because it is now complete or because the transport
       * is closing.
       */
      _destroyIncoming() {
        if (this._incoming) {
          this._incoming.destroy();
        }
        this._incomingHeader = "";
        this._incoming = null;
      },
    };

    /**
     * An adapter that handles data transfers between the debugger client
     * and server when they both run in the same process. It presents the
     * same API as DebuggerTransport, but instead of transmitting serialized
     * messages across a connection it merely calls the packet dispatcher of
     * the other side.
     *
     * @param {LocalDebuggerTransport} other
     *     The other endpoint for this debugger connection.
     *
     * @see {DebuggerTransport}
     */
    function LocalDebuggerTransport(other) {
      EventEmitter.decorate(this);

      this.other = other;
      this.hooks = null;

      // A packet number, shared between this and this.other. This isn't
      // used by the protocol at all, but it makes the packet traces a lot
      // easier to follow.
      this._serial = this.other ? this.other._serial : {count: 0};
      this.close = this.close.bind(this);
    }

    LocalDebuggerTransport.prototype = {
      /**
       * Transmit a message by directly calling the onPacket handler of the other
       * endpoint.
       */
      send(packet) {
        this.emit("send", packet);

        let serial = this._serial.count++;
        if (flags.wantLogging) {
          // Check 'from' first, as 'echo' packets have both.
          if (packet.from) {
            dumpv("Packet " + serial + " sent from " + uneval(packet.from));
          } else if (packet.to) {
            dumpv("Packet " + serial + " sent to " + uneval(packet.to));
          }
        }
        this._deepFreeze(packet);
        let other = this.other;
        if (other) {
          executeSoon(() => {
            // Avoid the cost of JSON.stringify() when logging is disabled.
            if (flags.wantLogging) {
              dumpv(`Received packet ${serial}: ` +
                  JSON.stringify(packet, null, 2));
            }
            if (other.hooks) {
              other.emit("packet", packet);
              other.hooks.onPacket(packet);
            }
          });
        }
      },

      /**
       * Send a streaming bulk packet directly to the onBulkPacket handler
       * of the other endpoint.
       *
       * This case is much simpler than the full DebuggerTransport, since
       * there is no primary stream we have to worry about managing while
       * we hand it off to others temporarily.  Instead, we can just make a
       * single use pipe and be done with it.
       */
      startBulkSend({actor, type, length}) {
        this.emit("startbulksend", {actor, type, length});

        let serial = this._serial.count++;

        dumpv("Sent bulk packet " + serial + " for actor " + actor);
        if (!this.other) {
          let error = new Error("startBulkSend: other side of transport missing");
          return Promise.reject(error);
        }

        let pipe = new Pipe(true, true, 0, 0, null);

        executeSoon(() => {
          dumpv("Received bulk packet " + serial);
          if (!this.other.hooks) {
            return;
          }

          // Receiver
          let deferred = defer();
          let packet = {
            actor,
            type,
            length,
            copyTo: (output) => {
              let copying =
              StreamUtils.copyStream(pipe.inputStream, output, length);
              deferred.resolve(copying);
              return copying;
            },
            stream: pipe.inputStream,
            done: deferred,
          };

          this.other.emit("bulkpacket", packet);
          this.other.hooks.onBulkPacket(packet);

          // Await the result of reading from the stream
          deferred.promise.then(() => pipe.inputStream.close(), this.close);
        });

        // Sender
        let sendDeferred = defer();

        // The remote transport is not capable of resolving immediately here,
        // so we shouldn't be able to either.
        executeSoon(() => {
          let copyDeferred = defer();

          sendDeferred.resolve({
            copyFrom: (input) => {
              let copying =
              StreamUtils.copyStream(input, pipe.outputStream, length);
              copyDeferred.resolve(copying);
              return copying;
            },
            stream: pipe.outputStream,
            done: copyDeferred,
          });

          // Await the result of writing to the stream
          copyDeferred.promise.then(() => pipe.outputStream.close(), this.close);
        });

        return sendDeferred.promise;
      },

      /**
       * Close the transport.
       */
      close() {
        this.emit("close");

        if (this.other) {
          // Remove the reference to the other endpoint before calling close(), to
          // avoid infinite recursion.
          let other = this.other;
          this.other = null;
          other.close();
        }
        if (this.hooks) {
          try {
            this.hooks.onClosed();
          } catch (ex) {
            console.error(ex);
          }
          this.hooks = null;
        }
      },

      /**
       * An empty method for emulating the DebuggerTransport API.
       */
      ready() {},

      /**
       * Helper function that makes an object fully immutable.
       */
      _deepFreeze(object) {
        Object.freeze(object);
        for (let prop in object) {
          // Freeze the properties that are objects, not on the prototype,
          // and not already frozen.  Note that this might leave an unfrozen
          // reference somewhere in the object if there is an already frozen
          // object containing an unfrozen object.
          if (object.hasOwnProperty(prop) && typeof object === "object" &&
              !Object.isFrozen(object)) {
            this._deepFreeze(object[prop]);
          }
        }
      },
    };

    /**
     * A transport for the debugging protocol that uses nsIMessageManagers to
     * exchange packets with servers running in child processes.
     *
     * In the parent process, <var>mm</var> should be the nsIMessageSender
     * for the child process. In a child process, |mm| should be the child
     * process message manager, which sends packets to the parent.
     *
     * <var>prefix</var> is a string included in the message names, to
     * distinguish multiple servers running in the same child process.
     *
     * This transport exchanges messages named <tt>debug:PREFIX:packet</tt>,
     * where <tt>PREFIX</tt> is <var>prefix</var>, whose data is the protocol
     * packet.
     */
    function ChildDebuggerTransport(mm, prefix) {
      EventEmitter.decorate(this);

      this._mm = mm;
      this._messageName = "debug:" + prefix + ":packet";
    }

    /*
     * To avoid confusion, we use 'message' to mean something that
     * nsIMessageSender conveys, and 'packet' to mean a remote debugging
     * protocol packet.
     */
    ChildDebuggerTransport.prototype = {
      constructor: ChildDebuggerTransport,

      hooks: null,

      _addListener() {
        this._mm.addMessageListener(this._messageName, this);
      },

      _removeListener() {
        try {
          this._mm.removeMessageListener(this._messageName, this);
        } catch (e) {
          if (e.result != Cr.NS_ERROR_NULL_POINTER) {
            throw e;
          }
          // In some cases, especially when using messageManagers in non-e10s
          // mode, we reach this point with a dead messageManager which only
          // throws errors but does not seem to indicate in any other way that
          // it is dead.
        }
      },

      ready() {
        this._addListener();
      },

      close() {
        this._removeListener();
        this.emit("close");
        this.hooks.onClosed();
      },

      receiveMessage({data}) {
        this.emit("packet", data);
        this.hooks.onPacket(data);
      },

      send(packet) {
        this.emit("send", packet);
        try {
          this._mm.sendAsyncMessage(this._messageName, packet);
        } catch (e) {
          if (e.result != Cr.NS_ERROR_NULL_POINTER) {
            throw e;
          }
          // In some cases, especially when using messageManagers in non-e10s
          // mode, we reach this point with a dead messageManager which only
          // throws errors but does not seem to indicate in any other way that
          // it is dead.
        }
      },

      startBulkSend() {
        throw new Error("Can't send bulk data to child processes.");
      },

      swapBrowser(mm) {
        this._removeListener();
        this._mm = mm;
        this._addListener();
      },
    };

    // WorkerDebuggerTransport is defined differently depending on whether we are
    // on the main thread or a worker thread. In the former case, we are required
    // by the devtools loader, and isWorker will be false. Otherwise, we are
    // required by the worker loader, and isWorker will be true.
    //
    // Each worker debugger supports only a single connection to the main thread.
    // However, its theoretically possible for multiple servers to connect to the
    // same worker. Consequently, each transport has a connection id, to allow
    // messages from multiple connections to be multiplexed on a single channel.

    if (!this.isWorker) {
      // Main thread
      (function() {
        /**
         * A transport that uses a WorkerDebugger to send packets from the main
         * thread to a worker thread.
         */
        function WorkerDebuggerTransport(dbg, id) {
          this._dbg = dbg;
          this._id = id;
          this.onMessage = this._onMessage.bind(this);
        }

        WorkerDebuggerTransport.prototype = {
          constructor: WorkerDebuggerTransport,

          ready() {
            this._dbg.addListener(this);
          },

          close() {
            this._dbg.removeListener(this);
            if (this.hooks) {
              this.hooks.onClosed();
            }
          },

          send(packet) {
            this._dbg.postMessage(JSON.stringify({
              type: "message",
              id: this._id,
              message: packet,
            }));
          },

          startBulkSend() {
            throw new Error("Can't send bulk data from worker threads!");
          },

          _onMessage(message) {
            let packet = JSON.parse(message);
            if (packet.type !== "message" || packet.id !== this._id) {
              return;
            }

            if (this.hooks) {
              this.hooks.onPacket(packet.message);
            }
          },
        };

      }).call(this);
    } else {
      // Worker thread
      (function() {
        /**
         * A transport that uses a WorkerDebuggerGlobalScope to send packets
         * from a worker thread to the main thread.
         */
        function WorkerDebuggerTransport(scope, id) {
          this._scope = scope;
          this._id = id;
          this._onMessage = this._onMessage.bind(this);
        }

        WorkerDebuggerTransport.prototype = {
          constructor: WorkerDebuggerTransport,

          ready() {
            this._scope.addEventListener("message", this._onMessage);
          },

          close() {
            this._scope.removeEventListener("message", this._onMessage);
            if (this.hooks) {
              this.hooks.onClosed();
            }
          },

          send(packet) {
            this._scope.postMessage(JSON.stringify({
              type: "message",
              id: this._id,
              message: packet,
            }));
          },

          startBulkSend() {
            throw new Error("Can't send bulk data from worker threads!");
          },

          _onMessage(event) {
            let packet = JSON.parse(event.data);
            if (packet.type !== "message" || packet.id !== this._id) {
              return;
            }

            if (this.hooks) {
              this.hooks.onPacket(packet.message);
            }
          },
        };

      }).call(this);
    }