From 3a336799a08564ddc91713a397707b01ae0425c5 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 24 Feb 2021 01:28:02 +0100 Subject: idb: custom structured clone, don't rely on typeson anymore --- packages/idb-bridge/package.json | 4 +- packages/idb-bridge/src/index.ts | 2 + packages/idb-bridge/src/util/structuredClone.ts | 227 +++++++++++++++++++++++- 3 files changed, 228 insertions(+), 5 deletions(-) (limited to 'packages/idb-bridge') diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json index 9db293b52..52bc872da 100644 --- a/packages/idb-bridge/package.json +++ b/packages/idb-bridge/package.json @@ -28,9 +28,7 @@ "typescript": "^4.1.3" }, "dependencies": { - "tslib": "^2.1.0", - "typeson": "^5.18.2", - "typeson-registry": "^1.0.0-alpha.38" + "tslib": "^2.1.0" }, "ava": { "require": [ diff --git a/packages/idb-bridge/src/index.ts b/packages/idb-bridge/src/index.ts index 2cb8bcf15..a0bd8b86d 100644 --- a/packages/idb-bridge/src/index.ts +++ b/packages/idb-bridge/src/index.ts @@ -115,3 +115,5 @@ export function shimIndexedDB(factory: BridgeIDBFactory): void { } export * from "./idbtypes"; + +export * from "./util/structuredClone"; \ No newline at end of file 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, + 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(); + } } /** -- cgit v1.2.3