1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
9 dom: "chrome://remote/content/shared/DOM.sys.mjs",
10 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
11 generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
12 Log: "chrome://remote/content/shared/Log.sys.mjs",
15 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
16 lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
20 * @typedef {object} IncludeShadowTreeMode
24 * Enum of include shadow tree modes supported by the serialization.
27 * @enum {IncludeShadowTreeMode}
29 export const IncludeShadowTreeMode = {
36 * @typedef {object} OwnershipModel
40 * Enum of ownership models supported by the serialization.
43 * @enum {OwnershipModel}
45 export const OwnershipModel = {
51 * Extra options for deserializing remote values.
53 * @typedef {object} ExtraDeserializationOptions
55 * @property {NodeCache=} nodeCache
56 * The cache containing DOM node references.
57 * @property {Function=} emitScriptMessage
58 * The function to emit "script.message" event.
62 * Extra options for serializing remote values.
64 * @typedef {object} ExtraSerializationOptions
66 * @property {NodeCache=} nodeCache
67 * The cache containing DOM node references.
68 * @property {Map<BrowsingContext, Array<string>>} seenNodeIds
69 * Map of browsing contexts to their seen node ids during the current
74 * An object which holds the information of how
75 * ECMAScript objects should be serialized.
77 * @typedef {object} SerializationOptions
79 * @property {number} [maxDomDepth=0]
80 * Depth of a serialization of DOM Nodes. Defaults to 0.
81 * @property {number} [maxObjectDepth=null]
82 * Depth of a serialization of objects. Defaults to null.
83 * @property {IncludeShadowTreeMode} [includeShadowTree=IncludeShadowTreeMode.None]
84 * Mode of a serialization of shadow dom. Defaults to "none".
87 const TYPED_ARRAY_CLASSES = [
102 * Build the serialized RemoteValue.
105 * An object with a mandatory `type` property, and optional `handle`,
106 * depending on the OwnershipModel, used for the serialization and
107 * on the value's type.
109 function buildSerialized(type, handle = null) {
110 const serialized = { type };
112 if (handle !== null) {
113 serialized.handle = handle;
120 * Helper to deserialize value list.
122 * @see https://w3c.github.io/webdriver-bidi/#deserialize-value-list
124 * @param {Realm} realm
125 * The Realm in which the value is deserialized.
126 * @param {Array} serializedValueList
127 * List of serialized values.
128 * @param {ExtraDeserializationOptions} extraOptions
129 * Extra Remote Value deserialization options.
131 * @returns {Array} List of deserialized values.
133 * @throws {InvalidArgumentError}
134 * If <var>serializedValueList</var> is not an array.
136 function deserializeValueList(realm, serializedValueList, extraOptions) {
139 `Expected "serializedValueList" to be an array, got ${serializedValueList}`
142 const deserializedValues = [];
144 for (const item of serializedValueList) {
145 deserializedValues.push(deserialize(realm, item, extraOptions));
148 return deserializedValues;
152 * Helper to deserialize key-value list.
154 * @see https://w3c.github.io/webdriver-bidi/#deserialize-key-value-list
156 * @param {Realm} realm
157 * The Realm in which the value is deserialized.
158 * @param {Array} serializedKeyValueList
159 * List of serialized key-value.
160 * @param {ExtraDeserializationOptions} extraOptions
161 * Extra Remote Value deserialization options.
163 * @returns {Array} List of deserialized key-value.
165 * @throws {InvalidArgumentError}
166 * If <var>serializedKeyValueList</var> is not an array or
167 * not an array of key-value arrays.
169 function deserializeKeyValueList(realm, serializedKeyValueList, extraOptions) {
171 serializedKeyValueList,
172 `Expected "serializedKeyValueList" to be an array, got ${serializedKeyValueList}`
175 const deserializedKeyValueList = [];
177 for (const serializedKeyValue of serializedKeyValueList) {
178 if (!Array.isArray(serializedKeyValue) || serializedKeyValue.length != 2) {
179 throw new lazy.error.InvalidArgumentError(
180 `Expected key-value pair to be an array with 2 elements, got ${serializedKeyValue}`
183 const [serializedKey, serializedValue] = serializedKeyValue;
184 const deserializedKey =
185 typeof serializedKey == "string"
187 : deserialize(realm, serializedKey, extraOptions);
188 const deserializedValue = deserialize(realm, serializedValue, extraOptions);
190 deserializedKeyValueList.push([deserializedKey, deserializedValue]);
193 return deserializedKeyValueList;
197 * Deserialize a Node as referenced by the shared unique reference.
199 * This unique reference can be shared by WebDriver clients with the WebDriver
200 * classic implementation (Marionette) if the reference is for an Element or
203 * @param {string} sharedRef
204 * Shared unique reference of the Node.
205 * @param {Realm} realm
206 * The Realm in which the value is deserialized.
207 * @param {ExtraDeserializationOptions} extraOptions
208 * Extra Remote Value deserialization options.
210 * @returns {Node} The deserialized DOM node.
212 function deserializeSharedReference(sharedRef, realm, extraOptions) {
213 const { nodeCache } = extraOptions;
215 const browsingContext = realm.browsingContext;
216 if (!browsingContext) {
217 throw new lazy.error.NoSuchNodeError("Realm isn't a Window global");
220 const node = nodeCache.getNode(browsingContext, sharedRef);
223 throw new lazy.error.NoSuchNodeError(
224 `The node with the reference ${sharedRef} is not known`
228 // Bug 1819902: Instead of a browsing context check compare the origin
229 const isSameBrowsingContext = sharedRef => {
230 const nodeDetails = nodeCache.getReferenceDetails(sharedRef);
232 if (nodeDetails.isTopBrowsingContext && browsingContext.parent === null) {
233 // As long as Navigables are not available any cross-group navigation will
234 // cause a swap of the current top-level browsing context. The only unique
235 // identifier in such a case is the browser id the top-level browsing
236 // context actually lives in.
237 return nodeDetails.browserId === browsingContext.browserId;
240 return nodeDetails.browsingContextId === browsingContext.id;
243 if (!isSameBrowsingContext(sharedRef)) {
251 * Deserialize a local value.
253 * @see https://w3c.github.io/webdriver-bidi/#deserialize-local-value
255 * @param {Realm} realm
256 * The Realm in which the value is deserialized.
257 * @param {object} serializedValue
258 * Value of any type to be deserialized.
259 * @param {ExtraDeserializationOptions} extraOptions
260 * Extra Remote Value deserialization options.
262 * @returns {object} Deserialized representation of the value.
264 export function deserialize(realm, serializedValue, extraOptions) {
265 const { handle, sharedId, type, value } = serializedValue;
267 // With a shared id present deserialize as node reference.
268 if (sharedId !== undefined) {
271 `Expected "sharedId" to be a string, got ${sharedId}`
274 return deserializeSharedReference(sharedId, realm, extraOptions);
277 // With a handle present deserialize as remote reference.
278 if (handle !== undefined) {
281 `Expected "handle" to be a string, got ${handle}`
284 const object = realm.getObjectForHandle(handle);
286 throw new lazy.error.NoSuchHandleError(
287 `Unable to find an object reference for "handle" ${handle}`
294 lazy.assert.string(type, `Expected "type" to be a string, got ${type}`);
296 // Primitive protocol values
305 `Expected "value" to be a string, got ${value}`
309 // If value is already a number return its value.
310 if (typeof value === "number") {
314 // Otherwise it has to be one of the special strings
317 ["NaN", "-0", "Infinity", "-Infinity"],
318 `Expected "value" to be one of "NaN", "-0", "Infinity", "-Infinity", got ${value}`
320 return Number(value);
324 `Expected "value" to be a boolean, got ${value}`
330 `Expected "value" to be a string, got ${value}`
333 return BigInt(value);
335 throw new lazy.error.InvalidArgumentError(
336 `Failed to deserialize value as BigInt: ${value}`
342 const channel = message =>
343 extraOptions.emitScriptMessage(realm, value, message);
344 return realm.cloneIntoRealm(channel);
347 // Non-primitive protocol values
349 const array = realm.cloneIntoRealm([]);
350 deserializeValueList(realm, value, extraOptions).forEach(v =>
355 // We want to support only Date Time String format,
356 // check if the value follows it.
357 if (!ChromeUtils.isISOStyleDate(value)) {
358 throw new lazy.error.InvalidArgumentError(
359 `Expected "value" for Date to be a Date Time string, got ${value}`
363 return realm.cloneIntoRealm(new Date(value));
365 const map = realm.cloneIntoRealm(new Map());
366 deserializeKeyValueList(realm, value, extraOptions).forEach(([k, v]) =>
372 const object = realm.cloneIntoRealm({});
373 deserializeKeyValueList(realm, value, extraOptions).forEach(
374 ([k, v]) => (object[k] = v)
380 `Expected "value" for RegExp to be an object, got ${value}`
382 const { pattern, flags } = value;
385 `Expected "pattern" for RegExp to be a string, got ${pattern}`
387 if (flags !== undefined) {
390 `Expected "flags" for RegExp to be a string, got ${flags}`
394 return realm.cloneIntoRealm(new RegExp(pattern, flags));
396 throw new lazy.error.InvalidArgumentError(
397 `Failed to deserialize value as RegExp: ${value}`
401 const set = realm.cloneIntoRealm(new Set());
402 deserializeValueList(realm, value, extraOptions).forEach(v => set.add(v));
406 lazy.logger.warn(`Unsupported type for local value ${type}`);
411 * Helper to retrieve the handle id for a given object, for the provided realm
412 * and ownership type.
414 * See https://w3c.github.io/webdriver-bidi/#handle-for-an-object
416 * @param {Realm} realm
417 * The Realm from which comes the value being serialized.
418 * @param {OwnershipModel} ownershipType
419 * The ownership model to use for this serialization.
420 * @param {object} object
421 * The object being serialized.
423 * @returns {string} The unique handle id for the object. Will be null if the
424 * Ownership type is "none".
426 function getHandleForObject(realm, ownershipType, object) {
427 if (ownershipType === OwnershipModel.None) {
430 return realm.getHandleForObject(object);
434 * Gets or creates a new shared unique reference for the DOM node.
436 * This unique reference can be shared by WebDriver clients with the WebDriver
437 * classic implementation (Marionette) if the reference is for an Element or
441 * Node to create the unique reference for.
442 * @param {ExtraSerializationOptions} extraOptions
443 * Extra Remote Value serialization options.
446 * Shared unique reference for the Node.
448 function getSharedIdForNode(node, extraOptions) {
449 const { nodeCache, seenNodeIds } = extraOptions;
451 if (!Node.isInstance(node)) {
455 const browsingContext = node.ownerGlobal.browsingContext;
456 if (!browsingContext) {
460 return nodeCache.getOrCreateNodeReference(node, seenNodeIds);
464 * Helper to serialize an Array-like object.
466 * @see https://w3c.github.io/webdriver-bidi/#serialize-an-array-like
468 * @param {string} production
470 * @param {string} handleId
471 * The unique id of the <var>value</var>.
472 * @param {boolean} knownObject
473 * Indicates if the <var>value</var> has already been serialized.
474 * @param {object} value
475 * The Array-like object to serialize.
476 * @param {SerializationOptions} serializationOptions
477 * Options which define how ECMAScript objects should be serialized.
478 * @param {OwnershipModel} ownershipType
479 * The ownership model to use for this serialization.
480 * @param {Map} serializationInternalMap
481 * Map of internal ids.
482 * @param {Realm} realm
483 * The Realm from which comes the value being serialized.
484 * @param {ExtraSerializationOptions} extraOptions
485 * Extra Remote Value serialization options.
487 * @returns {object} Object for serialized values.
489 function serializeArrayLike(
494 serializationOptions,
496 serializationInternalMap,
500 const serialized = buildSerialized(production, handleId);
501 setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
503 if (!knownObject && serializationOptions.maxObjectDepth !== 0) {
504 serialized.value = serializeList(
506 serializationOptions,
508 serializationInternalMap,
518 * Helper to serialize as a list.
520 * @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-list
522 * @param {Iterable} iterable
523 * List of values to be serialized.
524 * @param {SerializationOptions} serializationOptions
525 * Options which define how ECMAScript objects should be serialized.
526 * @param {OwnershipModel} ownershipType
527 * The ownership model to use for this serialization.
528 * @param {Map} serializationInternalMap
529 * Map of internal ids.
530 * @param {Realm} realm
531 * The Realm from which comes the value being serialized.
532 * @param {ExtraSerializationOptions} extraOptions
533 * Extra Remote Value serialization options.
535 * @returns {Array} List of serialized values.
537 function serializeList(
539 serializationOptions,
541 serializationInternalMap,
545 const { maxObjectDepth } = serializationOptions;
546 const serialized = [];
547 const childSerializationOptions = {
548 ...serializationOptions,
550 if (maxObjectDepth !== null) {
551 childSerializationOptions.maxObjectDepth = maxObjectDepth - 1;
554 for (const item of iterable) {
558 childSerializationOptions,
560 serializationInternalMap,
571 * Helper to serialize as a mapping.
573 * @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-mapping
575 * @param {Iterable} iterable
576 * List of values to be serialized.
577 * @param {SerializationOptions} serializationOptions
578 * Options which define how ECMAScript objects should be serialized.
579 * @param {OwnershipModel} ownershipType
580 * The ownership model to use for this serialization.
581 * @param {Map} serializationInternalMap
582 * Map of internal ids.
583 * @param {Realm} realm
584 * The Realm from which comes the value being serialized.
585 * @param {ExtraSerializationOptions} extraOptions
586 * Extra Remote Value serialization options.
588 * @returns {Array} List of serialized values.
590 function serializeMapping(
592 serializationOptions,
594 serializationInternalMap,
598 const { maxObjectDepth } = serializationOptions;
599 const serialized = [];
600 const childSerializationOptions = {
601 ...serializationOptions,
603 if (maxObjectDepth !== null) {
604 childSerializationOptions.maxObjectDepth = maxObjectDepth - 1;
607 for (const [key, item] of iterable) {
608 const serializedKey =
609 typeof key == "string"
613 childSerializationOptions,
615 serializationInternalMap,
619 const serializedValue = serialize(
621 childSerializationOptions,
623 serializationInternalMap,
628 serialized.push([serializedKey, serializedValue]);
635 * Helper to serialize as a Node.
638 * Node to be serialized.
639 * @param {SerializationOptions} serializationOptions
640 * Options which define how ECMAScript objects should be serialized.
641 * @param {OwnershipModel} ownershipType
642 * The ownership model to use for this serialization.
643 * @param {Map} serializationInternalMap
644 * Map of internal ids.
645 * @param {Realm} realm
646 * The Realm from which comes the value being serialized.
647 * @param {ExtraSerializationOptions} extraOptions
648 * Extra Remote Value serialization options.
650 * @returns {object} Serialized value.
652 function serializeNode(
654 serializationOptions,
656 serializationInternalMap,
660 const { includeShadowTree, maxDomDepth } = serializationOptions;
661 const isAttribute = Attr.isInstance(node);
662 const isElement = Element.isInstance(node);
665 nodeType: node.nodeType,
668 if (node.nodeValue !== null) {
669 serialized.nodeValue = node.nodeValue;
672 if (isElement || isAttribute) {
673 serialized.localName = node.localName;
674 serialized.namespaceURI = node.namespaceURI;
677 serialized.childNodeCount = node.childNodes.length;
680 (!lazy.dom.isShadowRoot(node) ||
681 (includeShadowTree === IncludeShadowTreeMode.Open &&
682 node.mode === "open") ||
683 includeShadowTree === IncludeShadowTreeMode.All)
686 const childSerializationOptions = {
687 ...serializationOptions,
689 if (maxDomDepth !== null) {
690 childSerializationOptions.maxDomDepth = maxDomDepth - 1;
692 for (const child of node.childNodes) {
696 childSerializationOptions,
698 serializationInternalMap,
705 serialized.children = children;
709 serialized.attributes = [...node.attributes].reduce((map, attr) => {
710 map[attr.name] = attr.value;
714 const shadowRoot = node.openOrClosedShadowRoot;
715 serialized.shadowRoot = null;
716 if (shadowRoot !== null) {
717 serialized.shadowRoot = serialize(
719 serializationOptions,
721 serializationInternalMap,
728 if (lazy.dom.isShadowRoot(node)) {
729 serialized.mode = node.mode;
736 * Serialize a value as a remote value.
738 * @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-remote-value
740 * @param {object} value
741 * Value of any type to be serialized.
742 * @param {SerializationOptions} serializationOptions
743 * Options which define how ECMAScript objects should be serialized.
744 * @param {OwnershipModel} ownershipType
745 * The ownership model to use for this serialization.
746 * @param {Map} serializationInternalMap
747 * Map of internal ids.
748 * @param {Realm} realm
749 * The Realm from which comes the value being serialized.
750 * @param {ExtraSerializationOptions} extraOptions
751 * Extra Remote Value serialization options.
753 * @returns {object} Serialized representation of the value.
755 export function serialize(
757 serializationOptions,
759 serializationInternalMap,
763 const { maxObjectDepth } = serializationOptions;
764 const type = typeof value;
766 // Primitive protocol values
767 if (type == "undefined") {
769 } else if (Object.is(value, null)) {
770 return { type: "null" };
771 } else if (Object.is(value, NaN)) {
772 return { type: "number", value: "NaN" };
773 } else if (Object.is(value, -0)) {
774 return { type: "number", value: "-0" };
775 } else if (Object.is(value, Infinity)) {
776 return { type: "number", value: "Infinity" };
777 } else if (Object.is(value, -Infinity)) {
778 return { type: "number", value: "-Infinity" };
779 } else if (type == "bigint") {
780 return { type, value: value.toString() };
781 } else if (["boolean", "number", "string"].includes(type)) {
782 return { type, value };
785 const handleId = getHandleForObject(realm, ownershipType, value);
786 const knownObject = serializationInternalMap.has(value);
788 // Set the OwnershipModel to use for all complex object serializations.
789 ownershipType = OwnershipModel.None;
793 // symbols are primitive JS values which can only be serialized
795 if (type == "symbol") {
796 return buildSerialized("symbol", handleId);
799 // All other remote values are non-primitives and their
800 // className can be extracted with ChromeUtils.getClassName
801 const className = ChromeUtils.getClassName(value);
802 if (["Array", "HTMLCollection", "NodeList"].includes(className)) {
803 return serializeArrayLike(
804 className.toLowerCase(),
808 serializationOptions,
810 serializationInternalMap,
814 } else if (className == "RegExp") {
815 const serialized = buildSerialized("regexp", handleId);
816 serialized.value = { pattern: value.source, flags: value.flags };
818 } else if (className == "Date") {
819 const serialized = buildSerialized("date", handleId);
820 serialized.value = value.toISOString();
822 } else if (className == "Map") {
823 const serialized = buildSerialized("map", handleId);
824 setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
826 if (!knownObject && maxObjectDepth !== 0) {
827 serialized.value = serializeMapping(
829 serializationOptions,
831 serializationInternalMap,
837 } else if (className == "Set") {
838 const serialized = buildSerialized("set", handleId);
839 setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
841 if (!knownObject && maxObjectDepth !== 0) {
842 serialized.value = serializeList(
844 serializationOptions,
846 serializationInternalMap,
853 ["ArrayBuffer", "Function", "Promise", "WeakMap", "WeakSet"].includes(
857 return buildSerialized(className.toLowerCase(), handleId);
858 } else if (className.includes("Generator")) {
859 return buildSerialized("generator", handleId);
860 } else if (lazy.error.isError(value)) {
861 return buildSerialized("error", handleId);
862 } else if (Cu.isProxy(value)) {
863 return buildSerialized("proxy", handleId);
864 } else if (TYPED_ARRAY_CLASSES.includes(className)) {
865 return buildSerialized("typedarray", handleId);
866 } else if (Node.isInstance(value)) {
867 const serialized = buildSerialized("node", handleId);
869 value = Cu.unwaiveXrays(value);
871 // Get or create the shared id for WebDriver classic compat from the node.
872 const sharedId = getSharedIdForNode(value, extraOptions);
873 if (sharedId !== null) {
874 serialized.sharedId = sharedId;
877 setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
880 serialized.value = serializeNode(
882 serializationOptions,
884 serializationInternalMap,
891 } else if (Window.isInstance(value)) {
892 const serialized = buildSerialized("window", handleId);
893 const window = Cu.unwaiveXrays(value);
895 if (window.browsingContext.parent == null) {
897 context: window.browsingContext.browserId.toString(),
898 isTopBrowsingContext: true,
902 context: window.browsingContext.id.toString(),
907 } else if (ChromeUtils.isDOMObject(value)) {
908 const serialized = buildSerialized("object", handleId);
912 // Otherwise serialize the JavaScript object as generic object.
913 const serialized = buildSerialized("object", handleId);
914 setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
916 if (!knownObject && maxObjectDepth !== 0) {
917 serialized.value = serializeMapping(
918 Object.entries(value),
919 serializationOptions,
921 serializationInternalMap,
930 * Set default serialization options.
932 * @param {SerializationOptions} options
933 * Options which define how ECMAScript objects should be serialized.
934 * @returns {SerializationOptions}
935 * Serialiation options with default value added.
937 export function setDefaultSerializationOptions(options = {}) {
938 const serializationOptions = { ...options };
939 if (!("maxDomDepth" in serializationOptions)) {
940 serializationOptions.maxDomDepth = 0;
942 if (!("maxObjectDepth" in serializationOptions)) {
943 serializationOptions.maxObjectDepth = null;
945 if (!("includeShadowTree" in serializationOptions)) {
946 serializationOptions.includeShadowTree = IncludeShadowTreeMode.None;
949 return serializationOptions;
953 * Set default values and assert if serialization options have
956 * @param {SerializationOptions} options
957 * Options which define how ECMAScript objects should be serialized.
958 * @returns {SerializationOptions}
959 * Serialiation options with default value added.
961 export function setDefaultAndAssertSerializationOptions(options = {}) {
962 lazy.assert.object(options);
964 const serializationOptions = setDefaultSerializationOptions(options);
966 const { includeShadowTree, maxDomDepth, maxObjectDepth } =
967 serializationOptions;
969 if (maxDomDepth !== null) {
970 lazy.assert.positiveInteger(maxDomDepth);
972 if (maxObjectDepth !== null) {
973 lazy.assert.positiveInteger(maxObjectDepth);
975 const includeShadowTreeModesValues = Object.values(IncludeShadowTreeMode);
978 includeShadowTreeModesValues.includes(includeShadowTree),
979 `includeShadowTree ${includeShadowTree} doesn't match allowed values "${includeShadowTreeModesValues.join(
982 )(includeShadowTree);
984 return serializationOptions;
988 * Set the internalId property of a provided serialized RemoteValue,
989 * and potentially of a previously created serialized RemoteValue,
990 * corresponding to the same provided object.
992 * @see https://w3c.github.io/webdriver-bidi/#set-internal-ids-if-needed
994 * @param {Map} serializationInternalMap
995 * Map of objects to remote values.
996 * @param {object} remoteValue
997 * A serialized RemoteValue for the provided object.
998 * @param {object} object
999 * Object of any type to be serialized.
1001 function setInternalIdsIfNeeded(serializationInternalMap, remoteValue, object) {
1002 if (!serializationInternalMap.has(object)) {
1003 // If the object was not tracked yet in the current serialization, add
1004 // a new entry in the serialization internal map. An internal id will only
1005 // be generated if the same object is encountered again.
1006 serializationInternalMap.set(object, remoteValue);
1008 // This is at least the second time this object is encountered, retrieve the
1009 // original remote value stored for this object.
1010 const previousRemoteValue = serializationInternalMap.get(object);
1012 if (!previousRemoteValue.internalId) {
1013 // If the original remote value has no internal id yet, generate a uuid
1014 // and update the internalId of the original remote value with it.
1015 previousRemoteValue.internalId = lazy.generateUUID();
1018 // Copy the internalId of the original remote value to the new remote value.
1019 remoteValue.internalId = previousRemoteValue.internalId;
1024 * Safely stringify a value.
1026 * @param {object} obj
1027 * Value of any type to be stringified.
1029 * @returns {string} String representation of the value.
1031 export function stringify(obj) {
1035 obj !== null && typeof obj === "object" ? obj.toString() : String(obj);
1037 // The error-case will also be handled in `finally {}`.
1039 if (typeof text != "string") {
1040 text = Object.prototype.toString.apply(obj);