diff options
Diffstat (limited to 'packages/idb-bridge/src/util')
-rw-r--r-- | packages/idb-bridge/src/util/structuredClone.ts | 227 |
1 files changed, 225 insertions, 2 deletions
diff --git a/packages/idb-bridge/src/util/structuredClone.ts b/packages/idb-bridge/src/util/structuredClone.ts index 9bbeb7151..1c96d7430 100644 --- a/packages/idb-bridge/src/util/structuredClone.ts +++ b/packages/idb-bridge/src/util/structuredClone.ts @@ -18,15 +18,238 @@ import Typeson from "typeson"; // @ts-ignore import structuredCloningThrowing from "typeson-registry/dist/presets/structured-cloning-throwing"; +import { DataCloneError } from "./errors"; const TSON = new Typeson().register(structuredCloningThrowing); +const { toString: toStr } = {}; +const hasOwn = {}.hasOwnProperty; +const getProto = Object.getPrototypeOf; +const fnToString = hasOwn.toString; + +function toStringTag(val: any) { + return toStr.call(val).slice(8, -1); +} + +function hasConstructorOf(a: any, b: any) { + if (!a || typeof a !== "object") { + return false; + } + const proto = getProto(a); + if (!proto) { + return b === null; + } + const Ctor = hasOwn.call(proto, "constructor") && proto.constructor; + if (typeof Ctor !== "function") { + return b === null; + } + if (b === Ctor) { + return true; + } + if (b !== null && fnToString.call(Ctor) === fnToString.call(b)) { + return true; + } + return false; +} + +/** + * + * @param {any} val + * @returns {boolean} + */ +function isPlainObject(val: any): boolean { + if (!val || toStringTag(val) !== "Object") { + return false; + } + + const proto = getProto(val); + if (!proto) { + // `Object.create(null)` + return true; + } + + return hasConstructorOf(val, Object); +} + +function isUserObject(val: any): boolean { + if (!val || toStringTag(val) !== "Object") { + return false; + } + + const proto = getProto(val); + if (!proto) { + // `Object.create(null)` + return true; + } + return hasConstructorOf(val, Object) || isUserObject(proto); +} + +function isRegExp(val: any): boolean { + return toStringTag(val) === "RegExp"; +} + +function internalEncapsulate( + val: any, + outRoot: any, + path: string[], + memo: Map<any, string[]>, + types: Array<[string[], string]>, +): any { + const memoPath = memo.get(val); + if (memoPath) { + types.push([path, "ref"]); + return memoPath; + } + if (val === null) { + return null; + } + if (val === undefined) { + types.push([path, "undef"]); + return 0; + } + if (Array.isArray(val)) { + memo.set(val, path); + const outArr: any[] = []; + let special = false; + for (const x in val) { + const n = Number(x); + if (n < 0 || n >= val.length || Number.isNaN(n)) { + special = true; + break; + } + } + if (special) { + types.push([path, "array"]); + } + for (const x in val) { + const p = [...path, x]; + outArr[x] = internalEncapsulate(val[x], outRoot, p, memo, types); + } + return outArr; + } + if (val instanceof Date) { + types.push([path, "date"]); + return val.getTime(); + } + if (isUserObject(val) || isPlainObject(val)) { + memo.set(val, path); + const outObj: any = {}; + for (const x in val) { + const p = [...path, x]; + outObj[x] = internalEncapsulate(val[x], outRoot, p, memo, types); + } + return outObj; + } + if (typeof val === "bigint") { + types.push([path, "bigint"]); + return val.toString(); + } + if (typeof val === "boolean") { + return val; + } + if (typeof val === "number") { + return val; + } + if (typeof val === "string") { + return val; + } + throw Error(); +} + +/** + * Encapsulate a cloneable value into a plain JSON object. + */ export function structuredEncapsulate(val: any): any { - return TSON.encapsulate(val); + const outRoot = {}; + const types: Array<[string[], string]> = []; + let res; + try { + res = internalEncapsulate(val, outRoot, [], new Map(), types); + } catch (e) { + throw new DataCloneError(); + } + if (res === null) { + return res; + } + // We need to further encapsulate the outer layer + if ( + Array.isArray(res) || + typeof res !== "object" || + "$" in res || + "$types" in res + ) { + res = { $: res }; + } + if (types.length > 0) { + res["$types"] = types; + } + return res; +} + +export function internalStructuredRevive(val: any): any { + val = JSON.parse(JSON.stringify(val)); + if (val === null) { + return null; + } + if (typeof val === "number") { + return val; + } + if (typeof val === "string") { + return val; + } + if (!isPlainObject(val)) { + throw Error(); + } + let types = val.$types ?? []; + delete val.$types; + let outRoot: any; + if ("$" in val) { + outRoot = val.$; + } else { + outRoot = val; + } + function mutatePath(path: string[], f: (x: any) => any): void { + if (path.length == 0) { + outRoot = f(outRoot); + return; + } + let obj = outRoot; + for (let i = 0; i < path.length - 1; i++) { + const n = path[i]; + if (!(n in obj)) { + obj[n] = {}; + } + obj = obj[n]; + } + const last = path[path.length - 1]; + obj[last] = f(obj[last]); + } + for (const [path, type] of types) { + if (type === "bigint") { + mutatePath(path, (x) => BigInt(x)); + } else if (type === "array") { + mutatePath(path, (x) => { + const newArr: any = []; + for (const k in x) { + newArr[k] = x[k]; + } + return newArr; + }); + } else if (type === "date") { + mutatePath(path, (x) => new Date(x)); + } else { + throw Error("type not implemented"); + } + } + return outRoot; } export function structuredRevive(val: any): any { - return TSON.revive(val); + try { + return internalStructuredRevive(val); + } catch (e) { + throw new DataCloneError(); + } } /** |