diff options
author | Florian Dold <florian@dold.me> | 2021-02-08 19:59:19 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2021-02-08 19:59:19 +0100 |
commit | 8c92499d85917693d2f87252419f0eeccd239a2b (patch) | |
tree | 569d0ee1c25d62caf0ac87131ecfa8166d12c62b /packages | |
parent | 5ff5a686e4f15dea839b18fda9275687557d23a7 (diff) |
idb: add first web platform tests, fix issues detected by them
Diffstat (limited to 'packages')
-rw-r--r-- | packages/idb-bridge/package.json | 4 | ||||
-rw-r--r-- | packages/idb-bridge/src/MemoryBackend.ts | 27 | ||||
-rw-r--r-- | packages/idb-bridge/src/backend-interface.ts | 6 | ||||
-rw-r--r-- | packages/idb-bridge/src/bridge-idb.ts | 316 | ||||
-rw-r--r-- | packages/idb-bridge/src/idb-wpt-ported/README | 3 | ||||
-rw-r--r-- | packages/idb-bridge/src/idb-wpt-ported/keypath.test.ts | 191 | ||||
-rw-r--r-- | packages/idb-bridge/src/idb-wpt-ported/value.test.ts | 46 | ||||
-rw-r--r-- | packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts | 30 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/injectKey.ts | 4 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/makeStoreKeyValue.ts | 10 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/normalizeKeyPath.ts | 41 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/structuredClone.ts | 21 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/query.ts | 6 |
13 files changed, 558 insertions, 147 deletions
diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json index cfa0fa9f6..5f7b3d114 100644 --- a/packages/idb-bridge/package.json +++ b/packages/idb-bridge/package.json @@ -25,7 +25,9 @@ "typescript": "^4.1.3" }, "dependencies": { - "tslib": "^2.1.0" + "tslib": "^2.1.0", + "typeson": "^5.18.2", + "typeson-registry": "^1.0.0-alpha.38" }, "ava": { "require": [ diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts index 6a52a555f..2b4437bcf 100644 --- a/packages/idb-bridge/src/MemoryBackend.ts +++ b/packages/idb-bridge/src/MemoryBackend.ts @@ -27,7 +27,11 @@ import { StoreLevel, RecordStoreResponse, } from "./backend-interface"; -import structuredClone from "./util/structuredClone"; +import { + structuredClone, + structuredEncapsulate, + structuredRevive, +} from "./util/structuredClone"; import { InvalidStateError, InvalidAccessError, @@ -39,7 +43,12 @@ import compareKeys from "./util/cmp"; import { StoreKeyResult, makeStoreKeyValue } from "./util/makeStoreKeyValue"; import getIndexKeys from "./util/getIndexKeys"; import openPromise from "./util/openPromise"; -import { IDBKeyPath, IDBKeyRange, IDBTransactionMode, IDBValidKey } from "./idbtypes"; +import { + IDBKeyPath, + IDBKeyRange, + IDBTransactionMode, + IDBValidKey, +} from "./idbtypes"; import { BridgeIDBKeyRange } from "./bridge-idb"; type Key = IDBValidKey; @@ -742,7 +751,7 @@ export class MemoryBackend implements Backend { createObjectStore( btx: DatabaseTransaction, name: string, - keyPath: string | string[] | null, + keyPath: string[] | null, autoIncrement: boolean, ): void { if (this.enableTracing) { @@ -776,9 +785,6 @@ export class MemoryBackend implements Backend { if (!schema) { throw Error("no schema for versionchange tx"); } - if (Array.isArray(keyPath)) { - throw Error("array key path not supported for object stores"); - } schema.objectStores[name] = { autoIncrement, keyPath, @@ -791,7 +797,7 @@ export class MemoryBackend implements Backend { btx: DatabaseTransaction, indexName: string, objectStoreName: string, - keyPath: IDBKeyPath, + keyPath: string[], multiEntry: boolean, unique: boolean, ): void { @@ -1401,9 +1407,10 @@ export class MemoryBackend implements Backend { schema.objectStores[storeReq.objectStoreName].autoIncrement; const keyPath = schema.objectStores[storeReq.objectStoreName].keyPath; let storeKeyResult: StoreKeyResult; + const revivedValue = structuredRevive(storeReq.value); try { storeKeyResult = makeStoreKeyValue( - storeReq.value, + revivedValue, storeReq.key, keygen, autoIncrement, @@ -1413,7 +1420,9 @@ export class MemoryBackend implements Backend { if (e instanceof DataError) { const kp = JSON.stringify(keyPath); const n = storeReq.objectStoreName; - const m = `Could not extract key from value, objectStore=${n}, keyPath=${kp}`; + const m = `Could not extract key from value, objectStore=${n}, keyPath=${kp}, value=${JSON.stringify( + storeReq.value, + )}`; if (this.enableTracing) { console.error(e); console.error("value was:", storeReq.value); diff --git a/packages/idb-bridge/src/backend-interface.ts b/packages/idb-bridge/src/backend-interface.ts index 756a5b967..a9e3e960e 100644 --- a/packages/idb-bridge/src/backend-interface.ts +++ b/packages/idb-bridge/src/backend-interface.ts @@ -25,14 +25,14 @@ import { /** @public */ export interface ObjectStoreProperties { - keyPath: IDBKeyPath | null; + keyPath: string[] | null; autoIncrement: boolean; indexes: { [nameame: string]: IndexProperties }; } /** @public */ export interface IndexProperties { - keyPath: IDBKeyPath; + keyPath: string[]; multiEntry: boolean; unique: boolean; } @@ -199,7 +199,7 @@ export interface Backend { btx: DatabaseTransaction, indexName: string, objectStoreName: string, - keyPath: IDBKeyPath, + keyPath: string | string[], multiEntry: boolean, unique: boolean, ): void; diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts index 2bced800d..ce09fcb8e 100644 --- a/packages/idb-bridge/src/bridge-idb.ts +++ b/packages/idb-bridge/src/bridge-idb.ts @@ -25,7 +25,23 @@ import { Schema, StoreLevel, } from "./backend-interface"; -import { EventListener, IDBCursorDirection, IDBKeyPath, IDBKeyRange, IDBTransactionMode, IDBValidKey } from "./idbtypes"; +import { + DOMException, + DOMStringList, + EventListener, + IDBCursor, + IDBCursorDirection, + IDBDatabase, + IDBIndex, + IDBKeyPath, + IDBKeyRange, + IDBObjectStore, + IDBOpenDBRequest, + IDBRequest, + IDBTransaction, + IDBTransactionMode, + IDBValidKey, +} from "./idbtypes"; import compareKeys from "./util/cmp"; import enforceRange from "./util/enforceRange"; import { @@ -42,9 +58,10 @@ import { import { fakeDOMStringList } from "./util/fakeDOMStringList"; import FakeEvent from "./util/FakeEvent"; import FakeEventTarget from "./util/FakeEventTarget"; +import { normalizeKeyPath } from "./util/normalizeKeyPath"; import openPromise from "./util/openPromise"; import queueTask from "./util/queueTask"; -import structuredClone from "./util/structuredClone"; +import { structuredClone, structuredEncapsulate, structuredRevive } from "./util/structuredClone"; import validateKeyPath from "./util/validateKeyPath"; import valueToKey from "./util/valueToKey"; @@ -87,7 +104,7 @@ function simplifyRange( * * @public */ -export class BridgeIDBCursor { +export class BridgeIDBCursor implements IDBCursor { _request: BridgeIDBRequest | undefined; private _gotValue: boolean = false; @@ -127,7 +144,7 @@ export class BridgeIDBCursor { if (this.source instanceof BridgeIDBObjectStore) { return this.source; } - return this.source.objectStore; + return this.source._objectStore; } get _backend(): Backend { @@ -149,15 +166,23 @@ export class BridgeIDBCursor { /* For babel */ } - get key() { - return this._key; + get key(): IDBValidKey { + const k = this._key; + if (k === null || k === undefined) { + throw Error("no key"); + } + return k; } set key(val) { /* For babel */ } - get primaryKey() { - return this._primaryKey; + get primaryKey(): IDBValidKey { + const k = this._primaryKey; + if (k === 0 || k === undefined) { + throw Error("no key"); + } + return k; } set primaryKey(val) { @@ -221,7 +246,7 @@ export class BridgeIDBCursor { this._primaryKey = response.primaryKeys![0]; if (!this._keyOnly) { - this._value = response.values![0]; + this._value = structuredRevive(response.values![0]); } this._gotValue = true; @@ -239,7 +264,7 @@ export class BridgeIDBCursor { throw new TypeError(); } - const transaction = this._effectiveObjectStore.transaction; + const transaction = this._effectiveObjectStore._transaction; if (transaction._state !== "active") { throw new TransactionInactiveError(); @@ -266,7 +291,7 @@ export class BridgeIDBCursor { const storeReq: RecordStoreRequest = { key: this._primaryKey, - value: value, + value: structuredEncapsulate(value), objectStoreName: this._objectStoreName, storeLevel: StoreLevel.UpdateExisting, }; @@ -295,7 +320,7 @@ export class BridgeIDBCursor { * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-continue-void-any-key */ public continue(key?: IDBValidKey) { - const transaction = this._effectiveObjectStore.transaction; + const transaction = this._effectiveObjectStore._transaction; if (transaction._state !== "active") { throw new TransactionInactiveError(); @@ -357,7 +382,7 @@ export class BridgeIDBCursor { } public delete() { - const transaction = this._effectiveObjectStore.transaction; + const transaction = this._effectiveObjectStore._transaction; if (transaction._state !== "active") { throw new TransactionInactiveError(); @@ -547,7 +572,7 @@ export class BridgeIDBDatabase extends FakeEventTarget { transaction._backend.createObjectStore( backendTx, name, - keyPath, + (keyPath !== null) ? normalizeKeyPath(keyPath) : null, autoIncrement, ); @@ -583,7 +608,7 @@ export class BridgeIDBDatabase extends FakeEventTarget { return ( transaction._state === "active" && transaction.mode === "versionchange" && - transaction.db === this + transaction._db === this ); }, ); @@ -656,7 +681,7 @@ export class BridgeIDBFactory { // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBFactory-deleteDatabase-IDBOpenDBRequest-DOMString-name public deleteDatabase(name: string): BridgeIDBOpenDBRequest { const request = new BridgeIDBOpenDBRequest(); - request.source = null; + request._source = null; queueTask(async () => { const databases = await this.backend.getDatabases(); @@ -709,7 +734,7 @@ export class BridgeIDBFactory { // tslint:disable-next-line max-line-length // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBFactory-open-IDBOpenDBRequest-DOMString-name-unsigned-long-long-version - public open(name: string, version?: number) { + public open(name: string, version?: number): BridgeIDBOpenDBRequest { if (arguments.length > 1 && version !== undefined) { // Based on spec, not sure why "MAX_SAFE_INTEGER" instead of "unsigned long long", but it's needed to pass // tests @@ -761,9 +786,7 @@ export class BridgeIDBFactory { }); event2.eventPath = [request]; request.dispatchEvent(event2); - } - - if (existingVersion < requestedVersion) { + } else if (existingVersion < requestedVersion) { // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-running-a-versionchange-transaction for (const otherConn of this.connections) { @@ -803,7 +826,9 @@ export class BridgeIDBFactory { request.transaction = transaction; request.dispatchEvent(event); + console.log("awaiting until initial transaction is done"); await transaction._waitDone(); + console.log("initial transaction is done"); // We don't explicitly exit the versionchange transaction, // since this is already done by the BridgeIDBTransaction. @@ -842,47 +867,51 @@ export class BridgeIDBFactory { const confirmActiveTransaction = ( index: BridgeIDBIndex, ): BridgeIDBTransaction => { - if (index._deleted || index.objectStore._deleted) { + if (index._deleted || index._objectStore._deleted) { throw new InvalidStateError(); } - if (index.objectStore.transaction._state !== "active") { + if (index._objectStore._transaction._state !== "active") { throw new TransactionInactiveError(); } - return index.objectStore.transaction; + return index._objectStore._transaction; }; // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#idl-def-IDBIndex /** @public */ -export class BridgeIDBIndex { - objectStore: BridgeIDBObjectStore; +export class BridgeIDBIndex implements IDBIndex { + _objectStore: BridgeIDBObjectStore; + + get objectStore(): IDBObjectStore { + return this._objectStore; + } get _schema(): Schema { - return this.objectStore.transaction.db._schema; + return this._objectStore._transaction._db._schema; } - get keyPath(): IDBKeyPath { - return this._schema.objectStores[this.objectStore.name].indexes[this._name] + get keyPath(): IDBKeyPath | IDBKeyPath[] { + return this._schema.objectStores[this._objectStore.name].indexes[this._name] .keyPath; } get multiEntry(): boolean { - return this._schema.objectStores[this.objectStore.name].indexes[this._name] + return this._schema.objectStores[this._objectStore.name].indexes[this._name] .multiEntry; } get unique(): boolean { - return this._schema.objectStores[this.objectStore.name].indexes[this._name] + return this._schema.objectStores[this._objectStore.name].indexes[this._name] .unique; } get _backend(): Backend { - return this.objectStore._backend; + return this._objectStore._backend; } _confirmActiveTransaction(): { btx: DatabaseTransaction } { - return this.objectStore._confirmActiveTransaction(); + return this._objectStore._confirmActiveTransaction(); } private _name: string; @@ -891,7 +920,7 @@ export class BridgeIDBIndex { constructor(objectStore: BridgeIDBObjectStore, name: string) { this._name = name; - this.objectStore = objectStore; + this._objectStore = objectStore; } get name() { @@ -900,9 +929,9 @@ export class BridgeIDBIndex { // https://w3c.github.io/IndexedDB/#dom-idbindex-name set name(name: any) { - const transaction = this.objectStore.transaction; + const transaction = this._objectStore._transaction; - if (!transaction.db._runningVersionchangeTransaction) { + if (!transaction._db._runningVersionchangeTransaction) { throw new InvalidStateError(); } @@ -919,9 +948,9 @@ export class BridgeIDBIndex { return; } - this._backend.renameIndex(btx, this.objectStore.name, oldName, newName); + this._backend.renameIndex(btx, this._objectStore.name, oldName, newName); - if (this.objectStore.indexNames.indexOf(name) >= 0) { + if (this._objectStore._indexNames.indexOf(name) >= 0) { throw new ConstraintError(); } } @@ -942,12 +971,12 @@ export class BridgeIDBIndex { } const request = new BridgeIDBRequest(); - request.source = this; - request.transaction = this.objectStore.transaction; + request._source = this; + request.transaction = this._objectStore._transaction; const cursor = new BridgeIDBCursorWithValue( this, - this.objectStore.name, + this._objectStore.name, this._name, range, direction, @@ -958,7 +987,7 @@ export class BridgeIDBIndex { return cursor._iterate(); }; - return this.objectStore.transaction._execRequestAsync({ + return this._objectStore._transaction._execRequestAsync({ operation, request, source: this, @@ -981,12 +1010,12 @@ export class BridgeIDBIndex { } const request = new BridgeIDBRequest(); - request.source = this; - request.transaction = this.objectStore.transaction; + request._source = this; + request.transaction = this._objectStore._transaction; const cursor = new BridgeIDBCursor( this, - this.objectStore.name, + this._objectStore.name, this._name, range, direction, @@ -994,7 +1023,7 @@ export class BridgeIDBIndex { true, ); - return this.objectStore.transaction._execRequestAsync({ + return this._objectStore._transaction._execRequestAsync({ operation: cursor._iterate.bind(cursor), request, source: this, @@ -1013,7 +1042,7 @@ export class BridgeIDBIndex { indexName: this._name, limit: 1, range: key, - objectStoreName: this.objectStore._name, + objectStoreName: this._objectStore._name, resultLevel: ResultLevel.Full, }; @@ -1027,17 +1056,20 @@ export class BridgeIDBIndex { if (!values) { throw Error("invariant violated"); } - return values[0]; + return structuredRevive(values[0]); }; - return this.objectStore.transaction._execRequestAsync({ + return this._objectStore._transaction._execRequestAsync({ operation, source: this, }); } // http://w3c.github.io/IndexedDB/#dom-idbindex-getall - public getAll(query?: BridgeIDBKeyRange | IDBValidKey, count?: number) { + public getAll( + query?: BridgeIDBKeyRange | IDBValidKey, + count?: number, + ): IDBRequest<any[]> { throw Error("not implemented"); } @@ -1054,7 +1086,7 @@ export class BridgeIDBIndex { indexName: this._name, limit: 1, range: key, - objectStoreName: this.objectStore._name, + objectStoreName: this._objectStore._name, resultLevel: ResultLevel.OnlyKeys, }; @@ -1071,14 +1103,17 @@ export class BridgeIDBIndex { return primaryKeys[0]; }; - return this.objectStore.transaction._execRequestAsync({ + return this._objectStore._transaction._execRequestAsync({ operation, source: this, }); } // http://w3c.github.io/IndexedDB/#dom-idbindex-getallkeys - public getAllKeys(query?: BridgeIDBKeyRange | IDBValidKey, count?: number) { + public getAllKeys( + query?: BridgeIDBKeyRange | IDBValidKey, + count?: number, + ): IDBRequest<IDBValidKey[]> { throw Error("not implemented"); } @@ -1098,7 +1133,7 @@ export class BridgeIDBIndex { indexName: this._name, limit: 1, range: key, - objectStoreName: this.objectStore._name, + objectStoreName: this._objectStore._name, resultLevel: ResultLevel.OnlyCount, }; @@ -1108,7 +1143,7 @@ export class BridgeIDBIndex { return result.count; }; - return this.objectStore.transaction._execRequestAsync({ + return this._objectStore._transaction._execRequestAsync({ operation, source: this, }); @@ -1231,36 +1266,44 @@ export class BridgeIDBKeyRange { // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#object-store /** @public */ -export class BridgeIDBObjectStore { +export class BridgeIDBObjectStore implements IDBObjectStore { _indexesCache: Map<string, BridgeIDBIndex> = new Map(); - transaction: BridgeIDBTransaction; + _transaction: BridgeIDBTransaction; + + get transaction(): IDBTransaction { + return this._transaction; + } get autoIncrement(): boolean { return this._schema.objectStores[this._name].autoIncrement; } - get indexNames(): FakeDOMStringList { + get _indexNames(): FakeDOMStringList { return fakeDOMStringList( Object.keys(this._schema.objectStores[this._name].indexes), - ).sort(); + ).sort() + } + + get indexNames(): DOMStringList { + return this._indexNames as DOMStringList; } - get keyPath(): IDBKeyPath | null { - return this._schema.objectStores[this._name].keyPath; + get keyPath(): IDBKeyPath | IDBKeyPath[] { + return this._schema.objectStores[this._name].keyPath!; } _name: string; get _schema(): Schema { - return this.transaction.db._schema; + return this._transaction._db._schema; } _deleted: boolean = false; constructor(transaction: BridgeIDBTransaction, name: string) { this._name = name; - this.transaction = transaction; + this._transaction = transaction; } get name() { @@ -1268,15 +1311,15 @@ export class BridgeIDBObjectStore { } get _backend(): Backend { - return this.transaction.db._backend; + return this._transaction._db._backend; } get _backendConnection(): DatabaseConnection { - return this.transaction.db._backendConnection; + return this._transaction._db._backendConnection; } _confirmActiveTransaction(): { btx: DatabaseTransaction } { - const btx = this.transaction._backendTransaction; + const btx = this._transaction._backendTransaction; if (!btx) { throw new InvalidStateError(); } @@ -1285,9 +1328,9 @@ export class BridgeIDBObjectStore { // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-name set name(newName: any) { - const transaction = this.transaction; + const transaction = this._transaction; - if (!transaction.db._runningVersionchangeTransaction) { + if (!transaction._db._runningVersionchangeTransaction) { throw new InvalidStateError(); } @@ -1302,7 +1345,7 @@ export class BridgeIDBObjectStore { } this._backend.renameObjectStore(btx, oldName, newName); - this.transaction.db._schema = this._backend.getSchema( + this._transaction._db._schema = this._backend.getSchema( this._backendConnection, ); } @@ -1311,7 +1354,7 @@ export class BridgeIDBObjectStore { if (BridgeIDBFactory.enableTracing) { console.log(`TRACE: IDBObjectStore._store`); } - if (this.transaction.mode === "readonly") { + if (this._transaction.mode === "readonly") { throw new ReadOnlyError(); } const operation = async () => { @@ -1319,7 +1362,7 @@ export class BridgeIDBObjectStore { const result = await this._backend.storeRecord(btx, { objectStoreName: this._name, key: key, - value: value, + value: structuredEncapsulate(value), storeLevel: overwrite ? StoreLevel.AllowOverwrite : StoreLevel.NoOverwrite, @@ -1327,7 +1370,7 @@ export class BridgeIDBObjectStore { return result.key; }; - return this.transaction._execRequestAsync({ operation, source: this }); + return this._transaction._execRequestAsync({ operation, source: this }); } public put(value: any, key?: IDBValidKey) { @@ -1349,7 +1392,7 @@ export class BridgeIDBObjectStore { throw new TypeError(); } - if (this.transaction.mode === "readonly") { + if (this._transaction.mode === "readonly") { throw new ReadOnlyError(); } @@ -1366,7 +1409,7 @@ export class BridgeIDBObjectStore { return this._backend.deleteRecord(btx, this._name, keyRange); }; - return this.transaction._execRequestAsync({ + return this._transaction._execRequestAsync({ operation, source: this, }); @@ -1424,31 +1467,39 @@ export class BridgeIDBObjectStore { if (!values) { throw Error("invariant violated"); } - return values[0]; + return structuredRevive(values[0]); }; - return this.transaction._execRequestAsync({ + return this._transaction._execRequestAsync({ operation, source: this, }); } // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getall - public getAll(query?: BridgeIDBKeyRange | IDBValidKey, count?: number) { + public getAll( + query?: BridgeIDBKeyRange | IDBValidKey, + count?: number, + ): IDBRequest<any[]> { throw Error("not implemented"); } // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getkey - public getKey(key?: BridgeIDBKeyRange | IDBValidKey) { + public getKey( + key?: BridgeIDBKeyRange | IDBValidKey, + ): IDBRequest<IDBValidKey | undefined> { throw Error("not implemented"); } // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getallkeys - public getAllKeys(query?: BridgeIDBKeyRange | IDBValidKey, count?: number) { + public getAllKeys( + query?: BridgeIDBKeyRange | IDBValidKey, + count?: number, + ): IDBRequest<any[]> { throw Error("not implemented"); } - public clear() { + public clear(): IDBRequest<undefined> { throw Error("not implemented"); } @@ -1464,8 +1515,8 @@ export class BridgeIDBObjectStore { } const request = new BridgeIDBRequest(); - request.source = this; - request.transaction = this.transaction; + request._source = this; + request.transaction = this._transaction; const cursor = new BridgeIDBCursorWithValue( this, @@ -1476,7 +1527,7 @@ export class BridgeIDBObjectStore { request, ); - return this.transaction._execRequestAsync({ + return this._transaction._execRequestAsync({ operation: () => cursor._iterate(), request, source: this, @@ -1499,8 +1550,8 @@ export class BridgeIDBObjectStore { } const request = new BridgeIDBRequest(); - request.source = this; - request.transaction = this.transaction; + request._source = this; + request.transaction = this._transaction; const cursor = new BridgeIDBCursor( this, @@ -1512,7 +1563,7 @@ export class BridgeIDBObjectStore { true, ); - return this.transaction._execRequestAsync({ + return this._transaction._execRequestAsync({ operation: cursor._iterate.bind(cursor), request, source: this, @@ -1530,7 +1581,7 @@ export class BridgeIDBObjectStore { throw new TypeError(); } - if (!this.transaction.db._runningVersionchangeTransaction) { + if (!this._transaction._db._runningVersionchangeTransaction) { throw new InvalidStateError(); } @@ -1545,11 +1596,11 @@ export class BridgeIDBObjectStore { ? optionalParameters.unique : false; - if (this.transaction.mode !== "versionchange") { + if (this._transaction.mode !== "versionchange") { throw new InvalidStateError(); } - if (this.indexNames.indexOf(indexName) >= 0) { + if (this._indexNames.indexOf(indexName) >= 0) { throw new ConstraintError(); } @@ -1563,7 +1614,7 @@ export class BridgeIDBObjectStore { btx, indexName, this._name, - keyPath, + normalizeKeyPath(keyPath), multiEntry, unique, ); @@ -1577,7 +1628,7 @@ export class BridgeIDBObjectStore { throw new TypeError(); } - if (this.transaction._state === "finished") { + if (this._transaction._state === "finished") { throw new InvalidStateError(); } @@ -1594,11 +1645,11 @@ export class BridgeIDBObjectStore { throw new TypeError(); } - if (this.transaction.mode !== "versionchange") { + if (this._transaction.mode !== "versionchange") { throw new InvalidStateError(); } - if (!this.transaction.db._runningVersionchangeTransaction) { + if (!this._transaction._db._runningVersionchangeTransaction) { throw new InvalidStateError(); } @@ -1638,7 +1689,7 @@ export class BridgeIDBObjectStore { return result.count; }; - return this.transaction._execRequestAsync({ operation, source: this }); + return this._transaction._execRequestAsync({ operation, source: this }); } public toString() { @@ -1647,10 +1698,20 @@ export class BridgeIDBObjectStore { } /** @public */ -export class BridgeIDBRequest extends FakeEventTarget { +export class BridgeIDBRequest extends FakeEventTarget implements IDBRequest { _result: any = null; _error: Error | null | undefined = null; - source: BridgeIDBCursor | BridgeIDBIndex | BridgeIDBObjectStore | null = null; + get source(): IDBObjectStore | IDBIndex | IDBCursor { + if (this._source) { + return this._source; + } + throw Error("source is null"); + } + _source: + | BridgeIDBCursor + | BridgeIDBIndex + | BridgeIDBObjectStore + | null = null; transaction: BridgeIDBTransaction | null = null; readyState: "done" | "pending" = "pending"; onsuccess: EventListener | null = null; @@ -1708,14 +1769,16 @@ export class BridgeIDBRequest extends FakeEventTarget { } /** @public */ -export class BridgeIDBOpenDBRequest extends BridgeIDBRequest { +export class BridgeIDBOpenDBRequest + extends BridgeIDBRequest + implements IDBOpenDBRequest { public onupgradeneeded: EventListener | null = null; public onblocked: EventListener | null = null; constructor() { super(); // https://www.w3.org/TR/IndexedDB/#open-requests - this.source = null; + this._source = null; } public toString() { @@ -1725,17 +1788,32 @@ export class BridgeIDBOpenDBRequest extends BridgeIDBRequest { // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#transaction /** @public */ -export class BridgeIDBTransaction extends FakeEventTarget { +export class BridgeIDBTransaction + extends FakeEventTarget + implements IDBTransaction { public _state: "active" | "inactive" | "committing" | "finished" = "active"; public _started = false; public _objectStoresCache: Map<string, BridgeIDBObjectStore> = new Map(); public _backendTransaction?: DatabaseTransaction; - public objectStoreNames: FakeDOMStringList; + public _objectStoreNames: FakeDOMStringList; + get objectStoreNames(): DOMStringList { + return this._objectStoreNames as DOMStringList; + } public mode: IDBTransactionMode; - public db: BridgeIDBDatabase; - public error: Error | null = null; + public _db: BridgeIDBDatabase; + + get db(): IDBDatabase { + return this.db; + } + + public _error: Error | null = null; + + get error(): DOMException { + return this._error as DOMException; + } + public onabort: EventListener | null = null; public oncomplete: EventListener | null = null; public onerror: EventListener | null = null; @@ -1750,7 +1828,7 @@ export class BridgeIDBTransaction extends FakeEventTarget { }> = []; get _backend(): Backend { - return this.db._backend; + return this._db._backend; } constructor( @@ -1768,10 +1846,10 @@ export class BridgeIDBTransaction extends FakeEventTarget { this._scope = new Set(storeNames); this._backendTransaction = backendTransaction; this.mode = mode; - this.db = db; - this.objectStoreNames = fakeDOMStringList(Array.from(this._scope).sort()); + this._db = db; + this._objectStoreNames = fakeDOMStringList(Array.from(this._scope).sort()); - this.db._transactions.push(this); + this._db._transactions.push(this); } // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-aborting-a-transaction @@ -1781,14 +1859,14 @@ export class BridgeIDBTransaction extends FakeEventTarget { if (errName !== null) { const e = new Error(); e.name = errName; - this.error = e; + this._error = e; } // Should this directly remove from _requests? for (const { request } of this._requests) { if (request.readyState !== "done") { request.readyState = "done"; // This will cancel execution of this request's operation - if (request.source) { + if (request._source) { request.result = undefined; request.error = new AbortError(); @@ -1796,7 +1874,7 @@ export class BridgeIDBTransaction extends FakeEventTarget { bubbles: true, cancelable: true, }); - event.eventPath = [this.db, this]; + event.eventPath = [this._db, this]; request.dispatchEvent(event); } } @@ -1813,7 +1891,7 @@ export class BridgeIDBTransaction extends FakeEventTarget { bubbles: true, cancelable: false, }); - event.eventPath = [this.db]; + event.eventPath = [this._db]; this.dispatchEvent(event); }); } @@ -1828,7 +1906,7 @@ export class BridgeIDBTransaction extends FakeEventTarget { } // http://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstore - public objectStore(name: string) { + public objectStore(name: string): BridgeIDBObjectStore { if (this._state !== "active") { throw new InvalidStateError(); } @@ -1858,7 +1936,7 @@ export class BridgeIDBTransaction extends FakeEventTarget { request = new BridgeIDBRequest(); } else { request = new BridgeIDBRequest(); - request.source = source; + request._source = source; request.transaction = (source as any).transaction; } } @@ -1884,7 +1962,7 @@ export class BridgeIDBTransaction extends FakeEventTarget { if (!this._backendTransaction) { this._backendTransaction = await this._backend.beginTransaction( - this.db._backendConnection, + this._db._backendConnection, Array.from(this._scope), this.mode, ); @@ -1905,7 +1983,7 @@ export class BridgeIDBTransaction extends FakeEventTarget { } if (request && operation) { - if (!request.source) { + if (!request._source) { // Special requests like indexes that just need to run some code, with error handling already built into // operation await operation(); @@ -1933,7 +2011,7 @@ export class BridgeIDBTransaction extends FakeEventTarget { }); try { - event.eventPath = [request, this, this.db]; + event.eventPath = [request, this, this._db]; request.dispatchEvent(event); } catch (err) { if (this._state !== "committing") { @@ -1959,7 +2037,7 @@ export class BridgeIDBTransaction extends FakeEventTarget { }); try { - event.eventPath = [this.db, this]; + event.eventPath = [this._db, this]; request.dispatchEvent(event); } catch (err) { if (this._state !== "committing") { @@ -1994,20 +2072,20 @@ export class BridgeIDBTransaction extends FakeEventTarget { this._state = "finished"; - if (!this.error) { + if (!this._error) { if (BridgeIDBFactory.enableTracing) { console.log("dispatching 'complete' event on transaction"); } const event = new FakeEvent("complete"); - event.eventPath = [this, this.db]; + event.eventPath = [this, this._db]; this.dispatchEvent(event); } - const idx = this.db._transactions.indexOf(this); + const idx = this._db._transactions.indexOf(this); if (idx < 0) { throw Error("invariant failed"); } - this.db._transactions.splice(idx, 1); + this._db._transactions.splice(idx, 1); this._resolveWait(); } diff --git a/packages/idb-bridge/src/idb-wpt-ported/README b/packages/idb-bridge/src/idb-wpt-ported/README new file mode 100644 index 000000000..e0b665aab --- /dev/null +++ b/packages/idb-bridge/src/idb-wpt-ported/README @@ -0,0 +1,3 @@ +This directory contains test cases from the W3C Web Platform Tests suite for IndexedDB. + +The original code for these tests can be found here: https://github.com/web-platform-tests/wpt/tree/master/IndexedDB
\ No newline at end of file diff --git a/packages/idb-bridge/src/idb-wpt-ported/keypath.test.ts b/packages/idb-bridge/src/idb-wpt-ported/keypath.test.ts new file mode 100644 index 000000000..61e416a53 --- /dev/null +++ b/packages/idb-bridge/src/idb-wpt-ported/keypath.test.ts @@ -0,0 +1,191 @@ +import test from "ava"; +import { assert_key_equals, createdb } from "./wptsupport"; + +test("WPT test keypath.htm", async (t) => { + function keypath( + keypath: any, + objects: any[], + expected_keys: any[], + desc?: string, + ) { + return new Promise<void>((resolve, reject) => { + console.log("key path", keypath); + console.log("checking", desc); + let db: any; + const store_name = "store-" + Date.now() + Math.random(); + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e) { + db = (e.target as any).result; + var objStore = db.createObjectStore(store_name, { keyPath: keypath }); + + for (var i = 0; i < objects.length; i++) objStore.add(objects[i]); + }; + + open_rq.onsuccess = function (e) { + var actual_keys: any[] = [], + rq = db.transaction(store_name).objectStore(store_name).openCursor(); + + rq.onsuccess = (e: any) => { + var cursor = e.target.result; + + if (cursor) { + actual_keys.push(cursor.key.valueOf()); + cursor.continue(); + } else { + assert_key_equals(actual_keys, expected_keys, "keyorder array"); + resolve(); + } + }; + }; + }); + } + + await keypath("my.key", [{ my: { key: 10 } }], [10]); + + await keypath("my.køi", [{ my: { køi: 5 } }], [5]); + + await keypath("my.key_ya", [{ my: { key_ya: 10 } }], [10]); + + await keypath("public.key$ya", [{ public: { key$ya: 10 } }], [10]); + + await keypath("true.$", [{ true: { $: 10 } }], [10]); + + await keypath("my._", [{ my: { _: 10 } }], [10]); + + await keypath("delete.a7", [{ delete: { a7: 10 } }], [10]); + + await keypath( + "p.p.p.p.p.p.p.p.p.p.p.p.p.p", + [ + { + p: { + p: { + p: { + p: { + p: { + p: { p: { p: { p: { p: { p: { p: { p: { p: 10 } } } } } } } }, + }, + }, + }, + }, + }, + }, + ], + [10], + ); + + await keypath( + "str.length", + [{ str: "pony" }, { str: "my" }, { str: "little" }, { str: "" }], + [0, 2, 4, 6], + ); + + await keypath( + "arr.length", + [ + { arr: [0, 0, 0, 0] }, + { arr: [{}, 0, "hei", "length", Infinity, []] }, + { arr: [10, 10] }, + { arr: [] }, + ], + [0, 2, 4, 6], + ); + + await keypath("length", [[10, 10], "123", { length: 20 }], [2, 3, 20]); + + await keypath( + "", + [["bags"], "bean", 10], + [10, "bean", ["bags"]], + "'' uses value as key", + ); + + await keypath( + [""], + [["bags"], "bean", 10], + [[10], ["bean"], [["bags"]]], + "[''] uses value as [key]", + ); + + await keypath( + ["x", "y"], + [ + { x: 10, y: 20 }, + { y: 1.337, x: 100 }, + ], + [ + [10, 20], + [100, 1.337], + ], + "['x', 'y']", + ); + + await keypath( + [["x"], ["y"]], + [ + { x: 10, y: 20 }, + { y: 1.337, x: 100 }, + ], + [ + [10, 20], + [100, 1.337], + ], + "[['x'], 'y'] (stringifies)", + ); + + await keypath( + [ + "x", + { + toString: function () { + return "y"; + }, + }, + ], + [ + { x: 10, y: 20 }, + { y: 1.337, x: 100 }, + ], + [ + [10, 20], + [100, 1.337], + ], + "['x', {toString->'y'}] (stringifies)", + ); + + await keypath( + ["name", "type"], + [ + { name: "orange", type: "fruit" }, + { name: "orange", type: ["telecom", "french"] }, + ], + [ + ["orange", "fruit"], + ["orange", ["telecom", "french"]], + ], + ); + + await keypath( + ["name", "type.name"], + [ + { name: "orange", type: { name: "fruit" } }, + { name: "orange", type: { name: "telecom" } }, + ], + [ + ["orange", "fruit"], + ["orange", "telecom"], + ], + ); + + const loop_array: any[] = []; + loop_array.push(loop_array); + await keypath( + loop_array, + ["a", 1, ["k"]], + [[1], ["a"], [["k"]]], + "array loop -> stringify becomes ['']", + ); + + t.pass(); +}); diff --git a/packages/idb-bridge/src/idb-wpt-ported/value.test.ts b/packages/idb-bridge/src/idb-wpt-ported/value.test.ts new file mode 100644 index 000000000..c4a8315c6 --- /dev/null +++ b/packages/idb-bridge/src/idb-wpt-ported/value.test.ts @@ -0,0 +1,46 @@ +import test from "ava"; +import { IDBVersionChangeEvent } from "../idbtypes"; +import { createdb } from "./wptsupport"; + +test.cb("WPT test value.htm, array", (t) => { + const value = new Array(); + const _instanceof = Array; + + t.plan(1); + + createdb(t).onupgradeneeded = function (e: IDBVersionChangeEvent) { + (e.target as any).result.createObjectStore("store").add(value, 1); + (e.target as any).onsuccess = (e: any) => { + console.log("in first onsuccess"); + e.target.result + .transaction("store") + .objectStore("store") + .get(1).onsuccess = (e: any) => { + t.assert(e.target.result instanceof _instanceof, "instanceof"); + t.end(); + }; + }; + }; +}); + +test.cb("WPT test value.htm, date", (t) => { + const value = new Date(); + const _instanceof = Date; + + t.plan(1); + + createdb(t).onupgradeneeded = function (e: IDBVersionChangeEvent) { + (e.target as any).result.createObjectStore("store").add(value, 1); + (e.target as any).onsuccess = (e: any) => { + console.log("in first onsuccess"); + e.target.result + .transaction("store") + .objectStore("store") + .get(1).onsuccess = (e: any) => { + t.assert(e.target.result instanceof _instanceof, "instanceof"); + t.end(); + }; + }; + }; + }); +
\ No newline at end of file diff --git a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts new file mode 100644 index 000000000..10c11b7a6 --- /dev/null +++ b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts @@ -0,0 +1,30 @@ +import { ExecutionContext } from "ava"; +import { BridgeIDBFactory } from ".."; +import { IDBOpenDBRequest } from "../idbtypes"; +import MemoryBackend from "../MemoryBackend"; +import compareKeys from "../util/cmp"; + +BridgeIDBFactory.enableTracing = true; +const idbFactory = new BridgeIDBFactory(new MemoryBackend()); + +const self = { + indexedDB: idbFactory, +}; + +export function createdb( + t: ExecutionContext<unknown>, + dbname?: string, + version?: number, +): IDBOpenDBRequest { + var rq_open: IDBOpenDBRequest; + dbname = dbname ? dbname : "testdb-" + new Date().getTime() + Math.random(); + if (version) rq_open = self.indexedDB.open(dbname, version); + else rq_open = self.indexedDB.open(dbname); + return rq_open; +} + +export function assert_key_equals(actual: any, expected: any, description?: string) { + if (0 != compareKeys(actual, expected)) { + throw Error("expected keys to be the same"); + } +} diff --git a/packages/idb-bridge/src/util/injectKey.ts b/packages/idb-bridge/src/util/injectKey.ts index 678f42d28..63c8deda4 100644 --- a/packages/idb-bridge/src/util/injectKey.ts +++ b/packages/idb-bridge/src/util/injectKey.ts @@ -16,10 +16,10 @@ */ import { IDBKeyPath, IDBValidKey } from "../idbtypes"; -import structuredClone from "./structuredClone"; +import { structuredClone } from "./structuredClone"; export function injectKey( - keyPath: IDBKeyPath, + keyPath: IDBKeyPath | IDBKeyPath[], value: any, key: IDBValidKey, ): any { diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.ts index b535bced5..2281e983d 100644 --- a/packages/idb-bridge/src/util/makeStoreKeyValue.ts +++ b/packages/idb-bridge/src/util/makeStoreKeyValue.ts @@ -17,7 +17,7 @@ import extractKey from "./extractKey"; import { DataError } from "./errors"; import valueToKey from "./valueToKey"; -import structuredClone from "./structuredClone"; +import { structuredClone } from "./structuredClone"; import injectKey from "./injectKey"; import { IDBKeyPath, IDBValidKey } from "../idbtypes"; @@ -32,7 +32,7 @@ export function makeStoreKeyValue( key: IDBValidKey | undefined, currentKeyGenerator: number, autoIncrement: boolean, - keyPath: IDBKeyPath | null, + keyPath: IDBKeyPath | IDBKeyPath[] | null, ): StoreKeyResult { const haveKey = key !== null && key !== undefined; const haveKeyPath = keyPath !== null && keyPath !== undefined; @@ -63,7 +63,11 @@ export function makeStoreKeyValue( }; } else { // (yes, no, no) - throw new DataError(); + return { + key: key!, + value: value, + updatedKeyGenerator: currentKeyGenerator, + }; } } } else { diff --git a/packages/idb-bridge/src/util/normalizeKeyPath.ts b/packages/idb-bridge/src/util/normalizeKeyPath.ts new file mode 100644 index 000000000..4e194b2d1 --- /dev/null +++ b/packages/idb-bridge/src/util/normalizeKeyPath.ts @@ -0,0 +1,41 @@ +/* + Copyright 2017 Jeremy Scheff + 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. +*/ + +import { IDBKeyPath } from "../idbtypes"; + +export function normalizeKeyPath( + keyPath: IDBKeyPath | IDBKeyPath[], +): string | string[] { + if (Array.isArray(keyPath)) { + const path: string[] = []; + for (let item of keyPath) { + // This doesn't make sense to me based on the spec, but it is needed to pass the W3C KeyPath tests (see same + // comment in validateKeyPath) + if ( + item !== undefined && + item !== null && + typeof item !== "string" && + (item as any).toString + ) { + item = (item as any).toString(); + } + path.push(item); + } + return path; + } + return keyPath; +} diff --git a/packages/idb-bridge/src/util/structuredClone.ts b/packages/idb-bridge/src/util/structuredClone.ts index c49d0377f..9bbeb7151 100644 --- a/packages/idb-bridge/src/util/structuredClone.ts +++ b/packages/idb-bridge/src/util/structuredClone.ts @@ -14,17 +14,24 @@ permissions and limitations under the License. */ -function structuredCloneImpl(val: any, visited: WeakMap<any, boolean>): any { - // FIXME: replace with real implementation! - return JSON.parse(JSON.stringify(val)); +// @ts-ignore +import Typeson from "typeson"; +// @ts-ignore +import structuredCloningThrowing from "typeson-registry/dist/presets/structured-cloning-throwing"; + +const TSON = new Typeson().register(structuredCloningThrowing); + +export function structuredEncapsulate(val: any): any { + return TSON.encapsulate(val); +} + +export function structuredRevive(val: any): any { + return TSON.revive(val); } /** * Structured clone for IndexedDB. */ export function structuredClone(val: any): any { - const visited: WeakMap<any, boolean> = new WeakMap<any, boolean>(); - return structuredCloneImpl(val, visited); + return structuredRevive(structuredEncapsulate(val)); } - -export default structuredClone; diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index d0b8c2ef6..71c809ec7 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -106,7 +106,7 @@ function transactionToPromise(tx: IDBTransaction): Promise<void> { }; tx.onerror = () => { console.error("Transaction failed:", stack); - reject(tx.error); + reject(tx._error); }; }); } @@ -394,8 +394,8 @@ function runWithTransaction<T, StoreTypes extends Store<string, {}>>( logger.error(`${stack}`); }; tx.onabort = () => { - if (tx.error) { - logger.error("Transaction aborted with error:", tx.error); + if (tx._error) { + logger.error("Transaction aborted with error:", tx._error); } else { logger.error("Trasaction aborted (no error)"); } |