diff --git a/public/index.html b/public/index.html index fac93ff29..ee5c89ed4 100644 --- a/public/index.html +++ b/public/index.html @@ -45,6 +45,7 @@ + diff --git a/public/lib/structured-clone/LICENSE b/public/lib/structured-clone/LICENSE new file mode 100644 index 000000000..48afbe52a --- /dev/null +++ b/public/lib/structured-clone/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2021, Andrea Giammarchi, @WebReflection + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/public/lib/structured-clone/deserialize.js b/public/lib/structured-clone/deserialize.js new file mode 100644 index 000000000..0fa70897d --- /dev/null +++ b/public/lib/structured-clone/deserialize.js @@ -0,0 +1,79 @@ +import { + VOID, PRIMITIVE, + ARRAY, OBJECT, + DATE, REGEXP, MAP, SET, + ERROR, BIGINT +} from './types.js'; + +const env = typeof self === 'object' ? self : globalThis; + +const deserializer = ($, _) => { + const as = (out, index) => { + $.set(index, out); + return out; + }; + + const unpair = index => { + if ($.has(index)) + return $.get(index); + + const [type, value] = _[index]; + switch (type) { + case PRIMITIVE: + case VOID: + return as(value, index); + case ARRAY: { + const arr = as([], index); + for (const index of value) + arr.push(unpair(index)); + return arr; + } + case OBJECT: { + const object = as({}, index); + for (const [key, index] of value) + object[unpair(key)] = unpair(index); + return object; + } + case DATE: + return as(new Date(value), index); + case REGEXP: { + const {source, flags} = value; + return as(new RegExp(source, flags), index); + } + case MAP: { + const map = as(new Map, index); + for (const [key, index] of value) + map.set(unpair(key), unpair(index)); + return map; + } + case SET: { + const set = as(new Set, index); + for (const index of value) + set.add(unpair(index)); + return set; + } + case ERROR: { + const {name, message} = value; + return as(new env[name](message), index); + } + case BIGINT: + return as(BigInt(value), index); + case 'BigInt': + return as(Object(BigInt(value)), index); + } + return as(new env[type](value), index); + }; + + return unpair; +}; + +/** + * @typedef {Array} Record a type representation + */ + +/** + * Returns a deserialized value from a serialized array of Records. + * @param {Record[]} serialized a previously serialized value. + * @returns {any} + */ +export const deserialize = serialized => deserializer(new Map, serialized)(0); diff --git a/public/lib/structured-clone/index.js b/public/lib/structured-clone/index.js new file mode 100644 index 000000000..d3b47479a --- /dev/null +++ b/public/lib/structured-clone/index.js @@ -0,0 +1,25 @@ +import {deserialize} from './deserialize.js'; +import {serialize} from './serialize.js'; + +/** + * @typedef {Array} Record a type representation + */ + +/** + * Returns an array of serialized Records. + * @param {any} any a serializable value. + * @param {{transfer?: any[], json?: boolean, lossy?: boolean}?} options an object with + * a transfer option (ignored when polyfilled) and/or non standard fields that + * fallback to the polyfill if present. + * @returns {Record[]} + */ +export default typeof structuredClone === "function" ? + /* c8 ignore start */ + (any, options) => ( + options && ('json' in options || 'lossy' in options) ? + deserialize(serialize(any, options)) : structuredClone(any) + ) : + (any, options) => deserialize(serialize(any, options)); + /* c8 ignore stop */ + +export {deserialize, serialize}; diff --git a/public/lib/structured-clone/json.js b/public/lib/structured-clone/json.js new file mode 100644 index 000000000..23eb95222 --- /dev/null +++ b/public/lib/structured-clone/json.js @@ -0,0 +1,21 @@ +/*! (c) Andrea Giammarchi - ISC */ + +import {deserialize} from './deserialize.js'; +import {serialize} from './serialize.js'; + +const {parse: $parse, stringify: $stringify} = JSON; +const options = {json: true, lossy: true}; + +/** + * Revive a previously stringified structured clone. + * @param {string} str previously stringified data as string. + * @returns {any} whatever was previously stringified as clone. + */ +export const parse = str => deserialize($parse(str)); + +/** + * Represent a structured clone value as string. + * @param {any} any some clone-able value to stringify. + * @returns {string} the value stringified. + */ +export const stringify = any => $stringify(serialize(any, options)); diff --git a/public/lib/structured-clone/monkey-patch.js b/public/lib/structured-clone/monkey-patch.js new file mode 100644 index 000000000..8489dc892 --- /dev/null +++ b/public/lib/structured-clone/monkey-patch.js @@ -0,0 +1,6 @@ +import structuredClone from './index.js'; + +if (!("structuredClone" in globalThis)) { + console.debug("Monkey-patching structuredClone"); + globalThis.structuredClone = structuredClone; +} diff --git a/public/lib/structured-clone/serialize.js b/public/lib/structured-clone/serialize.js new file mode 100644 index 000000000..8e098ddca --- /dev/null +++ b/public/lib/structured-clone/serialize.js @@ -0,0 +1,161 @@ +import { + VOID, PRIMITIVE, + ARRAY, OBJECT, + DATE, REGEXP, MAP, SET, + ERROR, BIGINT +} from './types.js'; + +const EMPTY = ''; + +const {toString} = {}; +const {keys} = Object; + +const typeOf = value => { + const type = typeof value; + if (type !== 'object' || !value) + return [PRIMITIVE, type]; + + const asString = toString.call(value).slice(8, -1); + switch (asString) { + case 'Array': + return [ARRAY, EMPTY]; + case 'Object': + return [OBJECT, EMPTY]; + case 'Date': + return [DATE, EMPTY]; + case 'RegExp': + return [REGEXP, EMPTY]; + case 'Map': + return [MAP, EMPTY]; + case 'Set': + return [SET, EMPTY]; + } + + if (asString.includes('Array')) + return [ARRAY, asString]; + + if (asString.includes('Error')) + return [ERROR, asString]; + + return [OBJECT, asString]; +}; + +const shouldSkip = ([TYPE, type]) => ( + TYPE === PRIMITIVE && + (type === 'function' || type === 'symbol') +); + +const serializer = (strict, json, $, _) => { + + const as = (out, value) => { + const index = _.push(out) - 1; + $.set(value, index); + return index; + }; + + const pair = value => { + if ($.has(value)) + return $.get(value); + + let [TYPE, type] = typeOf(value); + switch (TYPE) { + case PRIMITIVE: { + let entry = value; + switch (type) { + case 'bigint': + TYPE = BIGINT; + entry = value.toString(); + break; + case 'function': + case 'symbol': + if (strict) + throw new TypeError('unable to serialize ' + type); + entry = null; + break; + case 'undefined': + return as([VOID], value); + } + return as([TYPE, entry], value); + } + case ARRAY: { + if (type) + return as([type, [...value]], value); + + const arr = []; + const index = as([TYPE, arr], value); + for (const entry of value) + arr.push(pair(entry)); + return index; + } + case OBJECT: { + if (type) { + switch (type) { + case 'BigInt': + return as([type, value.toString()], value); + case 'Boolean': + case 'Number': + case 'String': + return as([type, value.valueOf()], value); + } + } + + if (json && ('toJSON' in value)) + return pair(value.toJSON()); + + const entries = []; + const index = as([TYPE, entries], value); + for (const key of keys(value)) { + if (strict || !shouldSkip(typeOf(value[key]))) + entries.push([pair(key), pair(value[key])]); + } + return index; + } + case DATE: + return as([TYPE, value.toISOString()], value); + case REGEXP: { + const {source, flags} = value; + return as([TYPE, {source, flags}], value); + } + case MAP: { + const entries = []; + const index = as([TYPE, entries], value); + for (const [key, entry] of value) { + if (strict || !(shouldSkip(typeOf(key)) || shouldSkip(typeOf(entry)))) + entries.push([pair(key), pair(entry)]); + } + return index; + } + case SET: { + const entries = []; + const index = as([TYPE, entries], value); + for (const entry of value) { + if (strict || !shouldSkip(typeOf(entry))) + entries.push(pair(entry)); + } + return index; + } + } + + const {message} = value; + return as([TYPE, {name: type, message}], value); + }; + + return pair; +}; + +/** + * @typedef {Array} Record a type representation + */ + +/** + * Returns an array of serialized Records. + * @param {any} value a serializable value. + * @param {{json?: boolean, lossy?: boolean}?} options an object with a `lossy` or `json` property that, + * if `true`, will not throw errors on incompatible types, and behave more + * like JSON stringify would behave. Symbol and Function will be discarded. + * @returns {Record[]} + */ + export const serialize = (value, {json, lossy} = {}) => { + const _ = []; + return serializer(!(json || lossy), !!json, new Map, _)(value), _; +}; diff --git a/public/lib/structured-clone/types.js b/public/lib/structured-clone/types.js new file mode 100644 index 000000000..50e60ca06 --- /dev/null +++ b/public/lib/structured-clone/types.js @@ -0,0 +1,11 @@ +export const VOID = -1; +export const PRIMITIVE = 0; +export const ARRAY = 1; +export const OBJECT = 2; +export const DATE = 3; +export const REGEXP = 4; +export const MAP = 5; +export const SET = 6; +export const ERROR = 7; +export const BIGINT = 8; +// export const SYMBOL = 9;