tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

channel.sub.js (39120B)


      1 (function() {
      2    function randInt(bits) {
      3        if (bits < 1 || bits > 53) {
      4            throw new TypeError();
      5        } else {
      6            if (bits >= 1 && bits <= 30) {
      7                return 0 | ((1 << bits) * Math.random());
      8            } else {
      9                var high = (0 | ((1 << (bits - 30)) * Math.random())) * (1 << 30);
     10                var low = 0 | ((1 << 30) * Math.random());
     11                return  high + low;
     12            }
     13        }
     14    }
     15 
     16 
     17    function toHex(x, length) {
     18        var rv = x.toString(16);
     19        while (rv.length < length) {
     20            rv = "0" + rv;
     21        }
     22        return rv;
     23    }
     24 
     25    function createUuid() {
     26        return [toHex(randInt(32), 8),
     27         toHex(randInt(16), 4),
     28         toHex(0x4000 | randInt(12), 4),
     29         toHex(0x8000 | randInt(14), 4),
     30         toHex(randInt(48), 12)].join("-");
     31    }
     32 
     33 
     34    /**
     35     * Cache of WebSocket instances per channel
     36     *
     37     * For reading there can only be one channel with each UUID, so we
     38     * just have a simple map of {uuid: WebSocket}. The socket can be
     39     * closed when the channel is closed.
     40     *
     41     * For writing there can be many channels for each uuid. Those can
     42     * share a websocket (within a specific global), so we have a map
     43     * of {uuid: [WebSocket, count]}.  Count is incremented when a
     44     * channel is opened with a given uuid, and decremented when its
     45     * closed. When the count reaches zero we can close the underlying
     46     * socket.
     47     */
     48    class SocketCache {
     49        constructor() {
     50            this.readSockets = new Map();
     51            this.writeSockets = new Map();
     52        };
     53 
     54        async getOrCreate(type, uuid, onmessage=null) {
     55            function createSocket() {
     56                let protocol = self.isSecureContext ? "wss" : "ws";
     57                let port = self.isSecureContext? "{{ports[wss][0]}}" : "{{ports[ws][0]}}";
     58                let url = `${protocol}://{{host}}:${port}/msg_channel?uuid=${uuid}&direction=${type}`;
     59                let socket = new WebSocket(url);
     60                if (onmessage !== null) {
     61                    socket.onmessage = onmessage;
     62                };
     63                return new Promise(resolve => socket.addEventListener("open", () => resolve(socket)));
     64            }
     65 
     66            let socket;
     67            if (type === "read") {
     68                if (this.readSockets.has(uuid)) {
     69                    throw new Error("Can't create multiple read sockets with same UUID");
     70                }
     71                socket = await createSocket();
     72                // If the socket is closed by the server, ensure it's removed from the cache
     73                socket.addEventListener("close", () => this.readSockets.delete(uuid));
     74                this.readSockets.set(uuid, socket);
     75            } else if (type === "write") {
     76                let count;
     77                if (onmessage !== null) {
     78                    throw new Error("Can't set message handler for write sockets");
     79                }
     80                if (this.writeSockets.has(uuid)) {
     81                    [socket, count] = this.writeSockets.get(uuid);
     82                } else {
     83                    socket = await createSocket();
     84                    count = 0;
     85                }
     86                count += 1;
     87                // If the socket is closed by the server, ensure it's removed from the cache
     88                socket.addEventListener("close", () => this.writeSockets.delete(uuid));
     89                this.writeSockets.set(uuid, [socket, count]);
     90            } else {
     91                throw new Error(`Unknown type ${type}`);
     92            }
     93            return socket;
     94        };
     95 
     96        async close(type, uuid) {
     97            let target = type === "read" ? this.readSockets : this.writeSockets;
     98            const data = target.get(uuid);
     99            if (!data) {
    100                return;
    101            }
    102            let count, socket;
    103            if (type == "read") {
    104                socket = data;
    105                count = 0;
    106            } else if (type === "write") {
    107                [socket, count] = data;
    108                count -= 1;
    109                if (count > 0) {
    110                    target.set(uuid, [socket, count]);
    111                }
    112            };
    113            if (count <= 0 && socket) {
    114                target.delete(uuid);
    115                socket.close(1000);
    116                await new Promise(resolve => socket.addEventListener("close", resolve));
    117            }
    118        };
    119 
    120        async closeAll() {
    121            let sockets = [];
    122            this.readSockets.forEach(value => sockets.push(value));
    123            this.writeSockets.forEach(value => sockets.push(value[0]));
    124            let closePromises = sockets.map(socket =>
    125                new Promise(resolve => socket.addEventListener("close", resolve)));
    126            sockets.forEach(socket => socket.close(1000));
    127            this.readSockets.clear();
    128            this.writeSockets.clear();
    129            await Promise.all(closePromises);
    130        }
    131    }
    132 
    133    const socketCache = new SocketCache();
    134 
    135    /**
    136     * Abstract base class for objects that allow sending / receiving
    137     * messages over a channel.
    138     */
    139    class Channel {
    140        type = null;
    141 
    142        constructor(uuid) {
    143            /** UUID for the channel */
    144            this.uuid = uuid;
    145            this.socket = null;
    146            this.eventListeners = {
    147                connect: new Set(),
    148                close: new Set()
    149            };
    150        }
    151 
    152        hasConnection() {
    153            return this.socket !== null && this.socket.readyState <= WebSocket.OPEN;
    154        }
    155 
    156        /**
    157         * Connect to the channel.
    158         *
    159         * @param {Function} onmessage - Event handler function for
    160         * the underlying websocket message.
    161         */
    162        async connect(onmessage) {
    163            if (this.hasConnection()) {
    164                return;
    165            }
    166            this.socket = await socketCache.getOrCreate(this.type, this.uuid, onmessage);
    167            this._dispatch("connect");
    168        }
    169 
    170        /**
    171         * Close the channel and underlying websocket connection
    172         */
    173        async close() {
    174            this.socket = null;
    175            await socketCache.close(this.type, this.uuid);
    176            this._dispatch("close");
    177        }
    178 
    179        /**
    180         * Add an event callback function. Supported message types are
    181         * "connect", "close", and "message" (for ``RecvChannel``).
    182         *
    183         * @param {string} type - Message type.
    184         * @param {Function} fn - Callback function. This is called
    185         * with an event-like object, with ``type`` and ``data``
    186         * properties.
    187         */
    188        addEventListener(type, fn) {
    189            if (typeof type !== "string") {
    190                throw new TypeError(`Expected string, got ${typeof type}`);
    191            }
    192            if (typeof fn !== "function") {
    193                throw new TypeError(`Expected function, got ${typeof fn}`);
    194            }
    195            if (!this.eventListeners.hasOwnProperty(type)) {
    196                throw new Error(`Unrecognised event type ${type}`);
    197            }
    198            this.eventListeners[type].add(fn);
    199        };
    200 
    201        /**
    202         * Remove an event callback function.
    203         *
    204         * @param {string} type - Event type.
    205         * @param {Function} fn - Callback function to remove.
    206         */
    207        removeEventListener(type, fn) {
    208            if (!typeof type === "string") {
    209                throw new TypeError(`Expected string, got ${typeof type}`);
    210            }
    211            if (typeof fn !== "function") {
    212                throw new TypeError(`Expected function, got ${typeof fn}`);
    213            }
    214            let listeners = this.eventListeners[type];
    215            if (listeners) {
    216                listeners.delete(fn);
    217            }
    218        };
    219 
    220        _dispatch(type, data) {
    221            let listeners = this.eventListeners[type];
    222            if (listeners) {
    223                // If any listener throws we end up not calling the other
    224                // listeners. This hopefully makes debugging easier, but
    225                // is different to DOM event listeners.
    226                listeners.forEach(fn => fn({type, data}));
    227            }
    228        };
    229 
    230    }
    231 
    232    /**
    233     * Send messages over a channel
    234     */
    235    class SendChannel extends Channel {
    236        type = "write";
    237 
    238        /**
    239         * Connect to the channel. Automatically called when sending the
    240         * first message.
    241         */
    242        async connect() {
    243            return super.connect(null);
    244        }
    245 
    246        async _send(cmd, body=null) {
    247            if (!this.hasConnection()) {
    248                await this.connect();
    249            }
    250            this.socket.send(JSON.stringify([cmd, body]));
    251        }
    252 
    253        /**
    254         * Send a message. The message object must be JSON-serializable.
    255         *
    256         * @param {Object} msg - The message object to send.
    257         */
    258        async send(msg) {
    259            await this._send("message", msg);
    260        }
    261 
    262        /**
    263         * Disconnect the associated `RecvChannel <#RecvChannel>`_, if
    264         * any, on the server side.
    265         */
    266        async disconnectReader() {
    267            await this._send("disconnectReader");
    268        }
    269 
    270        /**
    271         * Disconnect this channel on the server side.
    272         */
    273        async delete() {
    274            await this._send("delete");
    275        }
    276    };
    277    self.SendChannel = SendChannel;
    278 
    279    const recvChannelsCreated = new Set();
    280 
    281    /**
    282     * Receive messages over a channel
    283     */
    284    class RecvChannel extends Channel {
    285        type = "read";
    286 
    287        constructor(uuid) {
    288            if (recvChannelsCreated.has(uuid)) {
    289                throw new Error(`Already created RecvChannel with id ${uuid}`);
    290            }
    291            super(uuid);
    292            this.eventListeners.message = new Set();
    293        }
    294 
    295        async connect() {
    296            if (this.hasConnection()) {
    297                return;
    298            }
    299            await super.connect(event => this.readMessage(event.data));
    300        }
    301 
    302        readMessage(data) {
    303            let msg = JSON.parse(data);
    304            this._dispatch("message", msg);
    305        }
    306 
    307        /**
    308         * Wait for the next message and return it (after passing it to
    309         * existing handlers)
    310         *
    311         * @returns {Promise} - Promise that resolves to the message data.
    312         */
    313        nextMessage() {
    314            return new Promise(resolve => {
    315                let fn = ({data}) => {
    316                    this.removeEventListener("message", fn);
    317                    resolve(data);
    318                };
    319                this.addEventListener("message", fn);
    320            });
    321        }
    322    }
    323 
    324    /**
    325     * Create a new channel pair
    326     *
    327     * @returns {Array} - Array of [RecvChannel, SendChannel] for the same channel.
    328     */
    329    self.channel = function() {
    330        let uuid = createUuid();
    331        let recvChannel = new RecvChannel(uuid);
    332        let sendChannel = new SendChannel(uuid);
    333        return [recvChannel, sendChannel];
    334    };
    335 
    336    /**
    337     * Create an unconnected channel defined by a `uuid` in
    338     * ``location.href`` for listening for `RemoteGlobal
    339     * <#RemoteGlobal>`_ messages.
    340     *
    341     * @returns {RemoteGlobalCommandRecvChannel} - Disconnected channel
    342     */
    343    self.global_channel = function() {
    344        let uuid = new URLSearchParams(location.search).get("uuid");
    345        if (!uuid) {
    346            throw new Error("URL must have a uuid parameter to use as a RemoteGlobal");
    347        }
    348        return new RemoteGlobalCommandRecvChannel(new RecvChannel(uuid));
    349    };
    350 
    351    /**
    352     * Start listening for `RemoteGlobal <#RemoteGlobal>`_ messages on
    353     * a channel defined by a `uuid` in `location.href`
    354     *
    355     * @returns {RemoteGlobalCommandRecvChannel} - Connected channel
    356     */
    357    self.start_global_channel = async function() {
    358        let channel = self.global_channel();
    359        await channel.connect();
    360        return channel;
    361    };
    362 
    363    /**
    364     * Close all WebSockets used by channels in the current realm.
    365     *
    366     */
    367    self.close_all_channel_sockets = async function() {
    368        await socketCache.closeAll();
    369        // Spinning the event loop after the close events is necessary to
    370        // ensure that the channels really are closed and don't affect
    371        // bfcache behaviour in at least some implementations.
    372        await new Promise(resolve => setTimeout(resolve, 0));
    373    };
    374 
    375    /**
    376     * Handler for `RemoteGlobal <#RemoteGlobal>`_ commands.
    377     *
    378     * This can't be constructed directly but must be obtained from
    379     * `global_channel() <#global_channel>`_ or
    380     * `start_global_channel() <#start_global_channel>`_.
    381     */
    382    class RemoteGlobalCommandRecvChannel {
    383        constructor(recvChannel) {
    384            this.channel = recvChannel;
    385            this.uuid = recvChannel.uuid;
    386            this.channel.addEventListener("message", ({data}) => this.handleMessage(data));
    387            this.messageHandlers = new Set();
    388        };
    389 
    390        /**
    391         * Connect to the channel and start handling messages.
    392         */
    393        async connect() {
    394            await this.channel.connect();
    395        }
    396 
    397        /**
    398         * Close the channel and underlying websocket connection
    399         */
    400        async close() {
    401            await this.channel.close();
    402        }
    403 
    404        async handleMessage(msg) {
    405            const {id, command, params, respChannel} = msg;
    406            let result = {};
    407            let resp = {id, result};
    408            if (command === "call") {
    409                const fn = deserialize(params.fn);
    410                const args = params.args.map(deserialize);
    411                try {
    412                    let resultValue = await fn(...args);
    413                    result.result = serialize(resultValue);
    414                } catch(e) {
    415                    let exception = serialize(e);
    416                    const getAsInt = (obj, prop) =>  {
    417                        let value = prop in obj ? parseInt(obj[prop]) : 0;
    418                        return Number.isNaN(value) ? 0 : value;
    419                    };
    420                    result.exceptionDetails = {
    421                        text: e.toString(),
    422                        lineNumber: getAsInt(e, "lineNumber"),
    423                        columnNumber: getAsInt(e, "columnNumber"),
    424                        exception
    425                    };
    426                }
    427            } else if (command === "postMessage") {
    428                this.messageHandlers.forEach(fn => fn(deserialize(params.msg)));
    429            }
    430            if (respChannel) {
    431                let chan = deserialize(respChannel);
    432                await chan.connect();
    433                await chan.send(resp);
    434            }
    435        }
    436 
    437        /**
    438         * Add a handler for ``postMessage`` messages
    439         *
    440         * @param {Function} fn - Callback function that receives the
    441         * message.
    442         */
    443        addMessageHandler(fn) {
    444            this.messageHandlers.add(fn);
    445        }
    446 
    447        /**
    448         * Remove a handler for ``postMessage`` messages
    449         *
    450         * @param {Function} fn - Callback function to remove
    451         */
    452        removeMessageHandler(fn) {
    453            this.messageHandlers.delete(fn);
    454        }
    455 
    456        /**
    457         * Wait for the next ``postMessage`` message and return it
    458         * (after passing it to existing handlers)
    459         *
    460         * @returns {Promise} - Promise that resolves to the message.
    461         */
    462        nextMessage() {
    463            return new Promise(resolve => {
    464                let fn = (msg) => {
    465                    this.removeMessageHandler(fn);
    466                    resolve(msg);
    467                };
    468                this.addMessageHandler(fn);
    469            });
    470        }
    471    }
    472 
    473    class RemoteGlobalResponseRecvChannel {
    474        constructor(recvChannel) {
    475            this.channel = recvChannel;
    476            this.channel.addEventListener("message", ({data}) => this.handleMessage(data));
    477            this.responseHandlers = new Map();
    478        }
    479 
    480        setResponseHandler(commandId, fn) {
    481            this.responseHandlers.set(commandId, fn);
    482        }
    483 
    484        handleMessage(msg) {
    485            let {id, result} = msg;
    486            let handler = this.responseHandlers.get(id);
    487            if (handler) {
    488                this.responseHandlers.delete(id);
    489                handler(result);
    490            }
    491        }
    492 
    493        close() {
    494            return this.channel.close();
    495        }
    496    }
    497 
    498    /**
    499     * Object representing a remote global that has a
    500     * `RemoteGlobalCommandRecvChannel
    501     * <#RemoteGlobalCommandRecvChannel>`_
    502     */
    503    class RemoteGlobal {
    504        /**
    505         * Create a new RemoteGlobal object.
    506         *
    507         * This doesn't actually construct the global itself; that
    508         * must be done elsewhere, with a ``uuid`` query parameter in
    509         * its URL set to the same as the ``uuid`` property of this
    510         * object.
    511         *
    512         * @param {SendChannel|string} [dest] - Either a SendChannel
    513         * to the destination, or the UUID of the destination. If
    514         * ommitted, a new UUID is generated, which can be used when
    515         * constructing the URL for the global.
    516         *
    517         */
    518        constructor(dest) {
    519            if (dest === undefined || dest === null) {
    520                dest = createUuid();
    521            }
    522            if (typeof dest == "string") {
    523                /** UUID for the global */
    524                this.uuid = dest;
    525                this.sendChannel = new SendChannel(dest);
    526            } else if (dest instanceof SendChannel) {
    527                this.sendChannel = dest;
    528                this.uuid = dest.uuid;
    529            } else {
    530                throw new TypeError("Unrecognised type, expected string or SendChannel");
    531            }
    532            this.recvChannel = null;
    533            this.respChannel = null;
    534            this.connected = false;
    535            this.commandId = 0;
    536        }
    537 
    538        /**
    539         * Connect to the channel. Automatically called when sending the
    540         * first message
    541         */
    542        async connect() {
    543            if (this.connected) {
    544                return;
    545            }
    546            let [recvChannel, respChannel] = self.channel();
    547            await Promise.all([this.sendChannel.connect(), recvChannel.connect()]);
    548            this.recvChannel = new RemoteGlobalResponseRecvChannel(recvChannel);
    549            this.respChannel = respChannel;
    550            this.connected = true;
    551        }
    552 
    553        async sendMessage(command, params, hasResp=true) {
    554            if (!this.connected) {
    555                await this.connect();
    556            }
    557            let msg = {id: this.commandId++, command, params};
    558            if (hasResp) {
    559                msg.respChannel = serialize(this.respChannel);
    560            }
    561            let response;
    562            if (hasResp) {
    563                response = new Promise(resolve =>
    564                    this.recvChannel.setResponseHandler(msg.id, resolve));
    565            } else {
    566                response = null;
    567            }
    568            this.sendChannel.send(msg);
    569            return await response;
    570        }
    571 
    572        /**
    573         * Run the function ``fn`` in the remote global, passing arguments
    574         * ``args``, and return the result after awaiting any returned
    575         * promise.
    576         *
    577         * @param {Function} fn - Function to run in the remote global.
    578         * @param {...Any} args  - Arguments to pass to the function
    579         * @returns {Promise} - Promise resolving to the return value
    580         * of the function.
    581         */
    582        async call(fn, ...args) {
    583            let result = await this.sendMessage("call", {fn: serialize(fn), args: args.map(x => serialize(x))}, true);
    584            if (result.exceptionDetails) {
    585                throw deserialize(result.exceptionDetails.exception);
    586            }
    587            return deserialize(result.result);
    588        }
    589 
    590        /**
    591         * Post a message to the remote
    592         *
    593         * @param {Any} msg - The message to send.
    594         */
    595        async postMessage(msg) {
    596            await this.sendMessage("postMessage", {msg: serialize(msg)}, false);
    597        }
    598 
    599        /**
    600         * Disconnect the associated `RemoteGlobalCommandRecvChannel
    601         * <#RemoteGlobalCommandRecvChannel>`_, if any, on the server
    602         * side.
    603         *
    604         * @returns {Promise} - Resolved once the channel is disconnected.
    605         */
    606        disconnectReader() {
    607            // This causes any readers to disconnect until they are explicitly reconnected
    608            return this.sendChannel.disconnectReader();
    609        }
    610 
    611        /**
    612         * Close the channel and underlying websocket connections
    613         */
    614        close() {
    615            let closers = [this.sendChannel.close()];
    616            if (this.recvChannel !== null) {
    617                closers.push(this.recvChannel.close());
    618            }
    619            if (this.respChannel !== null) {
    620                closers.push(this.respChannel.close());
    621            }
    622            return Promise.all(closers);
    623        }
    624    }
    625 
    626    self.RemoteGlobal = RemoteGlobal;
    627 
    628    function typeName(value) {
    629        let type = typeof value;
    630        if (type === "undefined" ||
    631            type === "string" ||
    632            type === "boolean" ||
    633            type === "number" ||
    634            type === "bigint" ||
    635            type === "symbol" ||
    636            type === "function") {
    637            return type;
    638        }
    639 
    640        if (value === null) {
    641            return "null";
    642        }
    643        // The handling of cross-global objects here is broken
    644        if (value instanceof RemoteObject) {
    645            return "remoteobject";
    646        }
    647        if (value instanceof SendChannel) {
    648            return "sendchannel";
    649        }
    650        if (value instanceof RecvChannel) {
    651            return "recvchannel";
    652        }
    653        if (value instanceof Error) {
    654            return "error";
    655        }
    656        if (Array.isArray(value)) {
    657            return "array";
    658        }
    659        let constructor = value.constructor && value.constructor.name;
    660        if (constructor === "RegExp" ||
    661            constructor === "Date" ||
    662            constructor === "Map" ||
    663            constructor === "Set" ||
    664            constructor == "WeakMap" ||
    665            constructor == "WeakSet") {
    666            return constructor.toLowerCase();
    667        }
    668        // The handling of cross-global objects here is broken
    669        if (typeof window == "object" && window === self) {
    670            if (value instanceof Element) {
    671                return "element";
    672            }
    673            if (value instanceof Document) {
    674                return "document";
    675            }
    676            if (value instanceof Node) {
    677                return "node";
    678            }
    679            if (value instanceof Window) {
    680                return "window";
    681            }
    682        }
    683        if (Promise.resolve(value) === value) {
    684            return "promise";
    685        }
    686        return "object";
    687    }
    688 
    689    let remoteObjectsById = new Map();
    690 
    691    function remoteId(obj) {
    692        let rv;
    693        rv = createUuid();
    694        remoteObjectsById.set(rv, obj);
    695        return rv;
    696    }
    697 
    698    /**
    699     * Representation of a non-primitive type passed through a channel
    700     */
    701    class RemoteObject {
    702        constructor(type, objectId) {
    703            this.type = type;
    704            this.objectId = objectId;
    705        }
    706 
    707        /**
    708         * Create a RemoteObject containing a handle to reference obj
    709         *
    710         * @param {Any} obj - The object to reference.
    711         */
    712        static from(obj) {
    713            let type = typeName(obj);
    714            let id = remoteId(obj);
    715            return new RemoteObject(type, id);
    716        }
    717 
    718        /**
    719         * Return the local object referenced by the ``objectId`` of
    720         * this ``RemoteObject``, or ``null`` if there isn't a such an
    721         * object in this realm.
    722         */
    723        toLocal() {
    724            if (remoteObjectsById.has(this.objectId)) {
    725                return remoteObjectsById.get(this.objectId);
    726            }
    727            return null;
    728        }
    729 
    730        /**
    731         * Remove the object from the local cache. This means that future
    732         * calls to ``toLocal`` with the same objectId will always return
    733         * ``null``.
    734         */
    735        delete() {
    736            remoteObjectsById.delete(this.objectId);
    737        }
    738    }
    739 
    740    self.RemoteObject = RemoteObject;
    741 
    742    /**
    743     * Serialize an object as a JSON-compatible representation.
    744     *
    745     * The format used is similar (but not identical to)
    746     * `WebDriver-BiDi
    747     * <https://w3c.github.io/webdriver-bidi/#data-types-protocolValue>`_.
    748     *
    749     * Each item to be serialized can have the following fields:
    750     *
    751     * type - The name of the type being represented e.g. "string", or
    752     *  "map". For primitives this matches ``typeof``, but for
    753     *  ``object`` types that have particular support in the protocol
    754     *  e.g. arrays and maps, it is a custom value.
    755     *
    756     * value - A serialized representation of the object value. For
    757     * container types this is a JSON container (i.e. an object or an
    758     * array) containing a serialized representation of the child
    759     * values.
    760     *
    761     * objectId - An integer used to handle object graphs. Where
    762     * an object is present more than once in the serialization, the
    763     * first instance has both ``value`` and ``objectId`` fields, but
    764     * when encountered again, only ``objectId`` is present, with the
    765     * same value as the first instance of the object.
    766     *
    767     * @param {Any} inValue - The value to be serialized.
    768     * @returns {Object} - The serialized object value.
    769     */
    770    function serialize(inValue) {
    771        const queue = [{item: inValue}];
    772        let outValue = null;
    773 
    774        // Map from container object input to output value
    775        let objectsSeen = new Map();
    776        let lastObjectId = 0;
    777 
    778        /* Instead of making this recursive, use a queue holding the objects to be
    779         * serialized. Each item in the queue can have the following properties:
    780         *
    781         * item (required) - the input item to be serialized
    782         *
    783         * target - For collections, the output serialized object to
    784         * which the serialization of the current item will be added.
    785         *
    786         * targetName - For serializing object members, the name of
    787         * the property. For serializing maps either "key" or "value",
    788         * depending on whether the item represents a key or a value
    789         * in the map.
    790         */
    791        while (queue.length > 0) {
    792            const {item, target, targetName} = queue.shift();
    793            let type = typeName(item);
    794 
    795            let serialized = {type};
    796 
    797            if (objectsSeen.has(item)) {
    798                let outputValue = objectsSeen.get(item);
    799                if (!outputValue.hasOwnProperty("objectId")) {
    800                    outputValue.objectId = lastObjectId++;
    801                }
    802                serialized.objectId = outputValue.objectId;
    803            } else {
    804                switch (type) {
    805                case "undefined":
    806                case "null":
    807                    break;
    808                case "string":
    809                case "boolean":
    810                    serialized.value = item;
    811                    break;
    812                case "number":
    813                    if (item !== item) {
    814                        serialized.value = "NaN";
    815                    } else if (item === 0 && 1/item == Number.NEGATIVE_INFINITY) {
    816                        serialized.value = "-0";
    817                    } else if (item === Number.POSITIVE_INFINITY) {
    818                        serialized.value = "+Infinity";
    819                    } else if (item === Number.NEGATIVE_INFINITY) {
    820                        serialized.value = "-Infinity";
    821                    } else {
    822                        serialized.value = item;
    823                    }
    824                    break;
    825                case "bigint":
    826                case "function":
    827                    serialized.value = item.toString();
    828                    break;
    829                case "remoteobject":
    830                    serialized.value = {
    831                        type: item.type,
    832                        objectId: item.objectId
    833                    };
    834                    break;
    835                case "sendchannel":
    836                    serialized.value = item.uuid;
    837                    break;
    838                case "regexp":
    839                    serialized.value = {
    840                        pattern: item.source,
    841                        flags: item.flags
    842                    };
    843                    break;
    844                case "date":
    845                    serialized.value = Date.prototype.toJSON.call(item);
    846                    break;
    847                case "error":
    848                    serialized.value = {
    849                        type: item.constructor.name,
    850                        name: item.name,
    851                        message: item.message,
    852                        lineNumber: item.lineNumber,
    853                        columnNumber: item.columnNumber,
    854                        fileName: item.fileName,
    855                        stack: item.stack,
    856                    };
    857                    break;
    858                case "array":
    859                case "set":
    860                    serialized.value = [];
    861                    for (let child of item) {
    862                        queue.push({item: child, target: serialized});
    863                    }
    864                    break;
    865                case "object":
    866                    serialized.value = {};
    867                    for (let [targetName, child] of Object.entries(item)) {
    868                        queue.push({item: child, target: serialized, targetName});
    869                    }
    870                    break;
    871                case "map":
    872                    serialized.value = [];
    873                    for (let [childKey, childValue] of item.entries()) {
    874                        queue.push({item: childKey, target: serialized, targetName: "key"});
    875                        queue.push({item: childValue, target: serialized, targetName: "value"});
    876                    }
    877                    break;
    878                default:
    879                    throw new TypeError(`Can't serialize value of type ${type}; consider using RemoteObject.from() to wrap the object`);
    880                };
    881            }
    882            if (serialized.objectId === undefined) {
    883                objectsSeen.set(item, serialized);
    884            }
    885 
    886            if (target === undefined) {
    887                if (outValue !== null) {
    888                    throw new Error("Tried to create multiple output values");
    889                }
    890                outValue = serialized;
    891            } else {
    892                switch (target.type) {
    893                case "array":
    894                case "set":
    895                    target.value.push(serialized);
    896                    break;
    897                case "object":
    898                    target.value[targetName] = serialized;
    899                    break;
    900                case "map":
    901                    // We always serialize key and value as adjacent items in the queue,
    902                    // so when we get the key push a new output array and then the value will
    903                    // be added on the next iteration.
    904                    if (targetName === "key") {
    905                        target.value.push([]);
    906                    }
    907                    target.value[target.value.length - 1].push(serialized);
    908                    break;
    909                default:
    910                    throw new Error(`Unknown collection target type ${target.type}`);
    911                }
    912            }
    913        }
    914        return outValue;
    915    }
    916 
    917    /**
    918     * Deserialize an object from a JSON-compatible representation.
    919     *
    920     * For details on the serialized representation see serialize().
    921     *
    922     * @param {Object} obj - The value to be deserialized.
    923     * @returns {Any} - The deserialized value.
    924     */
    925    function deserialize(obj) {
    926        let deserialized = null;
    927        let queue = [{item: obj, target: null}];
    928        let objectMap = new Map();
    929 
    930        /* Instead of making this recursive, use a queue holding the objects to be
    931         * deserialized. Each item in the queue has the following properties:
    932         *
    933         * item - The input item to be deserialised.
    934         *
    935         * target - For members of a collection, a wrapper around the
    936         * output collection. This has a ``type`` field which is the
    937         * name of the collection type, and a ``value`` field which is
    938         * the actual output collection. For primitives, this is null.
    939         *
    940         * targetName - For object members, the property name on the
    941         * output object. For maps, "key" if the item is a key in the output map,
    942         * or "value" if it's a value in the output map.
    943         */
    944        while (queue.length > 0) {
    945            const {item, target, targetName} = queue.shift();
    946            const {type, value, objectId} = item;
    947            let result;
    948            let newTarget;
    949            if (objectId !== undefined && value === undefined) {
    950                result = objectMap.get(objectId);
    951            } else {
    952                switch(type) {
    953                case "undefined":
    954                    result = undefined;
    955                    break;
    956                case "null":
    957                    result = null;
    958                    break;
    959                case "string":
    960                case "boolean":
    961                    result = value;
    962                    break;
    963                case "number":
    964                    if (typeof value === "string") {
    965                        switch(value) {
    966                        case "NaN":
    967                            result = NaN;
    968                            break;
    969                        case "-0":
    970                            result = -0;
    971                            break;
    972                        case "+Infinity":
    973                            result = Number.POSITIVE_INFINITY;
    974                            break;
    975                        case "-Infinity":
    976                            result = Number.NEGATIVE_INFINITY;
    977                            break;
    978                        default:
    979                            throw new Error(`Unexpected number value "${value}"`);
    980                        }
    981                    } else {
    982                        result = value;
    983                    }
    984                    break;
    985                case "bigint":
    986                    result = BigInt(value);
    987                    break;
    988                case "function":
    989                    result = new Function("...args", `return (${value}).apply(null, args)`);
    990                    break;
    991                case "remoteobject":
    992                    let remote = new RemoteObject(value.type, value.objectId);
    993                    let local = remote.toLocal();
    994                    if (local !== null) {
    995                        result = local;
    996                    } else {
    997                        result = remote;
    998                    }
    999                    break;
   1000                case "sendchannel":
   1001                    result = new SendChannel(value);
   1002                    break;
   1003                case "regexp":
   1004                    result = new RegExp(value.pattern, value.flags);
   1005                    break;
   1006                case "date":
   1007                    result = new Date(value);
   1008                    break;
   1009                case "error":
   1010                    // The item.value.type property is the name of the error constructor.
   1011                    // If we have a constructor with the same name in the current realm,
   1012                    // construct an instance of that type, otherwise use a generic Error
   1013                    // type.
   1014                    if (item.value.type in self &&
   1015                        typeof self[item.value.type] === "function") {
   1016                        result = new self[item.value.type](item.value.message);
   1017                    } else {
   1018                        result = new Error(item.value.message);
   1019                    }
   1020                    result.name = item.value.name;
   1021                    result.lineNumber = item.value.lineNumber;
   1022                    result.columnNumber = item.value.columnNumber;
   1023                    result.fileName = item.value.fileName;
   1024                    result.stack = item.value.stack;
   1025                    break;
   1026                case "array":
   1027                    result = [];
   1028                    newTarget = {type, value: result};
   1029                    for (let child of value) {
   1030                        queue.push({item: child, target: newTarget});
   1031                    }
   1032                    break;
   1033                case "set":
   1034                    result = new Set();
   1035                    newTarget = {type, value: result};
   1036                    for (let child of value) {
   1037                        queue.push({item: child, target: newTarget});
   1038                    }
   1039                    break;
   1040                case "object":
   1041                    result = {};
   1042                    newTarget = {type, value: result};
   1043                    for (let [targetName, child] of Object.entries(value)) {
   1044                        queue.push({item: child, target: newTarget, targetName});
   1045                    }
   1046                    break;
   1047                case "map":
   1048                    result = new Map();
   1049                    newTarget = {type, value: result};
   1050                    for (let [key, child] of value) {
   1051                        queue.push({item: key, target: newTarget, targetName: "key"});
   1052                        queue.push({item: child, target: newTarget, targetName: "value"});
   1053                    }
   1054                    break;
   1055                default:
   1056                    throw new TypeError(`Can't deserialize object of type ${type}`);
   1057                }
   1058                if (objectId !== undefined) {
   1059                    objectMap.set(objectId, result);
   1060                }
   1061            }
   1062 
   1063            if (target === null) {
   1064                if (deserialized !== null) {
   1065                    throw new Error(`Tried to deserialized a non-root output value without a target`
   1066                                    ` container object.`);
   1067                }
   1068                deserialized = result;
   1069            } else {
   1070                switch(target.type) {
   1071                case "array":
   1072                    target.value.push(result);
   1073                    break;
   1074                case "set":
   1075                    target.value.add(result);
   1076                    break;
   1077                case "object":
   1078                    target.value[targetName] = result;
   1079                    break;
   1080                case "map":
   1081                    // For maps the same target wrapper is shared between key and value.
   1082                    // After deserializing the key, set the `key` property on the target
   1083                    // until we come to the value.
   1084                    if (targetName === "key") {
   1085                        target.key = result;
   1086                    } else {
   1087                        target.value.set(target.key, result);
   1088                    }
   1089                    break;
   1090                default:
   1091                    throw new Error(`Unknown target type ${target.type}`);
   1092                }
   1093            }
   1094        }
   1095        return deserialized;
   1096    }
   1097 })();