diff options
Diffstat (limited to 'packages/idb-bridge/src')
-rw-r--r-- | packages/idb-bridge/src/BridgeIDBCursor.ts | 4 | ||||
-rw-r--r-- | packages/idb-bridge/src/BridgeIDBDatabase.ts | 5 | ||||
-rw-r--r-- | packages/idb-bridge/src/BridgeIDBFactory.ts | 11 | ||||
-rw-r--r-- | packages/idb-bridge/src/BridgeIDBObjectStore.ts | 4 | ||||
-rw-r--r-- | packages/idb-bridge/src/BridgeIDBTransaction.ts | 71 | ||||
-rw-r--r-- | packages/idb-bridge/src/MemoryBackend.test.ts | 123 | ||||
-rw-r--r-- | packages/idb-bridge/src/MemoryBackend.ts | 497 | ||||
-rw-r--r-- | packages/idb-bridge/src/backend-interface.ts | 31 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/FakeEventTarget.ts | 262 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/getIndexKeys.test.ts | 24 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/getIndexKeys.ts | 28 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/makeStoreKeyValue.test.ts | 42 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/makeStoreKeyValue.ts | 24 |
13 files changed, 921 insertions, 205 deletions
diff --git a/packages/idb-bridge/src/BridgeIDBCursor.ts b/packages/idb-bridge/src/BridgeIDBCursor.ts index 0120bb7d5..8321e2a1d 100644 --- a/packages/idb-bridge/src/BridgeIDBCursor.ts +++ b/packages/idb-bridge/src/BridgeIDBCursor.ts @@ -18,7 +18,7 @@ import BridgeIDBKeyRange from "./BridgeIDBKeyRange"; import BridgeIDBObjectStore from "./BridgeIDBObjectStore"; import BridgeIDBRequest from "./BridgeIDBRequest"; -import cmp from "./util/cmp"; +import compareKeys from "./util/cmp"; import { DataError, InvalidAccessError, @@ -233,7 +233,7 @@ class BridgeIDBCursor { if (key !== undefined) { key = valueToKey(key); - const cmpResult = cmp(key, this._position); + const cmpResult = compareKeys(key, this._position); if ( (cmpResult <= 0 && diff --git a/packages/idb-bridge/src/BridgeIDBDatabase.ts b/packages/idb-bridge/src/BridgeIDBDatabase.ts index cff2fd6e3..bc2e8acca 100644 --- a/packages/idb-bridge/src/BridgeIDBDatabase.ts +++ b/packages/idb-bridge/src/BridgeIDBDatabase.ts @@ -144,7 +144,7 @@ class BridgeIDBDatabase extends FakeEventTarget { validateKeyPath(keyPath); } - if (!Object.keys(this._schema.objectStores).includes(name)) { + if (Object.keys(this._schema.objectStores).includes(name)) { throw new ConstraintError(); } @@ -156,7 +156,7 @@ class BridgeIDBDatabase extends FakeEventTarget { this._schema = this._backend.getSchema(this._backendConnection); - return transaction.objectStore("name"); + return transaction.objectStore(name); } public deleteObjectStore(name: string): void { @@ -214,6 +214,7 @@ class BridgeIDBDatabase extends FakeEventTarget { const tx = new BridgeIDBTransaction(storeNames, mode, this, backendTransaction); this._transactions.push(tx); + queueTask(() => tx._start()); return tx; } diff --git a/packages/idb-bridge/src/BridgeIDBFactory.ts b/packages/idb-bridge/src/BridgeIDBFactory.ts index c2747238e..ad02be461 100644 --- a/packages/idb-bridge/src/BridgeIDBFactory.ts +++ b/packages/idb-bridge/src/BridgeIDBFactory.ts @@ -31,6 +31,7 @@ class BridgeIDBFactory { public cmp = compareKeys; private backend: Backend; private connections: BridgeIDBDatabase[] = []; + static enableTracing: boolean = true; public constructor(backend: Backend) { this.backend = backend; @@ -165,7 +166,17 @@ class BridgeIDBFactory { await transaction._waitDone(); + // We don't explicitly exit the versionchange transaction, + // since this is already done by the BridgeIDBTransaction. db._runningVersionchangeTransaction = false; + + const event2 = new FakeEvent("success", { + bubbles: false, + cancelable: false, + }); + event2.eventPath = [request]; + + request.dispatchEvent(event2); } this.connections.push(db); diff --git a/packages/idb-bridge/src/BridgeIDBObjectStore.ts b/packages/idb-bridge/src/BridgeIDBObjectStore.ts index 197f06d86..eca4c1981 100644 --- a/packages/idb-bridge/src/BridgeIDBObjectStore.ts +++ b/packages/idb-bridge/src/BridgeIDBObjectStore.ts @@ -47,6 +47,7 @@ import { RecordGetRequest, ResultLevel, } from "./backend-interface"; +import BridgeIDBFactory from "./BridgeIDBFactory"; // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#object-store @@ -124,6 +125,9 @@ class BridgeIDBObjectStore { } public _store(value: Value, key: Key | undefined, overwrite: boolean) { + if (BridgeIDBFactory.enableTracing) { + console.log(`TRACE: IDBObjectStore._store`); + } if (this.transaction.mode === "readonly") { throw new ReadOnlyError(); } diff --git a/packages/idb-bridge/src/BridgeIDBTransaction.ts b/packages/idb-bridge/src/BridgeIDBTransaction.ts index a7057e297..09f324dfa 100644 --- a/packages/idb-bridge/src/BridgeIDBTransaction.ts +++ b/packages/idb-bridge/src/BridgeIDBTransaction.ts @@ -20,6 +20,7 @@ import queueTask from "./util/queueTask"; import openPromise from "./util/openPromise"; import { DatabaseTransaction, Backend } from "./backend-interface"; import { array } from "prop-types"; +import BridgeIDBFactory from "./BridgeIDBFactory"; // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#transaction class BridgeIDBTransaction extends FakeEventTarget { @@ -113,7 +114,6 @@ class BridgeIDBTransaction extends FakeEventTarget { event.eventPath = [this.db]; this.dispatchEvent(event); }); - } public abort() { @@ -169,9 +169,17 @@ class BridgeIDBTransaction extends FakeEventTarget { return request; } + /** + * Actually execute the scheduled work for this transaction. + */ public async _start() { + if (BridgeIDBFactory.enableTracing) { + console.log(`TRACE: IDBTransaction._start, ${this._requests.length} queued`); + } this._started = true; + console.log("beginning transaction"); + if (!this._backendTransaction) { this._backendTransaction = await this._backend.beginTransaction( this.db._backendConnection, @@ -180,6 +188,8 @@ class BridgeIDBTransaction extends FakeEventTarget { ); } + console.log("beginTransaction completed"); + // Remove from request queue - cursor ones will be added back if necessary by cursor.continue and such let operation; let request; @@ -198,9 +208,10 @@ class BridgeIDBTransaction extends FakeEventTarget { if (!request.source) { // Special requests like indexes that just need to run some code, with error handling already built into // operation + console.log("running operation without source"); await operation(); } else { - let defaultAction; + console.log("running operation with source"); let event; try { const result = await operation(); @@ -216,7 +227,20 @@ class BridgeIDBTransaction extends FakeEventTarget { bubbles: false, cancelable: false, }); + + try { + event.eventPath = [request, this, this.db]; + request.dispatchEvent(event); + } catch (err) { + if (this._state !== "committing") { + this._abort("AbortError"); + } + throw err; + } } catch (err) { + if (BridgeIDBFactory.enableTracing) { + console.log("TRACING: error during operation: ", err); + } request.readyState = "done"; request.result = undefined; request.error = err; @@ -230,23 +254,17 @@ class BridgeIDBTransaction extends FakeEventTarget { cancelable: true, }); - defaultAction = this._abort.bind(this, err.name); - } - - try { - event.eventPath = [this.db, this]; - request.dispatchEvent(event); - } catch (err) { - if (this._state !== "committing") { - this._abort("AbortError"); + try { + event.eventPath = [this.db, this]; + request.dispatchEvent(event); + } catch (err) { + if (this._state !== "committing") { + this._abort("AbortError"); + } + throw err; } - throw err; - } - - // Default action of event - if (!event.canceled) { - if (defaultAction) { - defaultAction(); + if (!event.canceled) { + this._abort(err.name); } } } @@ -261,13 +279,23 @@ class BridgeIDBTransaction extends FakeEventTarget { return; } - // Check if transaction complete event needs to be fired - if (this._state !== "finished") { - // Either aborted or committed already + if (this._state !== "finished" && this._state !== "committing") { + if (BridgeIDBFactory.enableTracing) { + console.log("finishing transaction"); + } + + this._state = "committing"; + + await this._backend.commit(this._backendTransaction); + this._state = "finished"; if (!this.error) { + if (BridgeIDBFactory.enableTracing) { + console.log("dispatching 'complete' event"); + } const event = new FakeEvent("complete"); + event.eventPath = [this, this.db]; this.dispatchEvent(event); } @@ -287,6 +315,7 @@ class BridgeIDBTransaction extends FakeEventTarget { } this._state = "committing"; + // We now just wait for auto-commit ... } public toString() { diff --git a/packages/idb-bridge/src/MemoryBackend.test.ts b/packages/idb-bridge/src/MemoryBackend.test.ts index 3d2d0fbc9..213bff750 100644 --- a/packages/idb-bridge/src/MemoryBackend.test.ts +++ b/packages/idb-bridge/src/MemoryBackend.test.ts @@ -1,31 +1,126 @@ -import test from 'ava'; -import MemoryBackend from './MemoryBackend'; -import BridgeIDBFactory from './BridgeIDBFactory'; +import test from "ava"; +import MemoryBackend from "./MemoryBackend"; +import BridgeIDBFactory from "./BridgeIDBFactory"; +import BridgeIDBRequest from "./BridgeIDBRequest"; +import BridgeIDBDatabase from "./BridgeIDBDatabase"; +import BridgeIDBTransaction from "./BridgeIDBTransaction"; -test.cb("basics", (t) => { +function promiseFromRequest(request: BridgeIDBRequest): Promise<any> { + return new Promise((resolve, reject) => { + request.onsuccess = () => { + resolve(request.result); + }; + request.onerror = () => { + reject(request.error); + }; + }); +} + +function promiseFromTransaction(transaction: BridgeIDBTransaction): Promise<any> { + return new Promise((resolve, reject) => { + console.log("attaching event handlers"); + transaction.oncomplete = () => { + console.log("oncomplete was called from promise") + resolve(); + }; + transaction.onerror = () => { + reject(); + }; + }); +} + +test("Spec: Example 1 Part 1", async t => { const backend = new MemoryBackend(); const idb = new BridgeIDBFactory(backend); const request = idb.open("library"); request.onupgradeneeded = () => { const db = request.result; - const store = db.createObjectStore("books", {keyPath: "isbn"}); - const titleIndex = store.createIndex("by_title", "title", {unique: true}); + const store = db.createObjectStore("books", { keyPath: "isbn" }); + const titleIndex = store.createIndex("by_title", "title", { unique: true }); const authorIndex = store.createIndex("by_author", "author"); - + // Populate with initial data. - store.put({title: "Quarry Memories", author: "Fred", isbn: 123456}); - store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567}); - store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678}); + store.put({ title: "Quarry Memories", author: "Fred", isbn: 123456 }); + store.put({ title: "Water Buffaloes", author: "Fred", isbn: 234567 }); + store.put({ title: "Bedrock Nights", author: "Barney", isbn: 345678 }); + }; + + await promiseFromRequest(request); + t.pass(); +}); + + +test("Spec: Example 1 Part 2", async t => { + const backend = new MemoryBackend(); + const idb = new BridgeIDBFactory(backend); + + const request = idb.open("library"); + request.onupgradeneeded = () => { + const db = request.result; + const store = db.createObjectStore("books", { keyPath: "isbn" }); + const titleIndex = store.createIndex("by_title", "title", { unique: true }); + const authorIndex = store.createIndex("by_author", "author"); + }; + + const db: BridgeIDBDatabase = await promiseFromRequest(request); + + t.is(db.name, "library"); + + const tx = db.transaction("books", "readwrite"); + tx.oncomplete = () => { + console.log("oncomplete called") }; - request.onsuccess = () => { - t.end(); + const store = tx.objectStore("books"); + + store.put({title: "Quarry Memories", author: "Fred", isbn: 123456}); + store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567}); + store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678}); + + await promiseFromTransaction(tx); + + t.pass(); +}); + + +test("Spec: Example 1 Part 3", async t => { + const backend = new MemoryBackend(); + const idb = new BridgeIDBFactory(backend); + + const request = idb.open("library"); + request.onupgradeneeded = () => { + const db = request.result; + const store = db.createObjectStore("books", { keyPath: "isbn" }); + const titleIndex = store.createIndex("by_title", "title", { unique: true }); + const authorIndex = store.createIndex("by_author", "author"); }; - request.onerror = () => { - t.fail(); + const db: BridgeIDBDatabase = await promiseFromRequest(request); + + t.is(db.name, "library"); + + const tx = db.transaction("books", "readwrite"); + tx.oncomplete = () => { + console.log("oncomplete called") }; + const store = tx.objectStore("books"); + + store.put({title: "Quarry Memories", author: "Fred", isbn: 123456}); + store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567}); + store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678}); + + await promiseFromTransaction(tx); + + const tx2 = db.transaction("books", "readonly"); + const store2 = tx2.objectStore("books"); + var index2 = store2.index("by_title"); + const request2 = index2.get("Bedrock Nights"); + const result2: any = await promiseFromRequest(request2); + + t.is(result2.author, "Barney"); + + t.pass(); }); diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts index 2d4b8ab93..831974882 100644 --- a/packages/idb-bridge/src/MemoryBackend.ts +++ b/packages/idb-bridge/src/MemoryBackend.ts @@ -5,14 +5,26 @@ import { Schema, RecordStoreRequest, IndexProperties, + RecordGetRequest, + RecordGetResponse, + ResultLevel, } from "./backend-interface"; import structuredClone from "./util/structuredClone"; -import { InvalidStateError, InvalidAccessError } from "./util/errors"; +import { + InvalidStateError, + InvalidAccessError, + ConstraintError, +} from "./util/errors"; import BTree, { ISortedMap, ISortedMapF } from "./tree/b+tree"; import BridgeIDBFactory from "./BridgeIDBFactory"; import compareKeys from "./util/cmp"; import extractKey from "./util/extractKey"; import { Key, Value, KeyPath } from "./util/types"; +import { StoreKeyResult, makeStoreKeyValue } from "./util/makeStoreKeyValue"; +import getIndexKeys from "./util/getIndexKeys"; +import openPromise from "./util/openPromise"; +import BridgeIDBKeyRange from "./BridgeIDBKeyRange"; +import { resetWarningCache } from "prop-types"; enum TransactionLevel { Disconnected = 0, @@ -25,8 +37,8 @@ enum TransactionLevel { interface ObjectStore { originalName: string; modifiedName: string | undefined; - originalData: ISortedMapF; - modifiedData: ISortedMapF | undefined; + originalData: ISortedMapF<Key, ObjectStoreRecord>; + modifiedData: ISortedMapF<Key, ObjectStoreRecord> | undefined; deleted: boolean; originalKeyGenerator: number; modifiedKeyGenerator: number | undefined; @@ -35,8 +47,8 @@ interface ObjectStore { interface Index { originalName: string; modifiedName: string | undefined; - originalData: ISortedMapF; - modifiedData: ISortedMapF | undefined; + originalData: ISortedMapF<Key, IndexRecord>; + modifiedData: ISortedMapF<Key, IndexRecord> | undefined; deleted: boolean; } @@ -74,28 +86,77 @@ interface Connection { indexMap: { [currentName: string]: Index }; } -class AsyncCondition { - wait(): Promise<void> { - throw Error("not implemented"); - } +interface IndexRecord { + indexKey: Key; + primaryKeys: Key[]; +} - trigger(): void {} +interface ObjectStoreRecord { + primaryKey: Key; + value: Value; } +class AsyncCondition { + _waitPromise: Promise<void>; + _resolveWaitPromise: () => void; + constructor() { + const op = openPromise<void>(); + this._waitPromise = op.promise; + this._resolveWaitPromise = op.resolve; + } + wait(): Promise<void> { + return this._waitPromise; + } + trigger(): void { + this._resolveWaitPromise(); + const op = openPromise<void>(); + this._waitPromise = op.promise; + this._resolveWaitPromise = op.resolve; + } +} -function insertIntoIndex( - index: Index, - value: Value, - indexProperties: IndexProperties, +function nextStoreKey<T>( + forward: boolean, + data: ISortedMapF<Key, ObjectStoreRecord>, + k: Key | undefined, ) { - if (indexProperties.multiEntry) { + if (k === undefined || k === null) { + return undefined; + } + const res = forward ? data.nextHigherPair(k) : data.nextLowerPair(k); + if (!res) { + return undefined; + } + return res[1].primaryKey; +} - } else { - const key = extractKey(value, indexProperties.keyPath); + +function furthestKey(forward: boolean, key1: Key | undefined, key2: Key | undefined) { + if (key1 === undefined) { + return key2; + } + if (key2 === undefined) { + return key1; + } + const cmpResult = compareKeys(key1, key2); + if (cmpResult === 0) { + // Same result + return key1; + } + if (forward && cmpResult === 1) { + return key1; + } + if (forward && cmpResult === -1) { + return key2; + } + if (!forward && cmpResult === 1) { + return key2; + } + if (!forward && cmpResult === -1) { + return key1; } - throw Error("not implemented"); } /** @@ -129,7 +190,12 @@ export class MemoryBackend implements Backend { */ transactionDoneCond: AsyncCondition = new AsyncCondition(); + enableTracing: boolean = true; + async getDatabases(): Promise<{ name: string; version: number }[]> { + if (this.enableTracing) { + console.log("TRACING: getDatabase"); + } const dbList = []; for (const name in this.databases) { dbList.push({ @@ -141,6 +207,9 @@ export class MemoryBackend implements Backend { } async deleteDatabase(tx: DatabaseTransaction, name: string): Promise<void> { + if (this.enableTracing) { + console.log("TRACING: deleteDatabase"); + } const myConn = this.connectionsByTransaction[tx.transactionCookie]; if (!myConn) { throw Error("no connection associated with transaction"); @@ -162,6 +231,9 @@ export class MemoryBackend implements Backend { } async connectDatabase(name: string): Promise<DatabaseConnection> { + if (this.enableTracing) { + console.log(`TRACING: connectDatabase(${name})`); + } const connectionId = this.connectionIdCounter++; const connectionCookie = `connection-${connectionId}`; @@ -193,6 +265,16 @@ export class MemoryBackend implements Backend { database.txLevel = TransactionLevel.Connected; database.connectionCookie = connectionCookie; + const myConn: Connection = { + dbName: name, + deleted: false, + indexMap: Object.assign({}, database.committedIndexes), + objectStoreMap: Object.assign({}, database.committedObjectStores), + modifiedSchema: structuredClone(database.committedSchema), + }; + + this.connections[connectionCookie] = myConn; + return { connectionCookie }; } @@ -201,6 +283,9 @@ export class MemoryBackend implements Backend { objectStores: string[], mode: import("./util/types").TransactionMode, ): Promise<DatabaseTransaction> { + if (this.enableTracing) { + console.log(`TRACING: beginTransaction`); + } const transactionCookie = `tx-${this.transactionIdCounter++}`; const myConn = this.connections[conn.connectionCookie]; if (!myConn) { @@ -212,6 +297,9 @@ export class MemoryBackend implements Backend { } while (myDb.txLevel !== TransactionLevel.Connected) { + if (this.enableTracing) { + console.log(`TRACING: beginTransaction -- waiting for others to close`); + } await this.transactionDoneCond.wait(); } @@ -232,6 +320,9 @@ export class MemoryBackend implements Backend { conn: DatabaseConnection, newVersion: number, ): Promise<DatabaseTransaction> { + if (this.enableTracing) { + console.log(`TRACING: enterVersionChange`); + } const transactionCookie = `tx-vc-${this.transactionIdCounter++}`; const myConn = this.connections[conn.connectionCookie]; if (!myConn) { @@ -254,6 +345,9 @@ export class MemoryBackend implements Backend { } async close(conn: DatabaseConnection): Promise<void> { + if (this.enableTracing) { + console.log(`TRACING: close`); + } const myConn = this.connections[conn.connectionCookie]; if (!myConn) { throw Error("connection not found - already closed?"); @@ -266,9 +360,13 @@ export class MemoryBackend implements Backend { myDb.txLevel = TransactionLevel.Disconnected; } delete this.connections[conn.connectionCookie]; + this.disconnectCond.trigger(); } getSchema(dbConn: DatabaseConnection): Schema { + if (this.enableTracing) { + console.log(`TRACING: getSchema`); + } const myConn = this.connections[dbConn.connectionCookie]; if (!myConn) { throw Error("unknown connection"); @@ -288,7 +386,10 @@ export class MemoryBackend implements Backend { oldName: string, newName: string, ): void { - const myConn = this.connections[btx.transactionCookie]; + if (this.enableTracing) { + console.log(`TRACING: renameIndex(?, ${oldName}, ${newName})`); + } + const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); } @@ -331,6 +432,9 @@ export class MemoryBackend implements Backend { } deleteIndex(btx: DatabaseTransaction, indexName: string): void { + if (this.enableTracing) { + console.log(`TRACING: deleteIndex(${indexName})`); + } const myConn = this.connections[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); @@ -365,6 +469,9 @@ export class MemoryBackend implements Backend { } deleteObjectStore(btx: DatabaseTransaction, name: string): void { + if (this.enableTracing) { + console.log(`TRACING: deleteObjectStore(${name})`); + } const myConn = this.connections[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); @@ -403,6 +510,10 @@ export class MemoryBackend implements Backend { oldName: string, newName: string, ): void { + if (this.enableTracing) { + console.log(`TRACING: renameObjectStore(?, ${oldName}, ${newName})`); + } + const myConn = this.connections[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); @@ -441,7 +552,12 @@ export class MemoryBackend implements Backend { keyPath: string | string[] | null, autoIncrement: boolean, ): void { - const myConn = this.connections[btx.transactionCookie]; + if (this.enableTracing) { + console.log( + `TRACING: createObjectStore(${btx.transactionCookie}, ${name})`, + ); + } + const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); } @@ -482,7 +598,10 @@ export class MemoryBackend implements Backend { multiEntry: boolean, unique: boolean, ): void { - const myConn = this.connections[btx.transactionCookie]; + if (this.enableTracing) { + console.log(`TRACING: createIndex(${indexName})`); + } + const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); } @@ -526,7 +645,10 @@ export class MemoryBackend implements Backend { objectStoreName: string, range: import("./BridgeIDBKeyRange").default, ): Promise<void> { - const myConn = this.connections[btx.transactionCookie]; + if (this.enableTracing) { + console.log(`TRACING: deleteRecord`); + } + const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); } @@ -537,13 +659,17 @@ export class MemoryBackend implements Backend { if (db.txLevel < TransactionLevel.Write) { throw Error("only allowed in write transaction"); } + throw Error("not implemented"); } async getRecords( btx: DatabaseTransaction, - req: import("./backend-interface").RecordGetRequest, - ): Promise<import("./backend-interface").RecordGetResponse> { - const myConn = this.connections[btx.transactionCookie]; + req: RecordGetRequest, + ): Promise<RecordGetResponse> { + if (this.enableTracing) { + console.log(`TRACING: getRecords`); + } + const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); } @@ -551,17 +677,242 @@ export class MemoryBackend implements Backend { if (!db) { throw Error("db not found"); } - if (db.txLevel < TransactionLevel.Write) { + if (db.txLevel < TransactionLevel.Read) { throw Error("only allowed while running a transaction"); } - throw Error("not implemented"); + const objectStore = myConn.objectStoreMap[req.objectStoreName]; + if (!objectStore) { + throw Error("object store not found"); + } + + let range; + if (req.range == null || req.range === undefined) { + range = new BridgeIDBKeyRange(null, null, true, true); + } else { + range = req.range; + } + + let numResults = 0; + let indexKeys: Key[] = []; + let primaryKeys = []; + let values = []; + + const forward: boolean = + req.direction === "next" || req.direction === "nextunique"; + const unique: boolean = + req.direction === "prevunique" || req.direction === "nextunique"; + + const storeData = objectStore.modifiedData || objectStore.originalData; + + const haveIndex = req.indexName !== undefined; + + if (haveIndex) { + const index = myConn.indexMap[req.indexName!]; + const indexData = index.modifiedData || index.originalData; + let indexPos = req.lastIndexPosition; + + if (indexPos === undefined) { + // First time we iterate! So start at the beginning (lower/upper) + // of our allowed range. + indexPos = forward ? range.lower : range.upper; + } + + let primaryPos = req.lastObjectStorePosition; + + // We might have to advance the index key further! + if (req.advanceIndexKey !== undefined) { + const compareResult = compareKeys(req.advanceIndexKey, indexPos); + if ((forward && compareResult > 0) || (!forward && compareResult > 0)) { + indexPos = req.advanceIndexKey; + } else if (compareResult == 0 && req.advancePrimaryKey !== undefined) { + // index keys are the same, so advance the primary key + if (primaryPos === undefined) { + primaryPos = req.advancePrimaryKey; + } else { + const primCompareResult = compareKeys( + req.advancePrimaryKey, + primaryPos, + ); + if ( + (forward && primCompareResult > 0) || + (!forward && primCompareResult < 0) + ) { + primaryPos = req.advancePrimaryKey; + } + } + } + } + + let indexEntry; + indexEntry = indexData.get(indexPos); + if (!indexEntry) { + const res = indexData.nextHigherPair(indexPos); + if (res) { + indexEntry = res[1]; + } + } + + if (!indexEntry) { + // We're out of luck, no more data! + return { count: 0, primaryKeys: [], indexKeys: [], values: [] }; + } + + let primkeySubPos = 0; + + // Sort out the case where the index key is the same, so we have + // to get the prev/next primary key + if ( + req.lastIndexPosition !== undefined && + compareKeys(indexEntry.indexKey, req.lastIndexPosition) === 0 + ) { + let pos = forward ? 0 : indexEntry.primaryKeys.length - 1; + // Advance past the lastObjectStorePosition + while (pos >= 0 && pos < indexEntry.primaryKeys.length) { + const cmpResult = compareKeys( + req.lastObjectStorePosition, + indexEntry.primaryKeys[pos], + ); + if ((forward && cmpResult < 0) || (!forward && cmpResult > 0)) { + break; + } + pos += forward ? 1 : -1; + } + // Make sure we're at least at advancedPrimaryPos + while ( + primaryPos !== undefined && + pos >= 0 && + pos < indexEntry.primaryKeys.length + ) { + const cmpResult = compareKeys( + primaryPos, + indexEntry.primaryKeys[pos], + ); + if ((forward && cmpResult <= 0) || (!forward && cmpResult >= 0)) { + break; + } + pos += forward ? 1 : -1; + } + primkeySubPos = pos; + } else { + primkeySubPos = forward ? 0 : indexEntry.primaryKeys.length - 1; + } + + // FIXME: filter out duplicates + + while (1) { + if (req.limit != 0 && numResults == req.limit) { + break; + } + if (indexPos === undefined) { + break; + } + if (!range.includes(indexPos)) { + break; + } + if ( + primkeySubPos < 0 || + primkeySubPos >= indexEntry.primaryKeys.length + ) { + primkeySubPos = forward ? 0 : indexEntry.primaryKeys.length - 1; + const res = indexData.nextHigherPair(indexPos); + if (res) { + indexPos = res[1].indexKey; + } else { + break; + } + } + primaryKeys.push(indexEntry.primaryKeys[primkeySubPos]); + numResults++; + primkeySubPos = forward ? 0 : indexEntry.primaryKeys.length - 1; + } + + // Now we can collect the values based on the primary keys, + // if requested. + if (req.resultLevel === ResultLevel.Full) { + for (let i = 0; i < numResults; i++) { + const result = storeData.get(primaryKeys[i]); + if (!result) { + throw Error("invariant violated"); + } + values.push(result); + } + } + } else { + // only based on object store, no index involved, phew! + let storePos = req.lastObjectStorePosition; + if (storePos === undefined) { + storePos = forward ? range.lower : range.upper; + } + + if (req.advanceIndexKey !== undefined) { + throw Error("unsupported request"); + } + + storePos = furthestKey(forward, req.advancePrimaryKey, storePos); + + // Advance store position if we are either still at the last returned + // store key, or if we are currently not on a key. + const storeEntry = storeData.get(storePos); + if ( + !storeEntry || + (req.lastObjectStorePosition !== undefined && + compareKeys(req.lastObjectStorePosition, storeEntry.primaryKey)) + ) { + storePos = storeData.nextHigherKey(storePos); + } + + if (req.lastObjectStorePosition) + while (1) { + if (req.limit != 0 && numResults == req.limit) { + break; + } + if (storePos === null || storePos === undefined) { + break; + } + if (!range.includes(storePos)) { + break; + } + + const res = storeData.get(storePos); + + if (!res) { + break; + } + + if (req.resultLevel >= ResultLevel.OnlyKeys) { + primaryKeys.push(res.primaryKey); + } + + if (req.resultLevel >= ResultLevel.Full) { + values.push(res.value); + } + numResults++; + storePos = nextStoreKey(forward, storeData, storePos); + } + } + if (this.enableTracing) { + console.log(`TRACING: getRecords got ${numResults} results`) + } + return { + count: numResults, + indexKeys: + req.resultLevel >= ResultLevel.OnlyKeys && haveIndex + ? indexKeys + : undefined, + primaryKeys: + req.resultLevel >= ResultLevel.OnlyKeys ? primaryKeys : undefined, + values: req.resultLevel >= ResultLevel.Full ? values : undefined, + }; } async storeRecord( btx: DatabaseTransaction, storeReq: RecordStoreRequest, ): Promise<void> { - const myConn = this.connections[btx.transactionCookie]; + if (this.enableTracing) { + console.log(`TRACING: storeRecord`); + } + const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); } @@ -578,7 +929,7 @@ export class MemoryBackend implements Backend { const objectStore = myConn.objectStoreMap[storeReq.objectStoreName]; - const storeKeyResult: StoreKeyResult = getStoreKey( + const storeKeyResult: StoreKeyResult = makeStoreKeyValue( storeReq.value, storeReq.key, objectStore.modifiedKeyGenerator || objectStore.originalKeyGenerator, @@ -607,12 +958,54 @@ export class MemoryBackend implements Backend { throw Error("index referenced by object store does not exist"); } const indexProperties = schema.indexes[indexName]; - insertIntoIndex(index, value, indexProperties); + this.insertIntoIndex(index, key, value, indexProperties); + } + } + + insertIntoIndex( + index: Index, + primaryKey: Key, + value: Value, + indexProperties: IndexProperties, + ): void { + if (this.enableTracing) { + console.log( + `insertIntoIndex(${index.modifiedName || index.originalName})`, + ); + } + let indexData = index.modifiedData || index.originalData; + const indexKeys = getIndexKeys( + value, + indexProperties.keyPath, + indexProperties.multiEntry, + ); + for (const indexKey of indexKeys) { + const existingRecord = indexData.get(indexKey); + if (existingRecord) { + if (indexProperties.unique) { + throw new ConstraintError(); + } else { + const newIndexRecord = { + indexKey: indexKey, + primaryKeys: [primaryKey].concat(existingRecord.primaryKeys), + }; + index.modifiedData = indexData.with(indexKey, newIndexRecord, true); + } + } else { + const newIndexRecord: IndexRecord = { + indexKey: indexKey, + primaryKeys: [primaryKey], + }; + index.modifiedData = indexData.with(indexKey, newIndexRecord, true); + } } } async rollback(btx: DatabaseTransaction): Promise<void> { - const myConn = this.connections[btx.transactionCookie]; + if (this.enableTracing) { + console.log(`TRACING: rollback`); + } + const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); } @@ -642,10 +1035,15 @@ export class MemoryBackend implements Backend { objectStore.modifiedName = undefined; objectStore.modifiedKeyGenerator = undefined; } + delete this.connectionsByTransaction[btx.transactionCookie]; + this.transactionDoneCond.trigger(); } async commit(btx: DatabaseTransaction): Promise<void> { - const myConn = this.connections[btx.transactionCookie]; + if (this.enableTracing) { + console.log(`TRACING: commit`); + } + const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); } @@ -656,6 +1054,41 @@ export class MemoryBackend implements Backend { if (db.txLevel < TransactionLevel.Read) { throw Error("only allowed while running a transaction"); } + + db.committedSchema = myConn.modifiedSchema || db.committedSchema; + db.txLevel = TransactionLevel.Connected; + + db.committedIndexes = {}; + db.committedObjectStores = {}; + db.modifiedIndexes = {}; + db.committedObjectStores = {}; + + for (const indexName in myConn.indexMap) { + const index = myConn.indexMap[indexName]; + index.deleted = false; + index.originalData = index.modifiedData || index.originalData; + index.originalName = index.modifiedName || index.originalName; + db.committedIndexes[indexName] = index; + } + + for (const objectStoreName in myConn.objectStoreMap) { + const objectStore = myConn.objectStoreMap[objectStoreName]; + objectStore.deleted = false; + objectStore.originalData = + objectStore.modifiedData || objectStore.originalData; + objectStore.originalName = + objectStore.modifiedName || objectStore.originalName; + if (objectStore.modifiedKeyGenerator !== undefined) { + objectStore.originalKeyGenerator = objectStore.modifiedKeyGenerator; + } + db.committedObjectStores[objectStoreName] = objectStore; + } + + myConn.indexMap = Object.assign({}, db.committedIndexes); + myConn.objectStoreMap = Object.assign({}, db.committedObjectStores); + + delete this.connectionsByTransaction[btx.transactionCookie]; + this.transactionDoneCond.trigger(); } } diff --git a/packages/idb-bridge/src/backend-interface.ts b/packages/idb-bridge/src/backend-interface.ts index c0f498a10..c963b1896 100644 --- a/packages/idb-bridge/src/backend-interface.ts +++ b/packages/idb-bridge/src/backend-interface.ts @@ -45,18 +45,47 @@ export interface RecordGetRequest { direction: BridgeIDBCursorDirection; objectStoreName: string; indexName: string | undefined; + /** + * The range of keys to return. + * If indexName is defined, the range refers to the index keys. + * Otherwise it refers to the object store keys. + */ range: BridgeIDBKeyRange | undefined; + /** + * Last cursor position in terms of the index key. + * Can only be specified if indexName is defined and + * lastObjectStorePosition is defined. + * + * Must either be undefined or within range. + */ lastIndexPosition?: Key; + /** + * Last position in terms of the object store key. + */ lastObjectStorePosition?: Key; + /** + * If specified, the index key of the results must be + * greater or equal to advanceIndexKey. + * + * Only applicable if indexName is specified. + */ advanceIndexKey?: Key; + /** + * If specified, the primary key of the results must be greater + * or equal to advancePrimaryKey. + */ advancePrimaryKey?: Key; + /** + * Maximum number of resuts to return. + * If -1, return all available results + */ limit: number; resultLevel: ResultLevel; } export interface RecordGetResponse { values: Value[] | undefined; - keys: Key[] | undefined; + indexKeys: Key[] | undefined; primaryKeys: Key[] | undefined; count: number; } diff --git a/packages/idb-bridge/src/util/FakeEventTarget.ts b/packages/idb-bridge/src/util/FakeEventTarget.ts index 3c7eaf468..f20432df0 100644 --- a/packages/idb-bridge/src/util/FakeEventTarget.ts +++ b/packages/idb-bridge/src/util/FakeEventTarget.ts @@ -14,164 +14,172 @@ permissions and limitations under the License. */ - import { InvalidStateError } from "./errors"; import FakeEvent from "./FakeEvent"; import { EventCallback, EventType } from "./types"; type EventTypeProp = - | "onabort" - | "onblocked" - | "oncomplete" - | "onerror" - | "onsuccess" - | "onupgradeneeded" - | "onversionchange"; + | "onabort" + | "onblocked" + | "oncomplete" + | "onerror" + | "onsuccess" + | "onupgradeneeded" + | "onversionchange"; interface Listener { - callback: EventCallback; - capture: boolean; - type: EventType; + callback: EventCallback; + capture: boolean; + type: EventType; } const stopped = (event: FakeEvent, listener: Listener) => { - return ( - event.immediatePropagationStopped || - (event.eventPhase === event.CAPTURING_PHASE && - listener.capture === false) || - (event.eventPhase === event.BUBBLING_PHASE && listener.capture === true) - ); + return ( + event.immediatePropagationStopped || + (event.eventPhase === event.CAPTURING_PHASE && + listener.capture === false) || + (event.eventPhase === event.BUBBLING_PHASE && listener.capture === true) + ); }; // http://www.w3.org/TR/dom/#concept-event-listener-invoke const invokeEventListeners = (event: FakeEvent, obj: FakeEventTarget) => { - event.currentTarget = obj; - - // The callback might cause obj.listeners to mutate as we traverse it. - // Take a copy of the array so that nothing sneaks in and we don't lose - // our place. - for (const listener of obj.listeners.slice()) { - if (event.type !== listener.type || stopped(event, listener)) { - continue; - } - - // @ts-ignore - listener.callback.call(event.currentTarget, event); + event.currentTarget = obj; + + // The callback might cause obj.listeners to mutate as we traverse it. + // Take a copy of the array so that nothing sneaks in and we don't lose + // our place. + for (const listener of obj.listeners.slice()) { + if (event.type !== listener.type || stopped(event, listener)) { + continue; } - const typeToProp: { [key in EventType]: EventTypeProp } = { - abort: "onabort", - blocked: "onblocked", - complete: "oncomplete", - error: "onerror", - success: "onsuccess", - upgradeneeded: "onupgradeneeded", - versionchange: "onversionchange", + console.log(`invoking ${event.type} event listener`, listener); + // @ts-ignore + listener.callback.call(event.currentTarget, event); + } + + const typeToProp: { [key in EventType]: EventTypeProp } = { + abort: "onabort", + blocked: "onblocked", + complete: "oncomplete", + error: "onerror", + success: "onsuccess", + upgradeneeded: "onupgradeneeded", + versionchange: "onversionchange", + }; + const prop = typeToProp[event.type]; + if (prop === undefined) { + throw new Error(`Unknown event type: "${event.type}"`); + } + + const callback = event.currentTarget[prop]; + if (callback) { + const listener = { + callback, + capture: false, + type: event.type, }; - const prop = typeToProp[event.type]; - if (prop === undefined) { - throw new Error(`Unknown event type: "${event.type}"`); - } - - const callback = event.currentTarget[prop]; - if (callback) { - const listener = { - callback, - capture: false, - type: event.type, - }; - if (!stopped(event, listener)) { - // @ts-ignore - listener.callback.call(event.currentTarget, event); - } + if (!stopped(event, listener)) { + console.log(`invoking on${event.type} event listener`, listener); + // @ts-ignore + listener.callback.call(event.currentTarget, event); } + } }; abstract class FakeEventTarget { - public readonly listeners: Listener[] = []; - - // These will be overridden in individual subclasses and made not readonly - public readonly onabort: EventCallback | null | undefined; - public readonly onblocked: EventCallback | null | undefined; - public readonly oncomplete: EventCallback | null | undefined; - public readonly onerror: EventCallback | null | undefined; - public readonly onsuccess: EventCallback | null | undefined; - public readonly onupgradeneeded: EventCallback | null | undefined; - public readonly onversionchange: EventCallback | null | undefined; - - public addEventListener( - type: EventType, - callback: EventCallback, - capture = false, - ) { - this.listeners.push({ - callback, - capture, - type, - }); - } - - public removeEventListener( - type: EventType, - callback: EventCallback, - capture = false, - ) { - const i = this.listeners.findIndex(listener => { - return ( - listener.type === type && - listener.callback === callback && - listener.capture === capture - ); - }); - - this.listeners.splice(i, 1); + public readonly listeners: Listener[] = []; + + // These will be overridden in individual subclasses and made not readonly + public readonly onabort: EventCallback | null | undefined; + public readonly onblocked: EventCallback | null | undefined; + public readonly oncomplete: EventCallback | null | undefined; + public readonly onerror: EventCallback | null | undefined; + public readonly onsuccess: EventCallback | null | undefined; + public readonly onupgradeneeded: EventCallback | null | undefined; + public readonly onversionchange: EventCallback | null | undefined; + + static enableTracing: boolean = true; + + public addEventListener( + type: EventType, + callback: EventCallback, + capture = false, + ) { + this.listeners.push({ + callback, + capture, + type, + }); + } + + public removeEventListener( + type: EventType, + callback: EventCallback, + capture = false, + ) { + const i = this.listeners.findIndex(listener => { + return ( + listener.type === type && + listener.callback === callback && + listener.capture === capture + ); + }); + + this.listeners.splice(i, 1); + } + + // http://www.w3.org/TR/dom/#dispatching-events + public dispatchEvent(event: FakeEvent) { + if (event.dispatched || !event.initialized) { + throw new InvalidStateError("The object is in an invalid state."); } + event.isTrusted = false; - // http://www.w3.org/TR/dom/#dispatching-events - public dispatchEvent(event: FakeEvent) { - if (event.dispatched || !event.initialized) { - throw new InvalidStateError("The object is in an invalid state."); - } - event.isTrusted = false; + event.dispatched = true; + event.target = this; + // NOT SURE WHEN THIS SHOULD BE SET event.eventPath = []; - event.dispatched = true; - event.target = this; - // NOT SURE WHEN THIS SHOULD BE SET event.eventPath = []; + event.eventPhase = event.CAPTURING_PHASE; + if (FakeEventTarget.enableTracing) { + console.log( + `dispatching '${event.type}' event along path with ${event.eventPath.length} elements`, + ); + } + for (const obj of event.eventPath) { + if (!event.propagationStopped) { + invokeEventListeners(event, obj); + } + } - event.eventPhase = event.CAPTURING_PHASE; - for (const obj of event.eventPath) { - if (!event.propagationStopped) { - invokeEventListeners(event, obj); - } - } + event.eventPhase = event.AT_TARGET; + if (!event.propagationStopped) { + invokeEventListeners(event, event.target); + } - event.eventPhase = event.AT_TARGET; + if (event.bubbles) { + event.eventPath.reverse(); + event.eventPhase = event.BUBBLING_PHASE; + if (event.eventPath.length === 0 && event.type === "error") { + console.error("Unhandled error event: ", event.target); + } + for (const obj of event.eventPath) { if (!event.propagationStopped) { - invokeEventListeners(event, event.target); - } - - if (event.bubbles) { - event.eventPath.reverse(); - event.eventPhase = event.BUBBLING_PHASE; - if (event.eventPath.length === 0 && event.type === "error") { - console.error("Unhandled error event: ", event.target); - } - for (const obj of event.eventPath) { - if (!event.propagationStopped) { - invokeEventListeners(event, obj); - } - } + invokeEventListeners(event, obj); } + } + } - event.dispatched = false; - event.eventPhase = event.NONE; - event.currentTarget = null; + event.dispatched = false; + event.eventPhase = event.NONE; + event.currentTarget = null; - if (event.canceled) { - return false; - } - return true; + if (event.canceled) { + return false; } + return true; + } } export default FakeEventTarget; diff --git a/packages/idb-bridge/src/util/getIndexKeys.test.ts b/packages/idb-bridge/src/util/getIndexKeys.test.ts new file mode 100644 index 000000000..e1bc9dd00 --- /dev/null +++ b/packages/idb-bridge/src/util/getIndexKeys.test.ts @@ -0,0 +1,24 @@ +import test from "ava"; +import { getIndexKeys } from "./getIndexKeys"; + +test("basics", (t) => { + t.deepEqual(getIndexKeys({foo: 42}, "foo", false), [42]); + t.deepEqual(getIndexKeys({foo: {bar: 42}}, "foo.bar", false), [42]); + t.deepEqual(getIndexKeys({foo: [42, 43]}, "foo.0", false), [42]); + t.deepEqual(getIndexKeys({foo: [42, 43]}, "foo.1", false), [43]); + + t.deepEqual(getIndexKeys([1, 2, 3], "", false), [[1, 2, 3]]); + + t.throws(() => { + getIndexKeys({foo: 42}, "foo.bar", false); + }); + + t.deepEqual(getIndexKeys({foo: 42}, "foo", true), [42]); + t.deepEqual(getIndexKeys({foo: 42, bar: 10}, ["foo", "bar"], true), [42, 10]); + t.deepEqual(getIndexKeys({foo: 42, bar: 10}, ["foo", "bar"], false), [[42, 10]]); + t.deepEqual(getIndexKeys({foo: 42, bar: 10}, ["foo", "bar", "spam"], true), [42, 10]); + + t.throws(() => { + getIndexKeys({foo: 42, bar: 10}, ["foo", "bar", "spam"], false); + }); +}); diff --git a/packages/idb-bridge/src/util/getIndexKeys.ts b/packages/idb-bridge/src/util/getIndexKeys.ts new file mode 100644 index 000000000..416cf9ea2 --- /dev/null +++ b/packages/idb-bridge/src/util/getIndexKeys.ts @@ -0,0 +1,28 @@ +import { Key, Value, KeyPath } from "./types"; +import extractKey from "./extractKey"; +import valueToKey from "./valueToKey"; + +export function getIndexKeys( + value: Value, + keyPath: KeyPath, + multiEntry: boolean, +): Key[] { + if (multiEntry && Array.isArray(keyPath)) { + const keys = []; + for (const subkeyPath of keyPath) { + const key = extractKey(subkeyPath, value); + try { + const k = valueToKey(key); + keys.push(k); + } catch { + // Ignore invalid subkeys + } + } + return keys; + } else { + let key = extractKey(keyPath, value); + return [valueToKey(key)]; + } +} + +export default getIndexKeys; diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts new file mode 100644 index 000000000..7820875c3 --- /dev/null +++ b/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts @@ -0,0 +1,42 @@ +import test from 'ava'; +import { makeStoreKeyValue } from "./makeStoreKeyValue"; + +test("basics", (t) => { + let result; + + result = makeStoreKeyValue({ name: "Florian" }, undefined, 42, true, "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"); + 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"); + 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"); + 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); + 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); + t.is(result.updatedKeyGenerator, 11); + t.is(result.key, 10); + t.is(result.value.name, "Florian"); + t.is(result.value.id, undefined); +}); diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.ts index 4850cec26..4f45e0d8a 100644 --- a/packages/idb-bridge/src/util/makeStoreKeyValue.ts +++ b/packages/idb-bridge/src/util/makeStoreKeyValue.ts @@ -63,10 +63,14 @@ export function makeStoreKeyValue( updatedKeyGenerator = currentKeyGenerator + 1; } else if (typeof maybeInlineKey === "number") { key = maybeInlineKey; - updatedKeyGenerator = maybeInlineKey; + if (maybeInlineKey >= currentKeyGenerator) { + updatedKeyGenerator = maybeInlineKey + 1; + } else { + updatedKeyGenerator = currentKeyGenerator; + } } else { key = maybeInlineKey; - updatedKeyGenerator = currentKeyGenerator + 1; + updatedKeyGenerator = currentKeyGenerator; } return { key: key, @@ -84,9 +88,17 @@ export function makeStoreKeyValue( }; } } else { - // (no, no, yes) - // (no, no, no) - throw new DataError(); + if (autoIncrement) { + // (no, no, yes) + return { + key: currentKeyGenerator, + value: value, + updatedKeyGenerator: currentKeyGenerator + 1, + } + } else { + // (no, no, no) + throw new DataError(); + } } } -}
\ No newline at end of file +} |