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;