• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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})();
1098