From bcefbd7aab5f33f93d626c6421a1a1218c1a91a2 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 31 Jul 2019 01:33:23 +0200 Subject: idb-bridge: test cases, package structure and missing functionality --- packages/idb-bridge/.vscode/settings.json | 3 + packages/idb-bridge/src/BridgeIDBCursor.ts | 36 +++- .../idb-bridge/src/BridgeIDBCursorWithValue.ts | 49 ++--- packages/idb-bridge/src/BridgeIDBIndex.ts | 2 +- packages/idb-bridge/src/BridgeIDBKeyRange.ts | 11 +- packages/idb-bridge/src/BridgeIDBObjectStore.ts | 39 +++- packages/idb-bridge/src/BridgeIDBTransaction.ts | 21 +- packages/idb-bridge/src/MemoryBackend.test.ts | 57 ++++++ packages/idb-bridge/src/MemoryBackend.ts | 211 ++++++++++++++++++--- packages/idb-bridge/src/backend-interface.ts | 8 +- packages/idb-bridge/src/index.ts | 60 ++++++ packages/idb-bridge/src/util/FakeEventTarget.ts | 4 +- packages/idb-bridge/src/util/makeStoreKeyValue.ts | 2 +- 13 files changed, 409 insertions(+), 94 deletions(-) create mode 100644 packages/idb-bridge/.vscode/settings.json create mode 100644 packages/idb-bridge/src/index.ts (limited to 'packages') diff --git a/packages/idb-bridge/.vscode/settings.json b/packages/idb-bridge/.vscode/settings.json new file mode 100644 index 000000000..ff30c4464 --- /dev/null +++ b/packages/idb-bridge/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.tabSize": 2 +} \ No newline at end of file diff --git a/packages/idb-bridge/src/BridgeIDBCursor.ts b/packages/idb-bridge/src/BridgeIDBCursor.ts index ed5aa3e88..6c3139083 100644 --- a/packages/idb-bridge/src/BridgeIDBCursor.ts +++ b/packages/idb-bridge/src/BridgeIDBCursor.ts @@ -42,12 +42,14 @@ import { Backend, DatabaseTransaction, RecordStoreRequest, + StoreLevel, } from "./backend-interface"; +import BridgeIDBFactory from "./BridgeIDBFactory"; /** * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#cursor */ -class BridgeIDBCursor { +export class BridgeIDBCursor { _request: BridgeIDBRequest | undefined; private _gotValue: boolean = false; @@ -119,14 +121,24 @@ class BridgeIDBCursor { get primaryKey() { return this._primaryKey; } + set primaryKey(val) { /* For babel */ } + protected get _isValueCursor(): boolean { + return false; + } + /** * https://w3c.github.io/IndexedDB/#iterate-a-cursor */ async _iterate(key?: Key, primaryKey?: Key): Promise { + BridgeIDBFactory.enableTracing && + console.log( + `iterating cursor os=${this._objectStoreName},idx=${this._indexName}`, + ); + BridgeIDBFactory.enableTracing && console.log("cursor type ", this.toString()); const recordGetRequest: RecordGetRequest = { direction: this.direction, indexName: this._indexName, @@ -145,7 +157,10 @@ class BridgeIDBCursor { let response = await this._backend.getRecords(btx, recordGetRequest); if (response.count === 0) { - console.log("cursor is returning empty result"); + if (BridgeIDBFactory.enableTracing) { + console.log("cursor is returning empty result"); + } + this._gotValue = false; return null; } @@ -153,8 +168,10 @@ class BridgeIDBCursor { throw Error("invariant failed"); } - console.log("request is:", JSON.stringify(recordGetRequest)); - console.log("get response is:", JSON.stringify(response)); + if (BridgeIDBFactory.enableTracing) { + console.log("request is:", JSON.stringify(recordGetRequest)); + console.log("get response is:", JSON.stringify(response)); + } if (this._indexName !== undefined) { this._key = response.indexKeys![0]; @@ -204,20 +221,23 @@ class BridgeIDBCursor { throw new InvalidStateError(); } - if (!this._gotValue || !this.hasOwnProperty("value")) { + if (!this._gotValue || !this._isValueCursor) { throw new InvalidStateError(); } const storeReq: RecordStoreRequest = { - overwrite: true, key: this._primaryKey, value: value, objectStoreName: this._objectStoreName, + storeLevel: StoreLevel.UpdateExisting, }; const operation = async () => { + if (BridgeIDBFactory.enableTracing) { + console.log("updating at cursor") + } const { btx } = this.source._confirmActiveTransaction(); - this._backend.storeRecord(btx, storeReq); + await this._backend.storeRecord(btx, storeReq); }; return transaction._execRequestAsync({ operation, @@ -318,7 +338,7 @@ class BridgeIDBCursor { throw new InvalidStateError(); } - if (!this._gotValue || !this.hasOwnProperty("value")) { + if (!this._gotValue || !this._isValueCursor) { throw new InvalidStateError(); } diff --git a/packages/idb-bridge/src/BridgeIDBCursorWithValue.ts b/packages/idb-bridge/src/BridgeIDBCursorWithValue.ts index b2f23147f..d75bd21e6 100644 --- a/packages/idb-bridge/src/BridgeIDBCursorWithValue.ts +++ b/packages/idb-bridge/src/BridgeIDBCursorWithValue.ts @@ -16,32 +16,35 @@ import BridgeIDBCursor from "./BridgeIDBCursor"; import { - CursorRange, - CursorSource, - BridgeIDBCursorDirection, - Value, + CursorRange, + CursorSource, + BridgeIDBCursorDirection, + Value, } from "./util/types"; class BridgeIDBCursorWithValue extends BridgeIDBCursor { - - get value(): Value { - return this._value; - } - - constructor( - source: CursorSource, - objectStoreName: string, - indexName: string | undefined, - range: CursorRange, - direction: BridgeIDBCursorDirection, - request?: any, - ) { - super(source, objectStoreName, indexName, range, direction, request, false); - } - - public toString() { - return "[object IDBCursorWithValue]"; - } + get value(): Value { + return this._value; + } + + protected get _isValueCursor(): boolean { + return true; + } + + constructor( + source: CursorSource, + objectStoreName: string, + indexName: string | undefined, + range: CursorRange, + direction: BridgeIDBCursorDirection, + request?: any, + ) { + super(source, objectStoreName, indexName, range, direction, request, false); + } + + public toString() { + return "[object IDBCursorWithValue]"; + } } export default BridgeIDBCursorWithValue; diff --git a/packages/idb-bridge/src/BridgeIDBIndex.ts b/packages/idb-bridge/src/BridgeIDBIndex.ts index 8179be83f..4d2022d3a 100644 --- a/packages/idb-bridge/src/BridgeIDBIndex.ts +++ b/packages/idb-bridge/src/BridgeIDBIndex.ts @@ -50,7 +50,7 @@ const confirmActiveTransaction = ( }; // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#idl-def-IDBIndex -class BridgeIDBIndex { +export class BridgeIDBIndex { objectStore: BridgeIDBObjectStore; get _schema(): Schema { diff --git a/packages/idb-bridge/src/BridgeIDBKeyRange.ts b/packages/idb-bridge/src/BridgeIDBKeyRange.ts index 2b32c7e0a..4055e092a 100644 --- a/packages/idb-bridge/src/BridgeIDBKeyRange.ts +++ b/packages/idb-bridge/src/BridgeIDBKeyRange.ts @@ -113,21 +113,20 @@ class BridgeIDBKeyRange { static _valueToKeyRange(value: any, nullDisallowedFlag: boolean = false) { if (value instanceof BridgeIDBKeyRange) { - return value; + return value; } if (value === null || value === undefined) { - if (nullDisallowedFlag) { - throw new DataError(); - } - return new BridgeIDBKeyRange(undefined, undefined, false, false); + if (nullDisallowedFlag) { + throw new DataError(); + } + return new BridgeIDBKeyRange(undefined, undefined, false, false); } const key = valueToKey(value); return BridgeIDBKeyRange.only(key); } - } export default BridgeIDBKeyRange; diff --git a/packages/idb-bridge/src/BridgeIDBObjectStore.ts b/packages/idb-bridge/src/BridgeIDBObjectStore.ts index eca4c1981..af5f80511 100644 --- a/packages/idb-bridge/src/BridgeIDBObjectStore.ts +++ b/packages/idb-bridge/src/BridgeIDBObjectStore.ts @@ -46,6 +46,7 @@ import { DatabaseTransaction, RecordGetRequest, ResultLevel, + StoreLevel, } from "./backend-interface"; import BridgeIDBFactory from "./BridgeIDBFactory"; @@ -137,7 +138,7 @@ class BridgeIDBObjectStore { objectStoreName: this._name, key: key, value: value, - overwrite, + storeLevel: overwrite ? StoreLevel.AllowOverwrite : StoreLevel.NoOverwrite, }); }; @@ -158,7 +159,7 @@ class BridgeIDBObjectStore { return this._store(value, key, false); } - public delete(key: Key) { + public delete(key: Key | BridgeIDBKeyRange) { if (arguments.length === 0) { throw new TypeError(); } @@ -167,13 +168,17 @@ class BridgeIDBObjectStore { throw new ReadOnlyError(); } - if (!(key instanceof BridgeIDBKeyRange)) { - key = valueToKey(key); + let keyRange: BridgeIDBKeyRange; + + if (key instanceof BridgeIDBKeyRange) { + keyRange = key; + } else { + keyRange = BridgeIDBKeyRange.only(valueToKey(key)); } const operation = async () => { const { btx } = this._confirmActiveTransaction(); - return this._backend.deleteRecord(btx, this._name, key); + return this._backend.deleteRecord(btx, this._name, keyRange); } return this.transaction._execRequestAsync({ @@ -183,12 +188,20 @@ class BridgeIDBObjectStore { } public get(key?: BridgeIDBKeyRange | Key) { + if (BridgeIDBFactory.enableTracing) { + console.log(`getting from object store ${this._name} key ${key}`); + } + if (arguments.length === 0) { throw new TypeError(); } - if (!(key instanceof BridgeIDBKeyRange)) { - key = valueToKey(key); + let keyRange: BridgeIDBKeyRange; + + if (key instanceof BridgeIDBKeyRange) { + keyRange = key; + } else { + keyRange = BridgeIDBKeyRange.only(valueToKey(key)); } const recordRequest: RecordGetRequest = { @@ -199,16 +212,24 @@ class BridgeIDBObjectStore { direction: "next", limit: 1, resultLevel: ResultLevel.Full, - range: key, + range: keyRange, }; const operation = async () => { + if (BridgeIDBFactory.enableTracing) { + console.log("running get operation:", recordRequest); + } const { btx } = this._confirmActiveTransaction(); const result = await this._backend.getRecords( btx, recordRequest, ); - if (result.count == 0) { + + if (BridgeIDBFactory.enableTracing) { + console.log("get operation result count:", result.count); + } + + if (result.count === 0) { return undefined; } const values = result.values; diff --git a/packages/idb-bridge/src/BridgeIDBTransaction.ts b/packages/idb-bridge/src/BridgeIDBTransaction.ts index a9f0201d3..250e27149 100644 --- a/packages/idb-bridge/src/BridgeIDBTransaction.ts +++ b/packages/idb-bridge/src/BridgeIDBTransaction.ts @@ -174,12 +174,12 @@ class BridgeIDBTransaction extends FakeEventTarget { */ public async _start() { if (BridgeIDBFactory.enableTracing) { - console.log(`TRACE: IDBTransaction._start, ${this._requests.length} queued`); + 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, @@ -188,8 +188,6 @@ 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; @@ -208,16 +206,17 @@ 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 { - console.log("running operation with source"); let event; try { + BridgeIDBFactory.enableTracing && + console.log("TRACE: running operation in transaction"); const result = await operation(); - if (BridgeIDBFactory.enableTracing) { - console.log("TRACE: tx operation finished with success"); - } + BridgeIDBFactory.enableTracing && + console.log( + "TRACE: operation in transaction finished with success", + ); request.readyState = "done"; request.result = result; request.error = undefined; @@ -295,7 +294,7 @@ class BridgeIDBTransaction extends FakeEventTarget { if (!this.error) { if (BridgeIDBFactory.enableTracing) { - console.log("dispatching 'complete' event"); + console.log("dispatching 'complete' event on transaction"); } const event = new FakeEvent("complete"); event.eventPath = [this, this.db]; diff --git a/packages/idb-bridge/src/MemoryBackend.test.ts b/packages/idb-bridge/src/MemoryBackend.test.ts index c21c2d064..5ec818f53 100644 --- a/packages/idb-bridge/src/MemoryBackend.test.ts +++ b/packages/idb-bridge/src/MemoryBackend.test.ts @@ -235,3 +235,60 @@ test("Spec: Example 1 Part 3", async t => { t.pass(); }); + + +test("simple deletion", 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"); + }; + + 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", "readwrite"); + + const store2 = tx2.objectStore("books"); + + const req1 = store2.get(234567); + await promiseFromRequest(req1); + t.is(req1.readyState, "done"); + t.is(req1.result.author, "Fred"); + + store2.delete(123456); + + const req2 = store2.get(123456); + await promiseFromRequest(req2); + t.is(req2.readyState, "done"); + t.is(req2.result, undefined); + + const req3 = store2.get(234567); + await promiseFromRequest(req3); + t.is(req3.readyState, "done"); + t.is(req3.result.author, "Fred"); + + await promiseFromTransaction(tx2); + + t.pass(); +}); + diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts index 1a85a7397..a31adb826 100644 --- a/packages/idb-bridge/src/MemoryBackend.ts +++ b/packages/idb-bridge/src/MemoryBackend.ts @@ -8,6 +8,7 @@ import { RecordGetRequest, RecordGetResponse, ResultLevel, + StoreLevel, } from "./backend-interface"; import structuredClone from "./util/structuredClone"; import { @@ -655,10 +656,10 @@ export class MemoryBackend implements Backend { async deleteRecord( btx: DatabaseTransaction, objectStoreName: string, - range: import("./BridgeIDBKeyRange").default, + range: BridgeIDBKeyRange, ): Promise { if (this.enableTracing) { - console.log(`TRACING: deleteRecord`); + console.log(`TRACING: deleteRecord from store ${objectStoreName}`); } const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { @@ -671,7 +672,112 @@ export class MemoryBackend implements Backend { if (db.txLevel < TransactionLevel.Write) { throw Error("only allowed in write transaction"); } - throw Error("not implemented"); + if (typeof range !== "object") { + throw Error("deleteRecord got invalid range (must be object)"); + } + if (!("lowerOpen" in range)) { + throw Error("deleteRecord got invalid range (sanity check failed, 'lowerOpen' missing)"); + } + + const schema = myConn.modifiedSchema + ? myConn.modifiedSchema + : db.committedSchema; + const objectStore = myConn.objectStoreMap[objectStoreName]; + + if (!objectStore.modifiedData) { + objectStore.modifiedData = objectStore.originalData; + } + + let modifiedData = objectStore.modifiedData; + let currKey: Key | undefined; + + if (range.lower === undefined || range.lower === null) { + currKey = modifiedData.minKey(); + } else { + currKey = range.lower; + // We have a range with an lowerOpen lower bound, so don't start + // deleting the upper bound. Instead start with the next higher key. + if (range.lowerOpen && currKey !== undefined) { + currKey = modifiedData.nextHigherKey(currKey); + } + } + + // invariant: (currKey is undefined) or (currKey is a valid key) + + while (true) { + if (currKey === undefined) { + // nothing more to delete! + break; + } + if (range.upper !== null && range.upper !== undefined) { + if (range.upperOpen && compareKeys(currKey, range.upper) === 0) { + // We have a range that's upperOpen, so stop before we delete the upper bound. + break; + } + if ((!range.upperOpen) && compareKeys(currKey, range.upper) > 0) { + // The upper range is inclusive, only stop if we're after the upper range. + break; + } + } + + const storeEntry = modifiedData.get(currKey); + if (!storeEntry) { + throw Error("assertion failed"); + } + + for (const indexName of schema.objectStores[objectStoreName].indexes) { + const index = myConn.indexMap[indexName]; + if (!index) { + throw Error("index referenced by object store does not exist"); + } + const indexProperties = schema.indexes[indexName]; + this.deleteFromIndex(index, storeEntry.primaryKey, storeEntry.value, indexProperties); + } + + modifiedData = modifiedData.without(currKey); + + currKey = modifiedData.nextHigherKey(currKey); + } + + objectStore.modifiedData = modifiedData; + } + + private deleteFromIndex( + index: Index, + primaryKey: Key, + value: Value, + indexProperties: IndexProperties, + ): void { + if (this.enableTracing) { + console.log( + `deleteFromIndex(${index.modifiedName || index.originalName})`, + ); + } + if (value === undefined || value === null) { + throw Error("cannot delete null/undefined value from index"); + } + 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) { + throw Error("db inconsistent: expected index entry missing"); + } + const newPrimaryKeys = existingRecord.primaryKeys.filter((x) => compareKeys(x, primaryKey) !== 0); + if (newPrimaryKeys.length === 0) { + index.originalData = indexData.without(indexKey); + } else { + const newIndexRecord = { + indexKey, + primaryKeys: newPrimaryKeys, + } + index.modifiedData = indexData.with(indexKey, newIndexRecord, true); + } + } } async getRecords( @@ -705,6 +811,18 @@ export class MemoryBackend implements Backend { range = req.range; } + if (typeof range !== "object") { + throw Error( + "getRecords was given an invalid range (sanity check failed, not an object)", + ); + } + + if (!("lowerOpen" in range)) { + throw Error( + "getRecords was given an invalid range (sanity check failed, lowerOpen missing)", + ); + } + let numResults = 0; let indexKeys: Key[] = []; let primaryKeys: Key[] = []; @@ -779,20 +897,21 @@ export class MemoryBackend implements Backend { compareKeys(indexEntry.indexKey, req.lastIndexPosition) === 0 ) { let pos = forward ? 0 : indexEntry.primaryKeys.length - 1; - console.log("number of primary keys", indexEntry.primaryKeys.length); - console.log("start pos is", pos); + this.enableTracing && + console.log("number of primary keys", indexEntry.primaryKeys.length); + this.enableTracing && console.log("start pos is", pos); // Advance past the lastObjectStorePosition do { const cmpResult = compareKeys( req.lastObjectStorePosition, indexEntry.primaryKeys[pos], ); - console.log("cmp result is", cmpResult); + this.enableTracing && console.log("cmp result is", cmpResult); if ((forward && cmpResult < 0) || (!forward && cmpResult > 0)) { break; } pos += forward ? 1 : -1; - console.log("now pos is", pos); + this.enableTracing && console.log("now pos is", pos); } while (pos >= 0 && pos < indexEntry.primaryKeys.length); // Make sure we're at least at advancedPrimaryPos @@ -815,8 +934,10 @@ export class MemoryBackend implements Backend { primkeySubPos = forward ? 0 : indexEntry.primaryKeys.length - 1; } - console.log("subPos=", primkeySubPos); - console.log("indexPos=", indexPos); + if (this.enableTracing) { + console.log("subPos=", primkeySubPos); + console.log("indexPos=", indexPos); + } while (1) { if (req.limit != 0 && numResults == req.limit) { @@ -867,12 +988,16 @@ export class MemoryBackend implements Backend { } } if (!skip) { - console.log(`not skipping!, subPos=${primkeySubPos}`); + if (this.enableTracing) { + console.log(`not skipping!, subPos=${primkeySubPos}`); + } indexKeys.push(indexEntry.indexKey); primaryKeys.push(indexEntry.primaryKeys[primkeySubPos]); numResults++; } else { - console.log("skipping!"); + if (this.enableTracing) { + console.log("skipping!"); + } } primkeySubPos += forward ? 1 : -1; } @@ -885,7 +1010,7 @@ export class MemoryBackend implements Backend { if (!result) { throw Error("invariant violated"); } - values.push(result); + values.push(result.value); } } } else { @@ -905,7 +1030,9 @@ export class MemoryBackend implements Backend { // 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); - console.log("store entry:", storeEntry); + if (this.enableTracing) { + console.log("store entry:", storeEntry); + } if ( !storeEntry || (req.lastObjectStorePosition !== undefined && @@ -915,7 +1042,9 @@ export class MemoryBackend implements Backend { } } else { storePos = forward ? storeData.minKey() : storeData.maxKey(); - console.log("setting starting store store pos to", storePos); + if (this.enableTracing) { + console.log("setting starting store pos to", storePos); + } } while (1) { @@ -940,7 +1069,7 @@ export class MemoryBackend implements Backend { } if (req.resultLevel >= ResultLevel.Full) { - values.push(res); + values.push(res.value); } numResults++; @@ -983,30 +1112,50 @@ export class MemoryBackend implements Backend { const schema = myConn.modifiedSchema ? myConn.modifiedSchema : db.committedSchema; - const objectStore = myConn.objectStoreMap[storeReq.objectStoreName]; - const storeKeyResult: StoreKeyResult = makeStoreKeyValue( - storeReq.value, - storeReq.key, - objectStore.modifiedKeyGenerator || objectStore.originalKeyGenerator, - schema.objectStores[storeReq.objectStoreName].autoIncrement, - schema.objectStores[storeReq.objectStoreName].keyPath, - ); - let key = storeKeyResult.key; - let value = storeKeyResult.value; - objectStore.modifiedKeyGenerator = storeKeyResult.updatedKeyGenerator; - if (!objectStore.modifiedData) { objectStore.modifiedData = objectStore.originalData; } const modifiedData = objectStore.modifiedData; - const hasKey = modifiedData.has(key); - if (hasKey && !storeReq.overwrite) { - throw Error("refusing to overwrite"); + + let key; + let value; + + if (storeReq.storeLevel === StoreLevel.UpdateExisting) { + if (storeReq.key === null || storeReq.key === undefined) { + throw Error("invalid update request (key not given)"); + } + + if (!objectStore.modifiedData.has(storeReq.key)) { + throw Error("invalid update request (record does not exist)"); + } + key = storeReq.key; + value = storeReq.value; + } else { + const storeKeyResult: StoreKeyResult = makeStoreKeyValue( + storeReq.value, + storeReq.key, + objectStore.modifiedKeyGenerator || objectStore.originalKeyGenerator, + schema.objectStores[storeReq.objectStoreName].autoIncrement, + schema.objectStores[storeReq.objectStoreName].keyPath, + ); + key = storeKeyResult.key; + value = storeKeyResult.value; + objectStore.modifiedKeyGenerator = storeKeyResult.updatedKeyGenerator; + const hasKey = modifiedData.has(key); + + if (hasKey && storeReq.storeLevel !== StoreLevel.AllowOverwrite) { + throw Error("refusing to overwrite"); + } } - objectStore.modifiedData = modifiedData.with(key, value, true); + const objectStoreRecord: ObjectStoreRecord = { + primaryKey: key, + value: value, + }; + + objectStore.modifiedData = modifiedData.with(key, objectStoreRecord, true); for (const indexName of schema.objectStores[storeReq.objectStoreName] .indexes) { diff --git a/packages/idb-bridge/src/backend-interface.ts b/packages/idb-bridge/src/backend-interface.ts index ab093d9cb..7329ed96a 100644 --- a/packages/idb-bridge/src/backend-interface.ts +++ b/packages/idb-bridge/src/backend-interface.ts @@ -41,6 +41,12 @@ export enum ResultLevel { Full, } +export enum StoreLevel { + NoOverwrite, + AllowOverwrite, + UpdateExisting, +} + export interface RecordGetRequest { direction: BridgeIDBCursorDirection; objectStoreName: string; @@ -94,7 +100,7 @@ export interface RecordStoreRequest { objectStoreName: string; value: Value; key: Key | undefined; - overwrite: boolean; + storeLevel: StoreLevel; } export interface Backend { diff --git a/packages/idb-bridge/src/index.ts b/packages/idb-bridge/src/index.ts new file mode 100644 index 000000000..a65458748 --- /dev/null +++ b/packages/idb-bridge/src/index.ts @@ -0,0 +1,60 @@ +import { BridgeIDBFactory } from "./BridgeIDBFactory"; +import { BridgeIDBCursor } from "./BridgeIDBCursor"; +import { BridgeIDBIndex } from "./BridgeIDBIndex"; +import BridgeIDBDatabase from "./BridgeIDBDatabase"; +import BridgeIDBKeyRange from "./BridgeIDBKeyRange"; +import BridgeIDBObjectStore from "./BridgeIDBObjectStore"; +import BridgeIDBOpenDBRequest from "./BridgeIDBOpenDBRequest"; +import BridgeIDBRequest from "./BridgeIDBRequest"; +import BridgeIDBTransaction from "./BridgeIDBTransaction"; +import BridgeIDBVersionChangeEvent from "./BridgeIDBVersionChangeEvent"; + +export { BridgeIDBFactory, BridgeIDBCursor }; + +export { MemoryBackend } from "./MemoryBackend"; + +// globalThis polyfill, see https://mathiasbynens.be/notes/globalthis +(function() { + if (typeof globalThis === "object") return; + Object.defineProperty(Object.prototype, "__magic__", { + get: function() { + return this; + }, + configurable: true, // This makes it possible to `delete` the getter later. + }); + // @ts-ignore: polyfill magic + __magic__.globalThis = __magic__; // lolwat + // @ts-ignore: polyfill magic + delete Object.prototype.__magic__; +})(); + +/** + * Populate the global name space such that the given IndexedDB factory is made + * available globally. + */ +export function shimIndexedDB(factory: BridgeIDBFactory): void { + // @ts-ignore: shimming + globalThis.indexedDB = factory; + // @ts-ignore: shimming + globalThis.IDBCursor = BridgeIDBCursor; + // @ts-ignore: shimming + globalThis.IDBKeyRange = BridgeIDBKeyRange; + // @ts-ignore: shimming + globalThis.IDBDatabase = BridgeIDBDatabase; + // @ts-ignore: shimming + globalThis.IDBFactory = BridgeIDBFactory; + // @ts-ignore: shimming + globalThis.IDBIndex = BridgeIDBIndex; + // @ts-ignore: shimming + globalThis.IDBKeyRange = BridgeIDBKeyRange; + // @ts-ignore: shimming + globalThis.IDBObjectStore = BridgeIDBObjectStore; + // @ts-ignore: shimming + globalThis.IDBOpenDBRequest = BridgeIDBOpenDBRequest; + // @ts-ignore: shimming + globalThis.IDBRequest = BridgeIDBRequest; + // @ts-ignore: shimming + globalThis.IDBTransaction = BridgeIDBTransaction; + // @ts-ignore: shimming + globalThis.IDBVersionChangeEvent = BridgeIDBVersionChangeEvent; +} diff --git a/packages/idb-bridge/src/util/FakeEventTarget.ts b/packages/idb-bridge/src/util/FakeEventTarget.ts index f20432df0..025f21b4c 100644 --- a/packages/idb-bridge/src/util/FakeEventTarget.ts +++ b/packages/idb-bridge/src/util/FakeEventTarget.ts @@ -54,7 +54,6 @@ const invokeEventListeners = (event: FakeEvent, obj: FakeEventTarget) => { continue; } - console.log(`invoking ${event.type} event listener`, listener); // @ts-ignore listener.callback.call(event.currentTarget, event); } @@ -81,7 +80,6 @@ const invokeEventListeners = (event: FakeEvent, obj: FakeEventTarget) => { type: event.type, }; if (!stopped(event, listener)) { - console.log(`invoking on${event.type} event listener`, listener); // @ts-ignore listener.callback.call(event.currentTarget, event); } @@ -100,7 +98,7 @@ abstract class FakeEventTarget { public readonly onupgradeneeded: EventCallback | null | undefined; public readonly onversionchange: EventCallback | null | undefined; - static enableTracing: boolean = true; + static enableTracing: boolean = false; public addEventListener( type: EventType, diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.ts index 4f45e0d8a..845634ac2 100644 --- a/packages/idb-bridge/src/util/makeStoreKeyValue.ts +++ b/packages/idb-bridge/src/util/makeStoreKeyValue.ts @@ -18,7 +18,7 @@ export function makeStoreKeyValue( autoIncrement: boolean, keyPath: KeyPath | null, ): StoreKeyResult { - const haveKey = key !== undefined && key !== null; + const haveKey = key !== null && key !== undefined; const haveKeyPath = keyPath !== null && keyPath !== undefined; // This models a decision table on (haveKey, haveKeyPath, autoIncrement) -- cgit v1.2.3