450 lines
11 KiB
TypeScript
450 lines
11 KiB
TypeScript
/*
|
|
Copyright 2019 Florian Dold
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
or implied. See the License for the specific language governing
|
|
permissions and limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* Encoding (new, compositional version):
|
|
*
|
|
* Encapsulate object that itself might contain a "$" field:
|
|
* { $: "obj", val: ... }
|
|
* (Outer level only:) Wrap other values into object
|
|
* { $: "lit", val: ... }
|
|
* Circular reference:
|
|
* { $: "ref" l: uplevel, p: path }
|
|
* Date:
|
|
* { $: "date", val: datestr }
|
|
* Bigint:
|
|
* { $: "bigint", val: bigintstr }
|
|
* Array with special (non-number) attributes:
|
|
* { $: "array", val: arrayobj }
|
|
* Undefined field
|
|
* { $: "undef" }
|
|
*/
|
|
|
|
/**
|
|
* Imports.
|
|
*/
|
|
import { DataCloneError } from "./errors.js";
|
|
|
|
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;
|
|
}
|
|
|
|
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 copyBuffer(cur: any) {
|
|
if (cur instanceof Buffer) {
|
|
return Buffer.from(cur);
|
|
}
|
|
|
|
return new cur.constructor(cur.buffer.slice(), cur.byteOffset, cur.length);
|
|
}
|
|
|
|
function checkCloneableOrThrow(x: any) {
|
|
if (x == null) return;
|
|
if (typeof x !== "object" && typeof x !== "function") return;
|
|
if (x instanceof Date) return;
|
|
if (Array.isArray(x)) return;
|
|
if (x instanceof Map) return;
|
|
if (x instanceof Set) return;
|
|
if (isUserObject(x)) return;
|
|
if (isPlainObject(x)) return;
|
|
throw new DataCloneError();
|
|
}
|
|
|
|
export function mkDeepClone() {
|
|
const refs = [] as any;
|
|
const refsNew = [] as any;
|
|
|
|
return clone;
|
|
|
|
function cloneArray(a: any) {
|
|
var keys = Object.keys(a);
|
|
var a2 = new Array(keys.length);
|
|
refs.push(a);
|
|
refsNew.push(a2);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var k = keys[i] as any;
|
|
var cur = a[k];
|
|
checkCloneableOrThrow(cur);
|
|
if (typeof cur !== "object" || cur === null) {
|
|
a2[k] = cur;
|
|
} else if (cur instanceof Date) {
|
|
a2[k] = new Date(cur);
|
|
} else if (ArrayBuffer.isView(cur)) {
|
|
a2[k] = copyBuffer(cur);
|
|
} else {
|
|
var index = refs.indexOf(cur);
|
|
if (index !== -1) {
|
|
a2[k] = refsNew[index];
|
|
} else {
|
|
a2[k] = clone(cur);
|
|
}
|
|
}
|
|
}
|
|
refs.pop();
|
|
refsNew.pop();
|
|
return a2;
|
|
}
|
|
|
|
function clone(o: any) {
|
|
checkCloneableOrThrow(o);
|
|
if (typeof o !== "object" || o === null) return o;
|
|
if (o instanceof Date) return new Date(o);
|
|
if (Array.isArray(o)) return cloneArray(o);
|
|
if (o instanceof Map) return new Map(cloneArray(Array.from(o)));
|
|
if (o instanceof Set) return new Set(cloneArray(Array.from(o)));
|
|
var o2 = {} as any;
|
|
refs.push(o);
|
|
refsNew.push(o2);
|
|
for (var k in o) {
|
|
if (Object.hasOwnProperty.call(o, k) === false) continue;
|
|
var cur = o[k] as any;
|
|
checkCloneableOrThrow(cur);
|
|
if (typeof cur !== "object" || cur === null) {
|
|
o2[k] = cur;
|
|
} else if (cur instanceof Date) {
|
|
o2[k] = new Date(cur);
|
|
} else if (cur instanceof Map) {
|
|
o2[k] = new Map(cloneArray(Array.from(cur)));
|
|
} else if (cur instanceof Set) {
|
|
o2[k] = new Set(cloneArray(Array.from(cur)));
|
|
} else if (ArrayBuffer.isView(cur)) {
|
|
o2[k] = copyBuffer(cur);
|
|
} else {
|
|
var i = refs.indexOf(cur);
|
|
if (i !== -1) {
|
|
o2[k] = refsNew[i];
|
|
} else {
|
|
o2[k] = clone(cur);
|
|
}
|
|
}
|
|
}
|
|
refs.pop();
|
|
refsNew.pop();
|
|
return o2;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if an object is deeply cloneable.
|
|
* Only called for the side-effect of throwing an exception.
|
|
*/
|
|
export function mkDeepCloneCheckOnly() {
|
|
const refs = [] as any;
|
|
|
|
return clone;
|
|
|
|
function cloneArray(a: any) {
|
|
var keys = Object.keys(a);
|
|
refs.push(a);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var k = keys[i] as any;
|
|
var cur = a[k];
|
|
checkCloneableOrThrow(cur);
|
|
if (typeof cur !== "object" || cur === null) {
|
|
// do nothing
|
|
} else if (cur instanceof Date) {
|
|
// do nothing
|
|
} else if (ArrayBuffer.isView(cur)) {
|
|
// do nothing
|
|
} else {
|
|
var index = refs.indexOf(cur);
|
|
if (index !== -1) {
|
|
// do nothing
|
|
} else {
|
|
clone(cur);
|
|
}
|
|
}
|
|
}
|
|
refs.pop();
|
|
}
|
|
|
|
function clone(o: any) {
|
|
checkCloneableOrThrow(o);
|
|
if (typeof o !== "object" || o === null) return o;
|
|
if (o instanceof Date) return;
|
|
if (Array.isArray(o)) return cloneArray(o);
|
|
if (o instanceof Map) return cloneArray(Array.from(o));
|
|
if (o instanceof Set) return cloneArray(Array.from(o));
|
|
refs.push(o);
|
|
for (var k in o) {
|
|
if (Object.hasOwnProperty.call(o, k) === false) continue;
|
|
var cur = o[k] as any;
|
|
checkCloneableOrThrow(cur);
|
|
if (typeof cur !== "object" || cur === null) {
|
|
// do nothing
|
|
} else if (cur instanceof Date) {
|
|
// do nothing
|
|
} else if (cur instanceof Map) {
|
|
cloneArray(Array.from(cur));
|
|
} else if (cur instanceof Set) {
|
|
cloneArray(Array.from(cur));
|
|
} else if (ArrayBuffer.isView(cur)) {
|
|
// do nothing
|
|
} else {
|
|
var i = refs.indexOf(cur);
|
|
if (i !== -1) {
|
|
// do nothing
|
|
} else {
|
|
clone(cur);
|
|
}
|
|
}
|
|
}
|
|
refs.pop();
|
|
}
|
|
}
|
|
|
|
function internalEncapsulate(
|
|
val: any,
|
|
path: string[],
|
|
memo: Map<any, string[]>,
|
|
): any {
|
|
const memoPath = memo.get(val);
|
|
if (memoPath) {
|
|
return { $: "ref", d: path.length, p: memoPath };
|
|
}
|
|
if (val === null) {
|
|
return null;
|
|
}
|
|
if (val === undefined) {
|
|
return { $: "undef" };
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
for (const x in val) {
|
|
const p = [...path, x];
|
|
outArr[x] = internalEncapsulate(val[x], p, memo);
|
|
}
|
|
if (special) {
|
|
return { $: "array", val: outArr };
|
|
} else {
|
|
return outArr;
|
|
}
|
|
}
|
|
if (val instanceof Date) {
|
|
return { $: "date", val: 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], p, memo);
|
|
}
|
|
if ("$" in outObj) {
|
|
return { $: "obj", val: outObj };
|
|
}
|
|
return outObj;
|
|
}
|
|
if (typeof val === "bigint") {
|
|
return { $: "bigint", val: val.toString() };
|
|
}
|
|
if (typeof val === "boolean") {
|
|
return val;
|
|
}
|
|
if (typeof val === "number") {
|
|
return val;
|
|
}
|
|
if (typeof val === "string") {
|
|
return val;
|
|
}
|
|
throw Error();
|
|
}
|
|
|
|
function derefPath(
|
|
root: any,
|
|
p1: Array<string | number>,
|
|
n: number,
|
|
p2: Array<string | number>,
|
|
): any {
|
|
let v = root;
|
|
for (let i = 0; i < n; i++) {
|
|
v = v[p1[i]];
|
|
}
|
|
for (let i = 0; i < p2.length; i++) {
|
|
v = v[p2[i]];
|
|
}
|
|
return v;
|
|
}
|
|
|
|
function internalReviveArray(sval: any, root: any, path: string[]): any {
|
|
const newArr: any[] = [];
|
|
if (root === undefined) {
|
|
root = newArr;
|
|
}
|
|
for (let i = 0; i < sval.length; i++) {
|
|
const p = [...path, String(i)];
|
|
newArr.push(internalStructuredRevive(sval[i], root, p));
|
|
}
|
|
return newArr;
|
|
}
|
|
|
|
function internalReviveObject(sval: any, root: any, path: string[]): any {
|
|
const newObj = {} as any;
|
|
if (root === undefined) {
|
|
root = newObj;
|
|
}
|
|
for (const key of Object.keys(sval)) {
|
|
const p = [...path, key];
|
|
newObj[key] = internalStructuredRevive(sval[key], root, p);
|
|
}
|
|
return newObj;
|
|
}
|
|
|
|
function internalStructuredRevive(sval: any, root: any, path: string[]): any {
|
|
if (typeof sval === "string") {
|
|
return sval;
|
|
}
|
|
if (typeof sval === "number") {
|
|
return sval;
|
|
}
|
|
if (typeof sval === "boolean") {
|
|
return sval;
|
|
}
|
|
if (sval === null) {
|
|
return null;
|
|
}
|
|
if (Array.isArray(sval)) {
|
|
return internalReviveArray(sval, root, path);
|
|
}
|
|
|
|
if (isUserObject(sval) || isPlainObject(sval)) {
|
|
if ("$" in sval) {
|
|
const dollar = sval.$;
|
|
switch (dollar) {
|
|
case "undef":
|
|
return undefined;
|
|
case "bigint":
|
|
return BigInt((sval as any).val);
|
|
case "date":
|
|
return new Date((sval as any).val);
|
|
case "obj": {
|
|
return internalReviveObject((sval as any).val, root, path);
|
|
}
|
|
case "array":
|
|
return internalReviveArray((sval as any).val, root, path);
|
|
case "ref": {
|
|
const level = (sval as any).l;
|
|
const p2 = (sval as any).p;
|
|
return derefPath(root, path, path.length - level, p2);
|
|
}
|
|
default:
|
|
throw Error();
|
|
}
|
|
} else {
|
|
return internalReviveObject(sval, root, path);
|
|
}
|
|
}
|
|
|
|
throw Error();
|
|
}
|
|
|
|
/**
|
|
* Encapsulate a cloneable value into a plain JSON value.
|
|
*/
|
|
export function structuredEncapsulate(val: any): any {
|
|
return internalEncapsulate(val, [], new Map());
|
|
}
|
|
|
|
export function structuredRevive(sval: any): any {
|
|
return internalStructuredRevive(sval, undefined, []);
|
|
}
|
|
|
|
/**
|
|
* Structured clone for IndexedDB.
|
|
*/
|
|
export function structuredClone(val: any): any {
|
|
// @ts-ignore
|
|
if (globalThis._tart?.structuredClone) {
|
|
// @ts-ignore
|
|
return globalThis._tart?.structuredClone(val);
|
|
}
|
|
return mkDeepClone()(val);
|
|
}
|
|
|
|
/**
|
|
* Structured clone for IndexedDB.
|
|
*/
|
|
export function checkStructuredCloneOrThrow(val: any): void {
|
|
// @ts-ignore
|
|
if (globalThis._tart?.structuredClone) {
|
|
// @ts-ignore
|
|
globalThis._tart?.structuredClone(val);
|
|
return;
|
|
}
|
|
mkDeepCloneCheckOnly()(val);
|
|
}
|