(function() {
    function randInt(bits) {
        if (bits < 1 || bits > 53) {
            throw new TypeError();
        } else {
            if (bits >= 1 && bits <= 30) {
                return 0 | ((1 << bits) * Math.random());
            } else {
                var high = (0 | ((1 << (bits - 30)) * Math.random())) * (1 << 30);
                var low = 0 | ((1 << 30) * Math.random());
                return  high + low;
            }
        }
    }


    function toHex(x, length) {
        var rv = x.toString(16);
        while (rv.length < length) {
            rv = "0" + rv;
        }
        return rv;
    }

    function createUuid() {
        return [toHex(randInt(32), 8),
         toHex(randInt(16), 4),
         toHex(0x4000 | randInt(12), 4),
         toHex(0x8000 | randInt(14), 4),
         toHex(randInt(48), 12)].join("-");
    }


    /**
     * Cache of WebSocket instances per channel
     *
     * For reading there can only be one channel with each UUID, so we
     * just have a simple map of {uuid: WebSocket}. The socket can be
     * closed when the channel is closed.
     *
     * For writing there can be many channels for each uuid. Those can
     * share a websocket (within a specific global), so we have a map
     * of {uuid: [WebSocket, count]}.  Count is incremented when a
     * channel is opened with a given uuid, and decremented when its
     * closed. When the count reaches zero we can close the underlying
     * socket.
     */
    class SocketCache {
        constructor() {
            this.readSockets = new Map();
            this.writeSockets = new Map();
        };

        async getOrCreate(type, uuid, onmessage=null) {
            function createSocket() {
                let protocol = self.isSecureContext ? "wss" : "ws";
                let port = self.isSecureContext? "{{ports[wss][0]}}" : "{{ports[ws][0]}}";
                let url = `${protocol}://{{host}}:${port}/msg_channel?uuid=${uuid}&direction=${type}`;
                let socket = new WebSocket(url);
                if (onmessage !== null) {
                    socket.onmessage = onmessage;
                };
                return new Promise(resolve => socket.addEventListener("open", () => resolve(socket)));
            }

            let socket;
            if (type === "read") {
                if (this.readSockets.has(uuid)) {
                    throw new Error("Can't create multiple read sockets with same UUID");
                }
                socket = await createSocket();
                // If the socket is closed by the server, ensure it's removed from the cache
                socket.addEventListener("close", () => this.readSockets.delete(uuid));
                this.readSockets.set(uuid, socket);
            } else if (type === "write") {
                let count;
                if (onmessage !== null) {
                    throw new Error("Can't set message handler for write sockets");
                }
                if (this.writeSockets.has(uuid)) {
                    [socket, count] = this.writeSockets.get(uuid);
                } else {
                    socket = await createSocket();
                    count = 0;
                }
                count += 1;
                // If the socket is closed by the server, ensure it's removed from the cache
                socket.addEventListener("close", () => this.writeSockets.delete(uuid));
                this.writeSockets.set(uuid, [socket, count]);
            } else {
                throw new Error(`Unknown type ${type}`);
            }
            return socket;
        };

        async close(type, uuid) {
            let target = type === "read" ? this.readSockets : this.writeSockets;
            const data = target.get(uuid);
            if (!data) {
                return;
            }
            let count, socket;
            if (type == "read") {
                socket = data;
                count = 0;
            } else if (type === "write") {
                [socket, count] = data;
                count -= 1;
                if (count > 0) {
                    target.set(uuid, [socket, count]);
                }
            };
            if (count <= 0 && socket) {
                target.delete(uuid);
                socket.close(1000);
                await new Promise(resolve => socket.addEventListener("close", resolve));
            }
        };

        async closeAll() {
            let sockets = [];
            this.readSockets.forEach(value => sockets.push(value));
            this.writeSockets.forEach(value => sockets.push(value[0]));
            let closePromises = sockets.map(socket =>
                new Promise(resolve => socket.addEventListener("close", resolve)));
            sockets.forEach(socket => socket.close(1000));
            this.readSockets.clear();
            this.writeSockets.clear();
            await Promise.all(closePromises);
        }
    }

    const socketCache = new SocketCache();

    /**
     * Abstract base class for objects that allow sending / receiving
     * messages over a channel.
     */
    class Channel {
        type = null;

        constructor(uuid) {
            /** UUID for the channel */
            this.uuid = uuid;
            this.socket = null;
            this.eventListeners = {
                connect: new Set(),
                close: new Set()
            };
        }

        hasConnection() {
            return this.socket !== null && this.socket.readyState <= WebSocket.OPEN;
        }

        /**
         * Connect to the channel.
         *
         * @param {Function} onmessage - Event handler function for
         * the underlying websocket message.
         */
        async connect(onmessage) {
            if (this.hasConnection()) {
                return;
            }
            this.socket = await socketCache.getOrCreate(this.type, this.uuid, onmessage);
            this._dispatch("connect");
        }

        /**
         * Close the channel and underlying websocket connection
         */
        async close() {
            this.socket = null;
            await socketCache.close(this.type, this.uuid);
            this._dispatch("close");
        }

        /**
         * Add an event callback function. Supported message types are
         * "connect", "close", and "message" (for ``RecvChannel``).
         *
         * @param {string} type - Message type.
         * @param {Function} fn - Callback function. This is called
         * with an event-like object, with ``type`` and ``data``
         * properties.
         */
        addEventListener(type, fn) {
            if (typeof type !== "string") {
                throw new TypeError(`Expected string, got ${typeof type}`);
            }
            if (typeof fn !== "function") {
                throw new TypeError(`Expected function, got ${typeof fn}`);
            }
            if (!this.eventListeners.hasOwnProperty(type)) {
                throw new Error(`Unrecognised event type ${type}`);
            }
            this.eventListeners[type].add(fn);
        };

        /**
         * Remove an event callback function.
         *
         * @param {string} type - Event type.
         * @param {Function} fn - Callback function to remove.
         */
        removeEventListener(type, fn) {
            if (!typeof type === "string") {
                throw new TypeError(`Expected string, got ${typeof type}`);
            }
            if (typeof fn !== "function") {
                throw new TypeError(`Expected function, got ${typeof fn}`);
            }
            let listeners = this.eventListeners[type];
            if (listeners) {
                listeners.delete(fn);
            }
        };

        _dispatch(type, data) {
            let listeners = this.eventListeners[type];
            if (listeners) {
                // If any listener throws we end up not calling the other
                // listeners. This hopefully makes debugging easier, but
                // is different to DOM event listeners.
                listeners.forEach(fn => fn({type, data}));
            }
        };

    }

    /**
     * Send messages over a channel
     */
    class SendChannel extends Channel {
        type = "write";

        /**
         * Connect to the channel. Automatically called when sending the
         * first message.
         */
        async connect() {
            return super.connect(null);
        }

        async _send(cmd, body=null) {
            if (!this.hasConnection()) {
                await this.connect();
            }
            this.socket.send(JSON.stringify([cmd, body]));
        }

        /**
         * Send a message. The message object must be JSON-serializable.
         *
         * @param {Object} msg - The message object to send.
         */
        async send(msg) {
            await this._send("message", msg);
        }

        /**
         * Disconnect the associated `RecvChannel <#RecvChannel>`_, if
         * any, on the server side.
         */
        async disconnectReader() {
            await this._send("disconnectReader");
        }

        /**
         * Disconnect this channel on the server side.
         */
        async delete() {
            await this._send("delete");
        }
    };
    self.SendChannel = SendChannel;

    const recvChannelsCreated = new Set();

    /**
     * Receive messages over a channel
     */
    class RecvChannel extends Channel {
        type = "read";

        constructor(uuid) {
            if (recvChannelsCreated.has(uuid)) {
                throw new Error(`Already created RecvChannel with id ${uuid}`);
            }
            super(uuid);
            this.eventListeners.message = new Set();
        }

        async connect() {
            if (this.hasConnection()) {
                return;
            }
            await super.connect(event => this.readMessage(event.data));
        }

        readMessage(data) {
            let msg = JSON.parse(data);
            this._dispatch("message", msg);
        }

        /**
         * Wait for the next message and return it (after passing it to
         * existing handlers)
         *
         * @returns {Promise} - Promise that resolves to the message data.
         */
        nextMessage() {
            return new Promise(resolve => {
                let fn = ({data}) => {
                    this.removeEventListener("message", fn);
                    resolve(data);
                };
                this.addEventListener("message", fn);
            });
        }
    }

    /**
     * Create a new channel pair
     *
     * @returns {Array} - Array of [RecvChannel, SendChannel] for the same channel.
     */
    self.channel = function() {
        let uuid = createUuid();
        let recvChannel = new RecvChannel(uuid);
        let sendChannel = new SendChannel(uuid);
        return [recvChannel, sendChannel];
    };

    /**
     * Create an unconnected channel defined by a `uuid` in
     * ``location.href`` for listening for `RemoteGlobal
     * <#RemoteGlobal>`_ messages.
     *
     * @returns {RemoteGlobalCommandRecvChannel} - Disconnected channel
     */
    self.global_channel = function() {
        let uuid = new URLSearchParams(location.search).get("uuid");
        if (!uuid) {
            throw new Error("URL must have a uuid parameter to use as a RemoteGlobal");
        }
        return new RemoteGlobalCommandRecvChannel(new RecvChannel(uuid));
    };

    /**
     * Start listening for `RemoteGlobal <#RemoteGlobal>`_ messages on
     * a channel defined by a `uuid` in `location.href`
     *
     * @returns {RemoteGlobalCommandRecvChannel} - Connected channel
     */
    self.start_global_channel = async function() {
        let channel = self.global_channel();
        await channel.connect();
        return channel;
    };

    /**
     * Close all WebSockets used by channels in the current realm.
     *
     */
    self.close_all_channel_sockets = async function() {
        await socketCache.closeAll();
        // Spinning the event loop after the close events is necessary to
        // ensure that the channels really are closed and don't affect
        // bfcache behaviour in at least some implementations.
        await new Promise(resolve => setTimeout(resolve, 0));
    };

    /**
     * Handler for `RemoteGlobal <#RemoteGlobal>`_ commands.
     *
     * This can't be constructed directly but must be obtained from
     * `global_channel() <#global_channel>`_ or
     * `start_global_channel() <#start_global_channel>`_.
     */
    class RemoteGlobalCommandRecvChannel {
        constructor(recvChannel) {
            this.channel = recvChannel;
            this.uuid = recvChannel.uuid;
            this.channel.addEventListener("message", ({data}) => this.handleMessage(data));
            this.messageHandlers = new Set();
        };

        /**
         * Connect to the channel and start handling messages.
         */
        async connect() {
            await this.channel.connect();
        }

        /**
         * Close the channel and underlying websocket connection
         */
        async close() {
            await this.channel.close();
        }

        async handleMessage(msg) {
            const {id, command, params, respChannel} = msg;
            let result = {};
            let resp = {id, result};
            if (command === "call") {
                const fn = deserialize(params.fn);
                const args = params.args.map(deserialize);
                try {
                    let resultValue = await fn(...args);
                    result.result = serialize(resultValue);
                } catch(e) {
                    let exception = serialize(e);
                    const getAsInt = (obj, prop) =>  {
                        let value = prop in obj ? parseInt(obj[prop]) : 0;
                        return Number.isNaN(value) ? 0 : value;
                    };
                    result.exceptionDetails = {
                        text: e.toString(),
                        lineNumber: getAsInt(e, "lineNumber"),
                        columnNumber: getAsInt(e, "columnNumber"),
                        exception
                    };
                }
            } else if (command === "postMessage") {
                this.messageHandlers.forEach(fn => fn(deserialize(params.msg)));
            }
            if (respChannel) {
                let chan = deserialize(respChannel);
                await chan.connect();
                await chan.send(resp);
            }
        }

        /**
         * Add a handler for ``postMessage`` messages
         *
         * @param {Function} fn - Callback function that receives the
         * message.
         */
        addMessageHandler(fn) {
            this.messageHandlers.add(fn);
        }

        /**
         * Remove a handler for ``postMessage`` messages
         *
         * @param {Function} fn - Callback function to remove
         */
        removeMessageHandler(fn) {
            this.messageHandlers.delete(fn);
        }

        /**
         * Wait for the next ``postMessage`` message and return it
         * (after passing it to existing handlers)
         *
         * @returns {Promise} - Promise that resolves to the message.
         */
        nextMessage() {
            return new Promise(resolve => {
                let fn = (msg) => {
                    this.removeMessageHandler(fn);
                    resolve(msg);
                };
                this.addMessageHandler(fn);
            });
        }
    }

    class RemoteGlobalResponseRecvChannel {
        constructor(recvChannel) {
            this.channel = recvChannel;
            this.channel.addEventListener("message", ({data}) => this.handleMessage(data));
            this.responseHandlers = new Map();
        }

        setResponseHandler(commandId, fn) {
            this.responseHandlers.set(commandId, fn);
        }

        handleMessage(msg) {
            let {id, result} = msg;
            let handler = this.responseHandlers.get(id);
            if (handler) {
                this.responseHandlers.delete(id);
                handler(result);
            }
        }

        close() {
            return this.channel.close();
        }
    }

    /**
     * Object representing a remote global that has a
     * `RemoteGlobalCommandRecvChannel
     * <#RemoteGlobalCommandRecvChannel>`_
     */
    class RemoteGlobal {
        /**
         * Create a new RemoteGlobal object.
         *
         * This doesn't actually construct the global itself; that
         * must be done elsewhere, with a ``uuid`` query parameter in
         * its URL set to the same as the ``uuid`` property of this
         * object.
         *
         * @param {SendChannel|string} [dest] - Either a SendChannel
         * to the destination, or the UUID of the destination. If
         * ommitted, a new UUID is generated, which can be used when
         * constructing the URL for the global.
         *
         */
        constructor(dest) {
            if (dest === undefined || dest === null) {
                dest = createUuid();
            }
            if (typeof dest == "string") {
                /** UUID for the global */
                this.uuid = dest;
                this.sendChannel = new SendChannel(dest);
            } else if (dest instanceof SendChannel) {
                this.sendChannel = dest;
                this.uuid = dest.uuid;
            } else {
                throw new TypeError("Unrecognised type, expected string or SendChannel");
            }
            this.recvChannel = null;
            this.respChannel = null;
            this.connected = false;
            this.commandId = 0;
        }

        /**
         * Connect to the channel. Automatically called when sending the
         * first message
         */
        async connect() {
            if (this.connected) {
                return;
            }
            let [recvChannel, respChannel] = self.channel();
            await Promise.all([this.sendChannel.connect(), recvChannel.connect()]);
            this.recvChannel = new RemoteGlobalResponseRecvChannel(recvChannel);
            this.respChannel = respChannel;
            this.connected = true;
        }

        async sendMessage(command, params, hasResp=true) {
            if (!this.connected) {
                await this.connect();
            }
            let msg = {id: this.commandId++, command, params};
            if (hasResp) {
                msg.respChannel = serialize(this.respChannel);
            }
            let response;
            if (hasResp) {
                response = new Promise(resolve =>
                    this.recvChannel.setResponseHandler(msg.id, resolve));
            } else {
                response = null;
            }
            this.sendChannel.send(msg);
            return await response;
        }

        /**
         * Run the function ``fn`` in the remote global, passing arguments
         * ``args``, and return the result after awaiting any returned
         * promise.
         *
         * @param {Function} fn - Function to run in the remote global.
         * @param {...Any} args  - Arguments to pass to the function
         * @returns {Promise} - Promise resolving to the return value
         * of the function.
         */
        async call(fn, ...args) {
            let result = await this.sendMessage("call", {fn: serialize(fn), args: args.map(x => serialize(x))}, true);
            if (result.exceptionDetails) {
                throw deserialize(result.exceptionDetails.exception);
            }
            return deserialize(result.result);
        }

        /**
         * Post a message to the remote
         *
         * @param {Any} msg - The message to send.
         */
        async postMessage(msg) {
            await this.sendMessage("postMessage", {msg: serialize(msg)}, false);
        }

        /**
         * Disconnect the associated `RemoteGlobalCommandRecvChannel
         * <#RemoteGlobalCommandRecvChannel>`_, if any, on the server
         * side.
         *
         * @returns {Promise} - Resolved once the channel is disconnected.
         */
        disconnectReader() {
            // This causes any readers to disconnect until they are explictly reconnected
            return this.sendChannel.disconnectReader();
        }

        /**
         * Close the channel and underlying websocket connections
         */
        close() {
            let closers = [this.sendChannel.close()];
            if (this.recvChannel !== null) {
                closers.push(this.recvChannel.close());
            }
            if (this.respChannel !== null) {
                closers.push(this.respChannel.close());
            }
            return Promise.all(closers);
        }
    }

    self.RemoteGlobal = RemoteGlobal;

    function typeName(value) {
        let type = typeof value;
        if (type === "undefined" ||
            type === "string" ||
            type === "boolean" ||
            type === "number" ||
            type === "bigint" ||
            type === "symbol" ||
            type === "function") {
            return type;
        }

        if (value === null) {
            return "null";
        }
        // The handling of cross-global objects here is broken
        if (value instanceof RemoteObject) {
            return "remoteobject";
        }
        if (value instanceof SendChannel) {
            return "sendchannel";
        }
        if (value instanceof RecvChannel) {
            return "recvchannel";
        }
        if (value instanceof Error) {
            return "error";
        }
        if (Array.isArray(value)) {
            return "array";
        }
        let constructor = value.constructor && value.constructor.name;
        if (constructor === "RegExp" ||
            constructor === "Date" ||
            constructor === "Map" ||
            constructor === "Set" ||
            constructor == "WeakMap" ||
            constructor == "WeakSet") {
            return constructor.toLowerCase();
        }
        // The handling of cross-global objects here is broken
        if (typeof window == "object" && window === self) {
            if (value instanceof Element) {
                return "element";
            }
            if (value instanceof Document) {
                return "document";
            }
            if (value instanceof Node) {
                return "node";
            }
            if (value instanceof Window) {
                return "window";
            }
        }
        if (Promise.resolve(value) === value) {
            return "promise";
        }
        return "object";
    }

    let remoteObjectsById = new Map();

    function remoteId(obj) {
        let rv;
        rv = createUuid();
        remoteObjectsById.set(rv, obj);
        return rv;
    }

    /**
     * Representation of a non-primitive type passed through a channel
     */
    class RemoteObject {
        constructor(type, objectId) {
            this.type = type;
            this.objectId = objectId;
        }

        /**
         * Create a RemoteObject containing a handle to reference obj
         *
         * @param {Any} obj - The object to reference.
         */
        static from(obj) {
            let type = typeName(obj);
            let id = remoteId(obj);
            return new RemoteObject(type, id);
        }

        /**
         * Return the local object referenced by the ``objectId`` of
         * this ``RemoteObject``, or ``null`` if there isn't a such an
         * object in this realm.
         */
        toLocal() {
            if (remoteObjectsById.has(this.objectId)) {
                return remoteObjectsById.get(this.objectId);
            }
            return null;
        }

        /**
         * Remove the object from the local cache. This means that future
         * calls to ``toLocal`` with the same objectId will always return
         * ``null``.
         */
        delete() {
            remoteObjectsById.delete(this.objectId);
        }
    }

    self.RemoteObject = RemoteObject;

    /**
     * Serialize an object as a JSON-compatible representation.
     *
     * The format used is similar (but not identical to)
     * `WebDriver-BiDi
     * <https://w3c.github.io/webdriver-bidi/#data-types-protocolValue>`_.
     *
     * Each item to be serialized can have the following fields:
     *
     * type - The name of the type being represented e.g. "string", or
     *  "map". For primitives this matches ``typeof``, but for
     *  ``object`` types that have particular support in the protocol
     *  e.g. arrays and maps, it is a custom value.
     *
     * value - A serialized representation of the object value. For
     * container types this is a JSON container (i.e. an object or an
     * array) containing a serialized representation of the child
     * values.
     *
     * objectId - An integer used to handle object graphs. Where
     * an object is present more than once in the serialization, the
     * first instance has both ``value`` and ``objectId`` fields, but
     * when encountered again, only ``objectId`` is present, with the
     * same value as the first instance of the object.
     *
     * @param {Any} inValue - The value to be serialized.
     * @returns {Object} - The serialized object value.
     */
    function serialize(inValue) {
        const queue = [{item: inValue}];
        let outValue = null;

        // Map from container object input to output value
        let objectsSeen = new Map();
        let lastObjectId = 0;

        /* Instead of making this recursive, use a queue holding the objects to be
         * serialized. Each item in the queue can have the following properties:
         *
         * item (required) - the input item to be serialized
         *
         * target - For collections, the output serialized object to
         * which the serialization of the current item will be added.
         *
         * targetName - For serializing object members, the name of
         * the property. For serializing maps either "key" or "value",
         * depending on whether the item represents a key or a value
         * in the map.
         */
        while (queue.length > 0) {
            const {item, target, targetName} = queue.shift();
            let type = typeName(item);

            let serialized = {type};

            if (objectsSeen.has(item)) {
                let outputValue = objectsSeen.get(item);
                if (!outputValue.hasOwnProperty("objectId")) {
                    outputValue.objectId = lastObjectId++;
                }
                serialized.objectId = outputValue.objectId;
            } else {
                switch (type) {
                case "undefined":
                case "null":
                    break;
                case "string":
                case "boolean":
                    serialized.value = item;
                    break;
                case "number":
                    if (item !== item) {
                        serialized.value = "NaN";
                    } else if (item === 0 && 1/item == Number.NEGATIVE_INFINITY) {
                        serialized.value = "-0";
                    } else if (item === Number.POSITIVE_INFINITY) {
                        serialized.value = "+Infinity";
                    } else if (item === Number.NEGATIVE_INFINITY) {
                        serialized.value = "-Infinity";
                    } else {
                        serialized.value = item;
                    }
                    break;
                case "bigint":
                case "function":
                    serialized.value = item.toString();
                    break;
                case "remoteobject":
                    serialized.value = {
                        type: item.type,
                        objectId: item.objectId
                    };
                    break;
                case "sendchannel":
                    serialized.value = item.uuid;
                    break;
                case "regexp":
                    serialized.value = {
                        pattern: item.source,
                        flags: item.flags
                    };
                    break;
                case "date":
                    serialized.value = Date.prototype.toJSON.call(item);
                    break;
                case "error":
                    serialized.value = {
                        type: item.constructor.name,
                        name: item.name,
                        message: item.message,
                        lineNumber: item.lineNumber,
                        columnNumber: item.columnNumber,
                        fileName: item.fileName,
                        stack: item.stack,
                    };
                    break;
                case "array":
                case "set":
                    serialized.value = [];
                    for (let child of item) {
                        queue.push({item: child, target: serialized});
                    }
                    break;
                case "object":
                    serialized.value = {};
                    for (let [targetName, child] of Object.entries(item)) {
                        queue.push({item: child, target: serialized, targetName});
                    }
                    break;
                case "map":
                    serialized.value = [];
                    for (let [childKey, childValue] of item.entries()) {
                        queue.push({item: childKey, target: serialized, targetName: "key"});
                        queue.push({item: childValue, target: serialized, targetName: "value"});
                    }
                    break;
                default:
                    throw new TypeError(`Can't serialize value of type ${type}; consider using RemoteObject.from() to wrap the object`);
                };
            }
            if (serialized.objectId === undefined) {
                objectsSeen.set(item, serialized);
            }

            if (target === undefined) {
                if (outValue !== null) {
                    throw new Error("Tried to create multiple output values");
                }
                outValue = serialized;
            } else {
                switch (target.type) {
                case "array":
                case "set":
                    target.value.push(serialized);
                    break;
                case "object":
                    target.value[targetName] = serialized;
                    break;
                case "map":
                    // We always serialize key and value as adjacent items in the queue,
                    // so when we get the key push a new output array and then the value will
                    // be added on the next iteration.
                    if (targetName === "key") {
                        target.value.push([]);
                    }
                    target.value[target.value.length - 1].push(serialized);
                    break;
                default:
                    throw new Error(`Unknown collection target type ${target.type}`);
                }
            }
        }
        return outValue;
    }

    /**
     * Deserialize an object from a JSON-compatible representation.
     *
     * For details on the serialized representation see serialize().
     *
     * @param {Object} obj - The value to be deserialized.
     * @returns {Any} - The deserialized value.
     */
    function deserialize(obj) {
        let deserialized = null;
        let queue = [{item: obj, target: null}];
        let objectMap = new Map();

        /* Instead of making this recursive, use a queue holding the objects to be
         * deserialized. Each item in the queue has the following properties:
         *
         * item - The input item to be deserialised.
         *
         * target - For members of a collection, a wrapper around the
         * output collection. This has a ``type`` field which is the
         * name of the collection type, and a ``value`` field which is
         * the actual output collection. For primitives, this is null.
         *
         * targetName - For object members, the property name on the
         * output object. For maps, "key" if the item is a key in the output map,
         * or "value" if it's a value in the output map.
         */
        while (queue.length > 0) {
            const {item, target, targetName} = queue.shift();
            const {type, value, objectId} = item;
            let result;
            let newTarget;
            if (objectId !== undefined && value === undefined) {
                result = objectMap.get(objectId);
            } else {
                switch(type) {
                case "undefined":
                    result = undefined;
                    break;
                case "null":
                    result = null;
                    break;
                case "string":
                case "boolean":
                    result = value;
                    break;
                case "number":
                    if (typeof value === "string") {
                        switch(value) {
                        case "NaN":
                            result = NaN;
                            break;
                        case "-0":
                            result = -0;
                            break;
                        case "+Infinity":
                            result = Number.POSITIVE_INFINITY;
                            break;
                        case "-Infinity":
                            result = Number.NEGATIVE_INFINITY;
                            break;
                        default:
                            throw new Error(`Unexpected number value "${value}"`);
                        }
                    } else {
                        result = value;
                    }
                    break;
                case "bigint":
                    result = BigInt(value);
                    break;
                case "function":
                    result = new Function("...args", `return (${value}).apply(null, args)`);
                    break;
                case "remoteobject":
                    let remote = new RemoteObject(value.type, value.objectId);
                    let local = remote.toLocal();
                    if (local !== null) {
                        result = local;
                    } else {
                        result = remote;
                    }
                    break;
                case "sendchannel":
                    result = new SendChannel(value);
                    break;
                case "regexp":
                    result = new RegExp(value.pattern, value.flags);
                    break;
                case "date":
                    result = new Date(value);
                    break;
                case "error":
                    // The item.value.type property is the name of the error constructor.
                    // If we have a constructor with the same name in the current realm,
                    // construct an instance of that type, otherwise use a generic Error
                    // type.
                    if (item.value.type in self &&
                        typeof self[item.value.type] === "function") {
                        result = new self[item.value.type](item.value.message);
                    } else {
                        result = new Error(item.value.message);
                    }
                    result.name = item.value.name;
                    result.lineNumber = item.value.lineNumber;
                    result.columnNumber = item.value.columnNumber;
                    result.fileName = item.value.fileName;
                    result.stack = item.value.stack;
                    break;
                case "array":
                    result = [];
                    newTarget = {type, value: result};
                    for (let child of value) {
                        queue.push({item: child, target: newTarget});
                    }
                    break;
                case "set":
                    result = new Set();
                    newTarget = {type, value: result};
                    for (let child of value) {
                        queue.push({item: child, target: newTarget});
                    }
                    break;
                case "object":
                    result = {};
                    newTarget = {type, value: result};
                    for (let [targetName, child] of Object.entries(value)) {
                        queue.push({item: child, target: newTarget, targetName});
                    }
                    break;
                case "map":
                    result = new Map();
                    newTarget = {type, value: result};
                    for (let [key, child] of value) {
                        queue.push({item: key, target: newTarget, targetName: "key"});
                        queue.push({item: child, target: newTarget, targetName: "value"});
                    }
                    break;
                default:
                    throw new TypeError(`Can't deserialize object of type ${type}`);
                }
                if (objectId !== undefined) {
                    objectMap.set(objectId, result);
                }
            }

            if (target === null) {
                if (deserialized !== null) {
                    throw new Error(`Tried to deserialized a non-root output value without a target`
                                    ` container object.`);
                }
                deserialized = result;
            } else {
                switch(target.type) {
                case "array":
                    target.value.push(result);
                    break;
                case "set":
                    target.value.add(result);
                    break;
                case "object":
                    target.value[targetName] = result;
                    break;
                case "map":
                    // For maps the same target wrapper is shared between key and value.
                    // After deserializing the key, set the `key` property on the target
                    // until we come to the value.
                    if (targetName === "key") {
                        target.key = result;
                    } else {
                        target.value.set(target.key, result);
                    }
                    break;
                default:
                    throw new Error(`Unknown target type ${target.type}`);
                }
            }
        }
        return deserialized;
    }
})();
