aboutsummaryrefslogtreecommitdiff
path: root/packages/idb-bridge/src/util
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-07-11 15:41:48 +0200
committerFlorian Dold <florian@dold.me>2023-08-22 08:01:13 +0200
commitb2d0ad57ddf251a109d536cdc49fb6505dbdc50c (patch)
tree7eaeca3ad8ec97a9c1970c1004feda2d61c3441b /packages/idb-bridge/src/util
parent58fdf9dc091b076787a9746c405fe6a9366f5da6 (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.ts103
-rw-r--r--packages/idb-bridge/src/util/FakeEventTarget.ts2
-rw-r--r--packages/idb-bridge/src/util/extractKey.ts4
-rw-r--r--packages/idb-bridge/src/util/key-storage.test.ts39
-rw-r--r--packages/idb-bridge/src/util/key-storage.ts363
-rw-r--r--packages/idb-bridge/src/util/makeStoreKeyValue.test.ts66
-rw-r--r--packages/idb-bridge/src/util/makeStoreKeyValue.ts20
-rw-r--r--packages/idb-bridge/src/util/queueTask.ts5
-rw-r--r--packages/idb-bridge/src/util/structuredClone.test.ts61
-rw-r--r--packages/idb-bridge/src/util/structuredClone.ts231
-rw-r--r--packages/idb-bridge/src/util/valueToKey.ts6
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>,