diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/resources/channel.sub.js | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/resources/channel.sub.js')
-rw-r--r-- | testing/web-platform/tests/resources/channel.sub.js | 1097 |
1 files changed, 1097 insertions, 0 deletions
diff --git a/testing/web-platform/tests/resources/channel.sub.js b/testing/web-platform/tests/resources/channel.sub.js new file mode 100644 index 0000000000..370d4f5905 --- /dev/null +++ b/testing/web-platform/tests/resources/channel.sub.js @@ -0,0 +1,1097 @@ +(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 explicitly 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; + } +})(); |