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