diff options
author | Florian Dold <florian@dold.me> | 2023-07-11 15:41:48 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2023-08-22 08:01:13 +0200 |
commit | b2d0ad57ddf251a109d536cdc49fb6505dbdc50c (patch) | |
tree | 7eaeca3ad8ec97a9c1970c1004feda2d61c3441b /packages/idb-bridge/src/util | |
parent | 58fdf9dc091b076787a9746c405fe6a9366f5da6 (diff) |
sqlite3 backend for idb-bridge / wallet-core
Diffstat (limited to 'packages/idb-bridge/src/util')
-rw-r--r-- | packages/idb-bridge/src/util/FakeDomEvent.ts | 103 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/FakeEventTarget.ts | 2 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/extractKey.ts | 4 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/key-storage.test.ts | 39 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/key-storage.ts | 363 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/makeStoreKeyValue.test.ts | 66 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/makeStoreKeyValue.ts | 20 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/queueTask.ts | 5 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/structuredClone.test.ts | 61 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/structuredClone.ts | 231 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/valueToKey.ts | 6 |
11 files changed, 739 insertions, 161 deletions
diff --git a/packages/idb-bridge/src/util/FakeDomEvent.ts b/packages/idb-bridge/src/util/FakeDomEvent.ts new file mode 100644 index 000000000..b3ff298ec --- /dev/null +++ b/packages/idb-bridge/src/util/FakeDomEvent.ts @@ -0,0 +1,103 @@ +/* + Copyright 2017 Jeremy Scheff + + 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. +*/ + +import FakeEventTarget from "./FakeEventTarget.js"; +import { Event, EventTarget } from "../idbtypes.js"; + +/** @public */ +export type EventType = + | "abort" + | "blocked" + | "complete" + | "error" + | "success" + | "upgradeneeded" + | "versionchange"; + +export class FakeDomEvent implements Event { + public eventPath: FakeEventTarget[] = []; + public type: EventType; + + public readonly NONE = 0; + public readonly CAPTURING_PHASE = 1; + public readonly AT_TARGET = 2; + public readonly BUBBLING_PHASE = 3; + + // Flags + public propagationStopped = false; + public immediatePropagationStopped = false; + public canceled = false; + public initialized = true; + public dispatched = false; + + public target: FakeEventTarget | null = null; + public currentTarget: FakeEventTarget | null = null; + + public eventPhase: 0 | 1 | 2 | 3 = 0; + + public defaultPrevented = false; + + public isTrusted = false; + public timeStamp = Date.now(); + + public bubbles: boolean; + public cancelable: boolean; + + constructor( + type: EventType, + eventInitDict: { bubbles?: boolean; cancelable?: boolean } = {}, + ) { + this.type = type; + + this.bubbles = + eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false; + this.cancelable = + eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false; + } + cancelBubble: boolean = false; + composed: boolean = false; + returnValue: boolean = false; + get srcElement(): EventTarget | null { + return this.target; + } + composedPath(): EventTarget[] { + throw new Error("Method not implemented."); + } + initEvent( + type: string, + bubbles?: boolean | undefined, + cancelable?: boolean | undefined, + ): void { + throw new Error("Method not implemented."); + } + + public preventDefault() { + if (this.cancelable) { + this.canceled = true; + } + } + + public stopPropagation() { + this.propagationStopped = true; + } + + public stopImmediatePropagation() { + this.propagationStopped = true; + this.immediatePropagationStopped = true; + } +} + +export default FakeDomEvent; diff --git a/packages/idb-bridge/src/util/FakeEventTarget.ts b/packages/idb-bridge/src/util/FakeEventTarget.ts index 79f57cce3..839906a34 100644 --- a/packages/idb-bridge/src/util/FakeEventTarget.ts +++ b/packages/idb-bridge/src/util/FakeEventTarget.ts @@ -180,7 +180,7 @@ abstract class FakeEventTarget implements EventTarget { fe.eventPath.reverse(); fe.eventPhase = event.BUBBLING_PHASE; if (fe.eventPath.length === 0 && event.type === "error") { - console.error("Unhandled error event: ", event.target); + console.error("Unhandled error event on target: ", event.target); } for (const obj of event.eventPath) { if (!event.propagationStopped) { diff --git a/packages/idb-bridge/src/util/extractKey.ts b/packages/idb-bridge/src/util/extractKey.ts index 6a3d468ef..2a4ec45b9 100644 --- a/packages/idb-bridge/src/util/extractKey.ts +++ b/packages/idb-bridge/src/util/extractKey.ts @@ -19,7 +19,11 @@ import { IDBKeyPath, IDBValidKey } from "../idbtypes.js"; import { valueToKey } from "./valueToKey.js"; // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-extracting-a-key-from-a-value-using-a-key-path +/** + * Algorithm to "extract a key from a value using a key path". + */ export const extractKey = (keyPath: IDBKeyPath | IDBKeyPath[], value: any) => { + //console.log(`extracting key ${JSON.stringify(keyPath)} from ${JSON.stringify(value)}`); if (Array.isArray(keyPath)) { const result: IDBValidKey[] = []; diff --git a/packages/idb-bridge/src/util/key-storage.test.ts b/packages/idb-bridge/src/util/key-storage.test.ts new file mode 100644 index 000000000..dc1e1827c --- /dev/null +++ b/packages/idb-bridge/src/util/key-storage.test.ts @@ -0,0 +1,39 @@ +/* + This file is part of GNU Taler + (C) 2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import test, { ExecutionContext } from "ava"; +import { deserializeKey, serializeKey } from "./key-storage.js"; +import { IDBValidKey } from "../idbtypes.js"; + +function checkKeySer(t: ExecutionContext, k: IDBValidKey): void { + const keyEnc = serializeKey(k); + const keyDec = deserializeKey(keyEnc); + t.deepEqual(k, keyDec); +} + +test("basics", (t) => { + checkKeySer(t, "foo"); + checkKeySer(t, "foo\0bar"); + checkKeySer(t, "foo\u1000bar"); + checkKeySer(t, "foo\u2000bar"); + checkKeySer(t, "foo\u5000bar"); + checkKeySer(t, "foo\uffffbar"); + checkKeySer(t, 42); + checkKeySer(t, 255); + checkKeySer(t, 254); + checkKeySer(t, [1, 2, 3, 4]); + checkKeySer(t, [[[1], 3], [4]]); +}); diff --git a/packages/idb-bridge/src/util/key-storage.ts b/packages/idb-bridge/src/util/key-storage.ts new file mode 100644 index 000000000..b71548dd3 --- /dev/null +++ b/packages/idb-bridge/src/util/key-storage.ts @@ -0,0 +1,363 @@ +/* + This file is part of GNU Taler + (C) 2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/* +Encoding rules (inspired by Firefox, but slightly simplified): + +Numbers: 0x10 n n n n n n n n +Dates: 0x20 n n n n n n n n +Strings: 0x30 s s s s ... 0 +Binaries: 0x40 s s s s ... 0 +Arrays: 0x50 i i i ... 0 + +Numbers/dates are encoded as 64-bit IEEE 754 floats with the sign bit +flipped, in order to make them sortable. +*/ + +/** + * Imports. + */ +import { IDBValidKey } from "../idbtypes.js"; + +const tagNum = 0xa0; +const tagDate = 0xb0; +const tagString = 0xc0; +const tagBinary = 0xc0; +const tagArray = 0xe0; + +const oneByteOffset = 0x01; +const twoByteOffset = 0x7f; +const oneByteMax = 0x7e; +const twoByteMax = 0x3fff + twoByteOffset; +const twoByteMask = 0b1000_0000; +const threeByteMask = 0b1100_0000; + +export function countEncSize(c: number): number { + if (c > twoByteMax) { + return 3; + } + if (c > oneByteMax) { + return 2; + } + return 1; +} + +export function writeEnc(dv: DataView, offset: number, c: number): number { + if (c > twoByteMax) { + dv.setUint8(offset + 2, (c & 0xff) << 6); + dv.setUint8(offset + 1, (c >>> 2) & 0xff); + dv.setUint8(offset, threeByteMask | (c >>> 10)); + return 3; + } else if (c > oneByteMax) { + c -= twoByteOffset; + dv.setUint8(offset + 1, c & 0xff); + dv.setUint8(offset, (c >>> 8) | twoByteMask); + return 2; + } else { + c += oneByteOffset; + dv.setUint8(offset, c); + return 1; + } +} + +export function internalSerializeString( + dv: DataView, + offset: number, + key: string, +): number { + dv.setUint8(offset, tagString); + let n = 1; + for (let i = 0; i < key.length; i++) { + let c = key.charCodeAt(i); + n += writeEnc(dv, offset + n, c); + } + // Null terminator + dv.setUint8(offset + n, 0); + n++; + return n; +} + +export function countSerializeKey(key: IDBValidKey): number { + if (typeof key === "number") { + return 9; + } + if (key instanceof Date) { + return 9; + } + if (key instanceof ArrayBuffer) { + let len = 2; + const uv = new Uint8Array(key); + for (let i = 0; i < uv.length; i++) { + len += countEncSize(uv[i]); + } + return len; + } + if (ArrayBuffer.isView(key)) { + let len = 2; + const uv = new Uint8Array(key.buffer, key.byteOffset, key.byteLength); + for (let i = 0; i < uv.length; i++) { + len += countEncSize(uv[i]); + } + return len; + } + if (typeof key === "string") { + let len = 2; + for (let i = 0; i < key.length; i++) { + len += countEncSize(key.charCodeAt(i)); + } + return len; + } + if (Array.isArray(key)) { + let len = 2; + for (let i = 0; i < key.length; i++) { + len += countSerializeKey(key[i]); + } + return len; + } + throw Error("unsupported type for key"); +} + +function internalSerializeNumeric( + dv: DataView, + offset: number, + tag: number, + val: number, +): number { + dv.setUint8(offset, tagNum); + dv.setFloat64(offset + 1, val); + // Flip sign bit + let b = dv.getUint8(offset + 1); + b ^= 0x80; + dv.setUint8(offset + 1, b); + return 9; +} + +function internalSerializeArray( + dv: DataView, + offset: number, + key: any[], +): number { + dv.setUint8(offset, tagArray); + let n = 1; + for (let i = 0; i < key.length; i++) { + n += internalSerializeKey(key[i], dv, offset + n); + } + dv.setUint8(offset + n, 0); + n++; + return n; +} + +function internalSerializeBinary( + dv: DataView, + offset: number, + key: Uint8Array, +): number { + dv.setUint8(offset, tagBinary); + let n = 1; + for (let i = 0; i < key.length; i++) { + n += internalSerializeKey(key[i], dv, offset + n); + } + dv.setUint8(offset + n, 0); + n++; + return n; +} + +function internalSerializeKey( + key: IDBValidKey, + dv: DataView, + offset: number, +): number { + if (typeof key === "number") { + return internalSerializeNumeric(dv, offset, tagNum, key); + } + if (key instanceof Date) { + return internalSerializeNumeric(dv, offset, tagDate, key.getDate()); + } + if (typeof key === "string") { + return internalSerializeString(dv, offset, key); + } + if (Array.isArray(key)) { + return internalSerializeArray(dv, offset, key); + } + if (key instanceof ArrayBuffer) { + return internalSerializeBinary(dv, offset, new Uint8Array(key)); + } + if (ArrayBuffer.isView(key)) { + const uv = new Uint8Array(key.buffer, key.byteOffset, key.byteLength); + return internalSerializeBinary(dv, offset, uv); + } + throw Error("unsupported type for key"); +} + +export function serializeKey(key: IDBValidKey): Uint8Array { + const len = countSerializeKey(key); + let buf = new Uint8Array(len); + const outLen = internalSerializeKey(key, new DataView(buf.buffer), 0); + if (len != outLen) { + throw Error("internal invariant failed"); + } + let numTrailingZeroes = 0; + for (let i = buf.length - 1; i >= 0 && buf[i] == 0; i--, numTrailingZeroes++); + if (numTrailingZeroes > 0) { + buf = buf.slice(0, buf.length - numTrailingZeroes); + } + return buf; +} + +function internalReadString(dv: DataView, offset: number): [number, string] { + const chars: string[] = []; + while (offset < dv.byteLength) { + const v = dv.getUint8(offset); + if (v == 0) { + // Got end-of-string. + offset += 1; + break; + } + let c: number; + if ((v & threeByteMask) === threeByteMask) { + const b1 = v; + const b2 = dv.getUint8(offset + 1); + const b3 = dv.getUint8(offset + 2); + c = (b1 << 10) | (b2 << 2) | (b3 >> 6); + offset += 3; + } else if ((v & twoByteMask) === twoByteMask) { + const b1 = v & ~twoByteMask; + const b2 = dv.getUint8(offset + 1); + c = ((b1 << 8) | b2) + twoByteOffset; + offset += 2; + } else { + c = v - oneByteOffset; + offset += 1; + } + chars.push(String.fromCharCode(c)); + } + return [offset, chars.join("")]; +} + +function internalReadBytes(dv: DataView, offset: number): [number, Uint8Array] { + let count = 0; + while (offset + count < dv.byteLength) { + const v = dv.getUint8(offset + count); + if (v === 0) { + break; + } + count++; + } + let writePos = 0; + const bytes = new Uint8Array(count); + while (offset < dv.byteLength) { + const v = dv.getUint8(offset); + if (v == 0) { + offset += 1; + break; + } + let c: number; + if ((v & threeByteMask) === threeByteMask) { + const b1 = v; + const b2 = dv.getUint8(offset + 1); + const b3 = dv.getUint8(offset + 2); + c = (b1 << 10) | (b2 << 2) | (b3 >> 6); + offset += 3; + } else if ((v & twoByteMask) === twoByteMask) { + const b1 = v & ~twoByteMask; + const b2 = dv.getUint8(offset + 1); + c = ((b1 << 8) | b2) + twoByteOffset; + offset += 2; + } else { + c = v - oneByteOffset; + offset += 1; + } + bytes[writePos] = c; + writePos++; + } + return [offset, bytes]; +} + +/** + * Same as DataView.getFloat64, but logically pad input + * with zeroes on the right if read offset would be out + * of bounds. + * + * This allows reading from buffers where zeros have been + * truncated. + */ +function getFloat64Trunc(dv: DataView, offset: number): number { + if (offset + 7 >= dv.byteLength) { + const buf = new Uint8Array(8); + for (let i = offset; i < dv.byteLength; i++) { + buf[i - offset] = dv.getUint8(i); + } + const dv2 = new DataView(buf.buffer); + return dv2.getFloat64(0); + } else { + return dv.getFloat64(offset); + } +} + +function internalDeserializeKey( + dv: DataView, + offset: number, +): [number, IDBValidKey] { + let tag = dv.getUint8(offset); + switch (tag) { + case tagNum: { + const num = -getFloat64Trunc(dv, offset + 1); + const newOffset = Math.min(offset + 9, dv.byteLength); + return [newOffset, num]; + } + case tagDate: { + const num = -getFloat64Trunc(dv, offset + 1); + const newOffset = Math.min(offset + 9, dv.byteLength); + return [newOffset, new Date(num)]; + } + case tagString: { + return internalReadString(dv, offset + 1); + } + case tagBinary: { + return internalReadBytes(dv, offset + 1); + } + case tagArray: { + const arr: any[] = []; + offset += 1; + while (offset < dv.byteLength) { + const innerTag = dv.getUint8(offset); + if (innerTag === 0) { + offset++; + break; + } + const [innerOff, innerVal] = internalDeserializeKey(dv, offset); + arr.push(innerVal); + offset = innerOff; + } + return [offset, arr]; + } + default: + throw Error("invalid key (unrecognized tag)"); + } +} + +export function deserializeKey(encodedKey: Uint8Array): IDBValidKey { + const dv = new DataView( + encodedKey.buffer, + encodedKey.byteOffset, + encodedKey.byteLength, + ); + let [off, res] = internalDeserializeKey(dv, 0); + if (off != encodedKey.byteLength) { + throw Error("internal invariant failed"); + } + return res; +} diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts index 971697021..c1216fe97 100644 --- a/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts +++ b/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts @@ -20,55 +20,73 @@ import { makeStoreKeyValue } from "./makeStoreKeyValue.js"; test("basics", (t) => { let result; - result = makeStoreKeyValue({ name: "Florian" }, undefined, 42, true, "id"); + result = makeStoreKeyValue({ + value: { name: "Florian" }, + key: undefined, + currentKeyGenerator: 42, + autoIncrement: true, + keyPath: "id", + }); t.is(result.updatedKeyGenerator, 43); t.is(result.key, 42); t.is(result.value.name, "Florian"); t.is(result.value.id, 42); - result = makeStoreKeyValue( - { name: "Florian", id: 10 }, - undefined, - 5, - true, - "id", - ); + result = makeStoreKeyValue({ + value: { name: "Florian", id: 10 }, + key: undefined, + currentKeyGenerator: 5, + autoIncrement: true, + keyPath: "id", + }); t.is(result.updatedKeyGenerator, 11); t.is(result.key, 10); t.is(result.value.name, "Florian"); t.is(result.value.id, 10); - result = makeStoreKeyValue( - { name: "Florian", id: 5 }, - undefined, - 10, - true, - "id", - ); + result = makeStoreKeyValue({ + value: { name: "Florian", id: 5 }, + key: undefined, + currentKeyGenerator: 10, + autoIncrement: true, + keyPath: "id", + }); t.is(result.updatedKeyGenerator, 10); t.is(result.key, 5); t.is(result.value.name, "Florian"); t.is(result.value.id, 5); - result = makeStoreKeyValue( - { name: "Florian", id: "foo" }, - undefined, - 10, - true, - "id", - ); + result = makeStoreKeyValue({ + value: { name: "Florian", id: "foo" }, + key: undefined, + currentKeyGenerator: 10, + autoIncrement: true, + keyPath: "id", + }); t.is(result.updatedKeyGenerator, 10); t.is(result.key, "foo"); t.is(result.value.name, "Florian"); t.is(result.value.id, "foo"); - result = makeStoreKeyValue({ name: "Florian" }, "foo", 10, true, null); + result = makeStoreKeyValue({ + value: { name: "Florian" }, + key: "foo", + currentKeyGenerator: 10, + autoIncrement: true, + keyPath: null, + }); t.is(result.updatedKeyGenerator, 10); t.is(result.key, "foo"); t.is(result.value.name, "Florian"); t.is(result.value.id, undefined); - result = makeStoreKeyValue({ name: "Florian" }, undefined, 10, true, null); + result = makeStoreKeyValue({ + value: { name: "Florian" }, + key: undefined, + currentKeyGenerator: 10, + autoIncrement: true, + keyPath: null, + }); t.is(result.updatedKeyGenerator, 11); t.is(result.key, 10); t.is(result.value.name, "Florian"); diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.ts index 4c7dab8d2..153cd9d81 100644 --- a/packages/idb-bridge/src/util/makeStoreKeyValue.ts +++ b/packages/idb-bridge/src/util/makeStoreKeyValue.ts @@ -75,19 +75,25 @@ function injectKey( return newValue; } -export function makeStoreKeyValue( - value: any, - key: IDBValidKey | undefined, - currentKeyGenerator: number, - autoIncrement: boolean, - keyPath: IDBKeyPath | IDBKeyPath[] | null, -): StoreKeyResult { +export interface MakeStoreKvRequest { + value: any; + key: IDBValidKey | undefined; + currentKeyGenerator: number; + autoIncrement: boolean; + keyPath: IDBKeyPath | IDBKeyPath[] | null; +} + +export function makeStoreKeyValue(req: MakeStoreKvRequest): StoreKeyResult { + const { keyPath, currentKeyGenerator, autoIncrement } = req; + let { key, value } = req; + const haveKey = key !== null && key !== undefined; const haveKeyPath = keyPath !== null && keyPath !== undefined; // This models a decision table on (haveKey, haveKeyPath, autoIncrement) try { + // FIXME: Perf: only do this if we need to inject something. value = structuredClone(value); } catch (e) { throw new DataCloneError(); diff --git a/packages/idb-bridge/src/util/queueTask.ts b/packages/idb-bridge/src/util/queueTask.ts index 297602c67..f8a6e799f 100644 --- a/packages/idb-bridge/src/util/queueTask.ts +++ b/packages/idb-bridge/src/util/queueTask.ts @@ -14,6 +14,11 @@ permissions and limitations under the License. */ +/** + * Queue a task to be executed *after* the microtask + * queue has been processed, but *before* subsequent setTimeout / setImmediate + * tasks. + */ export function queueTask(fn: () => void) { let called = false; const callFirst = () => { diff --git a/packages/idb-bridge/src/util/structuredClone.test.ts b/packages/idb-bridge/src/util/structuredClone.test.ts index 0c613e6cc..e13d4117f 100644 --- a/packages/idb-bridge/src/util/structuredClone.test.ts +++ b/packages/idb-bridge/src/util/structuredClone.test.ts @@ -15,7 +15,11 @@ */ import test, { ExecutionContext } from "ava"; -import { structuredClone } from "./structuredClone.js"; +import { + structuredClone, + structuredEncapsulate, + structuredRevive, +} from "./structuredClone.js"; function checkClone(t: ExecutionContext, x: any): void { t.deepEqual(structuredClone(x), x); @@ -59,3 +63,58 @@ test("structured clone (object cycles)", (t) => { const obj1Clone = structuredClone(obj1); t.is(obj1Clone, obj1Clone.c); }); + +test("encapsulate", (t) => { + t.deepEqual(structuredEncapsulate(42), 42); + t.deepEqual(structuredEncapsulate(true), true); + t.deepEqual(structuredEncapsulate(false), false); + t.deepEqual(structuredEncapsulate(null), null); + + t.deepEqual(structuredEncapsulate(undefined), { $: "undef" }); + t.deepEqual(structuredEncapsulate(42n), { $: "bigint", val: "42" }); + + t.deepEqual(structuredEncapsulate(new Date(42)), { $: "date", val: 42 }); + + t.deepEqual(structuredEncapsulate({ x: 42 }), { x: 42 }); + + t.deepEqual(structuredEncapsulate({ $: "bla", x: 42 }), { + $: "obj", + val: { $: "bla", x: 42 }, + }); + + const x = { foo: 42, bar: {} } as any; + x.bar.baz = x; + + t.deepEqual(structuredEncapsulate(x), { + foo: 42, + bar: { + baz: { $: "ref", d: 2, p: [] }, + }, + }); +}); + +test("revive", (t) => { + t.deepEqual(structuredRevive(42), 42); + t.deepEqual(structuredRevive([1, 2, 3]), [1, 2, 3]); + t.deepEqual(structuredRevive(true), true); + t.deepEqual(structuredRevive(false), false); + t.deepEqual(structuredRevive(null), null); + t.deepEqual(structuredRevive({ $: "undef" }), undefined); + t.deepEqual(structuredRevive({ x: { $: "undef" } }), { x: undefined }); + + t.deepEqual(structuredRevive({ $: "date", val: 42}), new Date(42)); + + { + const x = { foo: 42, bar: {} } as any; + x.bar.baz = x; + + const r = { + foo: 42, + bar: { + baz: { $: "ref", d: 2, p: [] }, + }, + }; + + t.deepEqual(structuredRevive(r), x); + } +}); diff --git a/packages/idb-bridge/src/util/structuredClone.ts b/packages/idb-bridge/src/util/structuredClone.ts index 2170118d5..2f857c6c5 100644 --- a/packages/idb-bridge/src/util/structuredClone.ts +++ b/packages/idb-bridge/src/util/structuredClone.ts @@ -16,22 +16,21 @@ /** * Encoding (new, compositional version): - * + * * Encapsulate object that itself might contain a "$" field: - * { $: { E... } } + * { $: "obj", val: ... } + * (Outer level only:) Wrap other values into object + * { $: "lit", val: ... } * Circular reference: - * { $: ["ref", uplevel, field...] } + * { $: "ref" l: uplevel, p: path } * Date: - * { $: ["data"], val: datestr } + * { $: "date", val: datestr } * Bigint: - * { $: ["bigint"], val: bigintstr } + * { $: "bigint", val: bigintstr } * Array with special (non-number) attributes: - * { $: ["array"], val: arrayobj } + * { $: "array", val: arrayobj } * Undefined field * { $: "undef" } - * - * Legacy (top-level only), for backwards compatibility: - * { $types: [...] } */ /** @@ -261,22 +260,18 @@ export function mkDeepCloneCheckOnly() { 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; + return { $: "ref", d: path.length, p: memoPath }; } if (val === null) { return null; } if (val === undefined) { - types.push([path, "undef"]); - return 0; + return { $: "undef" }; } if (Array.isArray(val)) { memo.set(val, path); @@ -289,31 +284,33 @@ function internalEncapsulate( 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); + outArr[x] = internalEncapsulate(val[x], p, memo); + } + if (special) { + return { $: "array", val: outArr }; + } else { + return outArr; } - return outArr; } if (val instanceof Date) { - types.push([path, "date"]); - return val.getTime(); + 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], outRoot, p, memo, types); + outObj[x] = internalEncapsulate(val[x], p, memo); + } + if ("$" in outObj) { + return { $: "obj", val: outObj }; } return outObj; } if (typeof val === "bigint") { - types.push([path, "bigint"]); - return val.toString(); + return { $: "bigint", val: val.toString() }; } if (typeof val === "boolean") { return val; @@ -327,123 +324,103 @@ function internalEncapsulate( throw Error(); } -/** - * Encapsulate a cloneable value into a plain JSON object. - */ -export function structuredEncapsulate(val: any): any { - const outRoot = {}; - const types: Array<[string[], string]> = []; - let res; - res = internalEncapsulate(val, outRoot, [], new Map(), types); - 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; +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; } -export function applyLegacyTypeAnnotations(val: any): any { - if (val === null) { - return null; +function internalReviveArray(sval: any, root: any, path: string[]): any { + const newArr: any[] = []; + if (root === undefined) { + root = newArr; } - if (typeof val === "number") { - return val; + for (let i = 0; i < sval.length; i++) { + const p = [...path, String(i)]; + newArr.push(internalStructuredRevive(sval[i], root, p)); } - if (typeof val === "string") { - return val; + return newArr; +} + +function internalReviveObject(sval: any, root: any, path: string[]): any { + const newObj = {} as any; + if (root === undefined) { + root = newObj; } - if (typeof val === "boolean") { - return val; + for (const key of Object.keys(sval)) { + const p = [...path, key]; + newObj[key] = internalStructuredRevive(sval[key], root, p); } - 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]); + return newObj; +} + +function internalStructuredRevive(sval: any, root: any, path: string[]): any { + if (typeof sval === "string") { + return sval; } - function lookupPath(path: string[]): any { - let obj = outRoot; - for (const n of path) { - obj = obj[n]; - } - return obj; + if (typeof sval === "number") { + return sval; } - for (const [path, type] of types) { - switch (type) { - case "bigint": { - mutatePath(path, (x) => BigInt(x)); - break; - } - case "array": { - mutatePath(path, (x) => { - const newArr: any = []; - for (const k in x) { - newArr[k] = x[k]; - } - return newArr; - }); - break; - } - case "date": { - mutatePath(path, (x) => new Date(x)); - break; - } - case "undef": { - mutatePath(path, (x) => undefined); - break; - } - case "ref": { - mutatePath(path, (x) => lookupPath(x)); - break; + 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(); } - default: - throw Error(`type '${type}' not implemented`); + } else { + return internalReviveObject(sval, root, path); } } - return outRoot; + + throw Error(); } -export function internalStructuredRevive(val: any): any { - // FIXME: Do the newly specified, compositional encoding here. - val = JSON.parse(JSON.stringify(val)); - return val; +/** + * Encapsulate a cloneable value into a plain JSON value. + */ +export function structuredEncapsulate(val: any): any { + return internalEncapsulate(val, [], new Map()); } -export function structuredRevive(val: any): any { - const r = internalStructuredRevive(val); - return applyLegacyTypeAnnotations(r); +export function structuredRevive(sval: any): any { + return internalStructuredRevive(sval, undefined, []); } /** diff --git a/packages/idb-bridge/src/util/valueToKey.ts b/packages/idb-bridge/src/util/valueToKey.ts index 6df82af81..0cd824689 100644 --- a/packages/idb-bridge/src/util/valueToKey.ts +++ b/packages/idb-bridge/src/util/valueToKey.ts @@ -17,7 +17,11 @@ import { IDBValidKey } from "../idbtypes.js"; import { DataError } from "./errors.js"; -// https://www.w3.org/TR/IndexedDB-2/#convert-a-value-to-a-key +/** + * Algorithm to "convert a value to a key". + * + * https://www.w3.org/TR/IndexedDB/#convert-value-to-key + */ export function valueToKey( input: any, seen?: Set<object>, |