idb-bridge: test cases, package structure and missing functionality

This commit is contained in:
Florian Dold 2019-07-31 01:33:23 +02:00
parent 16ecbc9f17
commit bcefbd7aab
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
13 changed files with 407 additions and 92 deletions

View File

@ -0,0 +1,3 @@
{
"editor.tabSize": 2
}

View File

@ -42,12 +42,14 @@ import {
Backend, Backend,
DatabaseTransaction, DatabaseTransaction,
RecordStoreRequest, RecordStoreRequest,
StoreLevel,
} from "./backend-interface"; } from "./backend-interface";
import BridgeIDBFactory from "./BridgeIDBFactory";
/** /**
* http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#cursor * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#cursor
*/ */
class BridgeIDBCursor { export class BridgeIDBCursor {
_request: BridgeIDBRequest | undefined; _request: BridgeIDBRequest | undefined;
private _gotValue: boolean = false; private _gotValue: boolean = false;
@ -119,14 +121,24 @@ class BridgeIDBCursor {
get primaryKey() { get primaryKey() {
return this._primaryKey; return this._primaryKey;
} }
set primaryKey(val) { set primaryKey(val) {
/* For babel */ /* For babel */
} }
protected get _isValueCursor(): boolean {
return false;
}
/** /**
* https://w3c.github.io/IndexedDB/#iterate-a-cursor * https://w3c.github.io/IndexedDB/#iterate-a-cursor
*/ */
async _iterate(key?: Key, primaryKey?: Key): Promise<any> { async _iterate(key?: Key, primaryKey?: Key): Promise<any> {
BridgeIDBFactory.enableTracing &&
console.log(
`iterating cursor os=${this._objectStoreName},idx=${this._indexName}`,
);
BridgeIDBFactory.enableTracing && console.log("cursor type ", this.toString());
const recordGetRequest: RecordGetRequest = { const recordGetRequest: RecordGetRequest = {
direction: this.direction, direction: this.direction,
indexName: this._indexName, indexName: this._indexName,
@ -145,7 +157,10 @@ class BridgeIDBCursor {
let response = await this._backend.getRecords(btx, recordGetRequest); let response = await this._backend.getRecords(btx, recordGetRequest);
if (response.count === 0) { 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; return null;
} }
@ -153,8 +168,10 @@ class BridgeIDBCursor {
throw Error("invariant failed"); throw Error("invariant failed");
} }
console.log("request is:", JSON.stringify(recordGetRequest)); if (BridgeIDBFactory.enableTracing) {
console.log("get response is:", JSON.stringify(response)); console.log("request is:", JSON.stringify(recordGetRequest));
console.log("get response is:", JSON.stringify(response));
}
if (this._indexName !== undefined) { if (this._indexName !== undefined) {
this._key = response.indexKeys![0]; this._key = response.indexKeys![0];
@ -204,20 +221,23 @@ class BridgeIDBCursor {
throw new InvalidStateError(); throw new InvalidStateError();
} }
if (!this._gotValue || !this.hasOwnProperty("value")) { if (!this._gotValue || !this._isValueCursor) {
throw new InvalidStateError(); throw new InvalidStateError();
} }
const storeReq: RecordStoreRequest = { const storeReq: RecordStoreRequest = {
overwrite: true,
key: this._primaryKey, key: this._primaryKey,
value: value, value: value,
objectStoreName: this._objectStoreName, objectStoreName: this._objectStoreName,
storeLevel: StoreLevel.UpdateExisting,
}; };
const operation = async () => { const operation = async () => {
if (BridgeIDBFactory.enableTracing) {
console.log("updating at cursor")
}
const { btx } = this.source._confirmActiveTransaction(); const { btx } = this.source._confirmActiveTransaction();
this._backend.storeRecord(btx, storeReq); await this._backend.storeRecord(btx, storeReq);
}; };
return transaction._execRequestAsync({ return transaction._execRequestAsync({
operation, operation,
@ -318,7 +338,7 @@ class BridgeIDBCursor {
throw new InvalidStateError(); throw new InvalidStateError();
} }
if (!this._gotValue || !this.hasOwnProperty("value")) { if (!this._gotValue || !this._isValueCursor) {
throw new InvalidStateError(); throw new InvalidStateError();
} }

View File

@ -16,32 +16,35 @@
import BridgeIDBCursor from "./BridgeIDBCursor"; import BridgeIDBCursor from "./BridgeIDBCursor";
import { import {
CursorRange, CursorRange,
CursorSource, CursorSource,
BridgeIDBCursorDirection, BridgeIDBCursorDirection,
Value, Value,
} from "./util/types"; } from "./util/types";
class BridgeIDBCursorWithValue extends BridgeIDBCursor { class BridgeIDBCursorWithValue extends BridgeIDBCursor {
get value(): Value {
return this._value;
}
get value(): Value { protected get _isValueCursor(): boolean {
return this._value; 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() { constructor(
return "[object IDBCursorWithValue]"; 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; export default BridgeIDBCursorWithValue;

View File

@ -50,7 +50,7 @@ const confirmActiveTransaction = (
}; };
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#idl-def-IDBIndex // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#idl-def-IDBIndex
class BridgeIDBIndex { export class BridgeIDBIndex {
objectStore: BridgeIDBObjectStore; objectStore: BridgeIDBObjectStore;
get _schema(): Schema { get _schema(): Schema {

View File

@ -113,21 +113,20 @@ class BridgeIDBKeyRange {
static _valueToKeyRange(value: any, nullDisallowedFlag: boolean = false) { static _valueToKeyRange(value: any, nullDisallowedFlag: boolean = false) {
if (value instanceof BridgeIDBKeyRange) { if (value instanceof BridgeIDBKeyRange) {
return value; return value;
} }
if (value === null || value === undefined) { if (value === null || value === undefined) {
if (nullDisallowedFlag) { if (nullDisallowedFlag) {
throw new DataError(); throw new DataError();
} }
return new BridgeIDBKeyRange(undefined, undefined, false, false); return new BridgeIDBKeyRange(undefined, undefined, false, false);
} }
const key = valueToKey(value); const key = valueToKey(value);
return BridgeIDBKeyRange.only(key); return BridgeIDBKeyRange.only(key);
} }
} }
export default BridgeIDBKeyRange; export default BridgeIDBKeyRange;

View File

@ -46,6 +46,7 @@ import {
DatabaseTransaction, DatabaseTransaction,
RecordGetRequest, RecordGetRequest,
ResultLevel, ResultLevel,
StoreLevel,
} from "./backend-interface"; } from "./backend-interface";
import BridgeIDBFactory from "./BridgeIDBFactory"; import BridgeIDBFactory from "./BridgeIDBFactory";
@ -137,7 +138,7 @@ class BridgeIDBObjectStore {
objectStoreName: this._name, objectStoreName: this._name,
key: key, key: key,
value: value, value: value,
overwrite, storeLevel: overwrite ? StoreLevel.AllowOverwrite : StoreLevel.NoOverwrite,
}); });
}; };
@ -158,7 +159,7 @@ class BridgeIDBObjectStore {
return this._store(value, key, false); return this._store(value, key, false);
} }
public delete(key: Key) { public delete(key: Key | BridgeIDBKeyRange) {
if (arguments.length === 0) { if (arguments.length === 0) {
throw new TypeError(); throw new TypeError();
} }
@ -167,13 +168,17 @@ class BridgeIDBObjectStore {
throw new ReadOnlyError(); throw new ReadOnlyError();
} }
if (!(key instanceof BridgeIDBKeyRange)) { let keyRange: BridgeIDBKeyRange;
key = valueToKey(key);
if (key instanceof BridgeIDBKeyRange) {
keyRange = key;
} else {
keyRange = BridgeIDBKeyRange.only(valueToKey(key));
} }
const operation = async () => { const operation = async () => {
const { btx } = this._confirmActiveTransaction(); const { btx } = this._confirmActiveTransaction();
return this._backend.deleteRecord(btx, this._name, key); return this._backend.deleteRecord(btx, this._name, keyRange);
} }
return this.transaction._execRequestAsync({ return this.transaction._execRequestAsync({
@ -183,12 +188,20 @@ class BridgeIDBObjectStore {
} }
public get(key?: BridgeIDBKeyRange | Key) { public get(key?: BridgeIDBKeyRange | Key) {
if (BridgeIDBFactory.enableTracing) {
console.log(`getting from object store ${this._name} key ${key}`);
}
if (arguments.length === 0) { if (arguments.length === 0) {
throw new TypeError(); throw new TypeError();
} }
if (!(key instanceof BridgeIDBKeyRange)) { let keyRange: BridgeIDBKeyRange;
key = valueToKey(key);
if (key instanceof BridgeIDBKeyRange) {
keyRange = key;
} else {
keyRange = BridgeIDBKeyRange.only(valueToKey(key));
} }
const recordRequest: RecordGetRequest = { const recordRequest: RecordGetRequest = {
@ -199,16 +212,24 @@ class BridgeIDBObjectStore {
direction: "next", direction: "next",
limit: 1, limit: 1,
resultLevel: ResultLevel.Full, resultLevel: ResultLevel.Full,
range: key, range: keyRange,
}; };
const operation = async () => { const operation = async () => {
if (BridgeIDBFactory.enableTracing) {
console.log("running get operation:", recordRequest);
}
const { btx } = this._confirmActiveTransaction(); const { btx } = this._confirmActiveTransaction();
const result = await this._backend.getRecords( const result = await this._backend.getRecords(
btx, btx,
recordRequest, recordRequest,
); );
if (result.count == 0) {
if (BridgeIDBFactory.enableTracing) {
console.log("get operation result count:", result.count);
}
if (result.count === 0) {
return undefined; return undefined;
} }
const values = result.values; const values = result.values;

View File

@ -174,12 +174,12 @@ class BridgeIDBTransaction extends FakeEventTarget {
*/ */
public async _start() { public async _start() {
if (BridgeIDBFactory.enableTracing) { if (BridgeIDBFactory.enableTracing) {
console.log(`TRACE: IDBTransaction._start, ${this._requests.length} queued`); console.log(
`TRACE: IDBTransaction._start, ${this._requests.length} queued`,
);
} }
this._started = true; this._started = true;
console.log("beginning transaction");
if (!this._backendTransaction) { if (!this._backendTransaction) {
this._backendTransaction = await this._backend.beginTransaction( this._backendTransaction = await this._backend.beginTransaction(
this.db._backendConnection, 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 // Remove from request queue - cursor ones will be added back if necessary by cursor.continue and such
let operation; let operation;
let request; let request;
@ -208,16 +206,17 @@ class BridgeIDBTransaction extends FakeEventTarget {
if (!request.source) { if (!request.source) {
// Special requests like indexes that just need to run some code, with error handling already built into // Special requests like indexes that just need to run some code, with error handling already built into
// operation // operation
console.log("running operation without source");
await operation(); await operation();
} else { } else {
console.log("running operation with source");
let event; let event;
try { try {
BridgeIDBFactory.enableTracing &&
console.log("TRACE: running operation in transaction");
const result = await operation(); const result = await operation();
if (BridgeIDBFactory.enableTracing) { BridgeIDBFactory.enableTracing &&
console.log("TRACE: tx operation finished with success"); console.log(
} "TRACE: operation in transaction finished with success",
);
request.readyState = "done"; request.readyState = "done";
request.result = result; request.result = result;
request.error = undefined; request.error = undefined;
@ -295,7 +294,7 @@ class BridgeIDBTransaction extends FakeEventTarget {
if (!this.error) { if (!this.error) {
if (BridgeIDBFactory.enableTracing) { if (BridgeIDBFactory.enableTracing) {
console.log("dispatching 'complete' event"); console.log("dispatching 'complete' event on transaction");
} }
const event = new FakeEvent("complete"); const event = new FakeEvent("complete");
event.eventPath = [this, this.db]; event.eventPath = [this, this.db];

View File

@ -235,3 +235,60 @@ test("Spec: Example 1 Part 3", async t => {
t.pass(); 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();
});

View File

@ -8,6 +8,7 @@ import {
RecordGetRequest, RecordGetRequest,
RecordGetResponse, RecordGetResponse,
ResultLevel, ResultLevel,
StoreLevel,
} from "./backend-interface"; } from "./backend-interface";
import structuredClone from "./util/structuredClone"; import structuredClone from "./util/structuredClone";
import { import {
@ -655,10 +656,10 @@ export class MemoryBackend implements Backend {
async deleteRecord( async deleteRecord(
btx: DatabaseTransaction, btx: DatabaseTransaction,
objectStoreName: string, objectStoreName: string,
range: import("./BridgeIDBKeyRange").default, range: BridgeIDBKeyRange,
): Promise<void> { ): Promise<void> {
if (this.enableTracing) { if (this.enableTracing) {
console.log(`TRACING: deleteRecord`); console.log(`TRACING: deleteRecord from store ${objectStoreName}`);
} }
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.connectionsByTransaction[btx.transactionCookie];
if (!myConn) { if (!myConn) {
@ -671,7 +672,112 @@ export class MemoryBackend implements Backend {
if (db.txLevel < TransactionLevel.Write) { if (db.txLevel < TransactionLevel.Write) {
throw Error("only allowed in write transaction"); 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( async getRecords(
@ -705,6 +811,18 @@ export class MemoryBackend implements Backend {
range = req.range; 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 numResults = 0;
let indexKeys: Key[] = []; let indexKeys: Key[] = [];
let primaryKeys: Key[] = []; let primaryKeys: Key[] = [];
@ -779,20 +897,21 @@ export class MemoryBackend implements Backend {
compareKeys(indexEntry.indexKey, req.lastIndexPosition) === 0 compareKeys(indexEntry.indexKey, req.lastIndexPosition) === 0
) { ) {
let pos = forward ? 0 : indexEntry.primaryKeys.length - 1; let pos = forward ? 0 : indexEntry.primaryKeys.length - 1;
console.log("number of primary keys", indexEntry.primaryKeys.length); this.enableTracing &&
console.log("start pos is", pos); console.log("number of primary keys", indexEntry.primaryKeys.length);
this.enableTracing && console.log("start pos is", pos);
// Advance past the lastObjectStorePosition // Advance past the lastObjectStorePosition
do { do {
const cmpResult = compareKeys( const cmpResult = compareKeys(
req.lastObjectStorePosition, req.lastObjectStorePosition,
indexEntry.primaryKeys[pos], indexEntry.primaryKeys[pos],
); );
console.log("cmp result is", cmpResult); this.enableTracing && console.log("cmp result is", cmpResult);
if ((forward && cmpResult < 0) || (!forward && cmpResult > 0)) { if ((forward && cmpResult < 0) || (!forward && cmpResult > 0)) {
break; break;
} }
pos += forward ? 1 : -1; 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); } while (pos >= 0 && pos < indexEntry.primaryKeys.length);
// Make sure we're at least at advancedPrimaryPos // Make sure we're at least at advancedPrimaryPos
@ -815,8 +934,10 @@ export class MemoryBackend implements Backend {
primkeySubPos = forward ? 0 : indexEntry.primaryKeys.length - 1; primkeySubPos = forward ? 0 : indexEntry.primaryKeys.length - 1;
} }
console.log("subPos=", primkeySubPos); if (this.enableTracing) {
console.log("indexPos=", indexPos); console.log("subPos=", primkeySubPos);
console.log("indexPos=", indexPos);
}
while (1) { while (1) {
if (req.limit != 0 && numResults == req.limit) { if (req.limit != 0 && numResults == req.limit) {
@ -867,12 +988,16 @@ export class MemoryBackend implements Backend {
} }
} }
if (!skip) { if (!skip) {
console.log(`not skipping!, subPos=${primkeySubPos}`); if (this.enableTracing) {
console.log(`not skipping!, subPos=${primkeySubPos}`);
}
indexKeys.push(indexEntry.indexKey); indexKeys.push(indexEntry.indexKey);
primaryKeys.push(indexEntry.primaryKeys[primkeySubPos]); primaryKeys.push(indexEntry.primaryKeys[primkeySubPos]);
numResults++; numResults++;
} else { } else {
console.log("skipping!"); if (this.enableTracing) {
console.log("skipping!");
}
} }
primkeySubPos += forward ? 1 : -1; primkeySubPos += forward ? 1 : -1;
} }
@ -885,7 +1010,7 @@ export class MemoryBackend implements Backend {
if (!result) { if (!result) {
throw Error("invariant violated"); throw Error("invariant violated");
} }
values.push(result); values.push(result.value);
} }
} }
} else { } else {
@ -905,7 +1030,9 @@ export class MemoryBackend implements Backend {
// Advance store position if we are either still at the last returned // Advance store position if we are either still at the last returned
// store key, or if we are currently not on a key. // store key, or if we are currently not on a key.
const storeEntry = storeData.get(storePos); const storeEntry = storeData.get(storePos);
console.log("store entry:", storeEntry); if (this.enableTracing) {
console.log("store entry:", storeEntry);
}
if ( if (
!storeEntry || !storeEntry ||
(req.lastObjectStorePosition !== undefined && (req.lastObjectStorePosition !== undefined &&
@ -915,7 +1042,9 @@ export class MemoryBackend implements Backend {
} }
} else { } else {
storePos = forward ? storeData.minKey() : storeData.maxKey(); 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) { while (1) {
@ -940,7 +1069,7 @@ export class MemoryBackend implements Backend {
} }
if (req.resultLevel >= ResultLevel.Full) { if (req.resultLevel >= ResultLevel.Full) {
values.push(res); values.push(res.value);
} }
numResults++; numResults++;
@ -983,30 +1112,50 @@ export class MemoryBackend implements Backend {
const schema = myConn.modifiedSchema const schema = myConn.modifiedSchema
? myConn.modifiedSchema ? myConn.modifiedSchema
: db.committedSchema; : db.committedSchema;
const objectStore = myConn.objectStoreMap[storeReq.objectStoreName]; 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) { if (!objectStore.modifiedData) {
objectStore.modifiedData = objectStore.originalData; objectStore.modifiedData = objectStore.originalData;
} }
const modifiedData = objectStore.modifiedData; const modifiedData = objectStore.modifiedData;
const hasKey = modifiedData.has(key);
if (hasKey && !storeReq.overwrite) { let key;
throw Error("refusing to overwrite"); 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] for (const indexName of schema.objectStores[storeReq.objectStoreName]
.indexes) { .indexes) {

View File

@ -41,6 +41,12 @@ export enum ResultLevel {
Full, Full,
} }
export enum StoreLevel {
NoOverwrite,
AllowOverwrite,
UpdateExisting,
}
export interface RecordGetRequest { export interface RecordGetRequest {
direction: BridgeIDBCursorDirection; direction: BridgeIDBCursorDirection;
objectStoreName: string; objectStoreName: string;
@ -94,7 +100,7 @@ export interface RecordStoreRequest {
objectStoreName: string; objectStoreName: string;
value: Value; value: Value;
key: Key | undefined; key: Key | undefined;
overwrite: boolean; storeLevel: StoreLevel;
} }
export interface Backend { export interface Backend {

View File

@ -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;
}

View File

@ -54,7 +54,6 @@ const invokeEventListeners = (event: FakeEvent, obj: FakeEventTarget) => {
continue; continue;
} }
console.log(`invoking ${event.type} event listener`, listener);
// @ts-ignore // @ts-ignore
listener.callback.call(event.currentTarget, event); listener.callback.call(event.currentTarget, event);
} }
@ -81,7 +80,6 @@ const invokeEventListeners = (event: FakeEvent, obj: FakeEventTarget) => {
type: event.type, type: event.type,
}; };
if (!stopped(event, listener)) { if (!stopped(event, listener)) {
console.log(`invoking on${event.type} event listener`, listener);
// @ts-ignore // @ts-ignore
listener.callback.call(event.currentTarget, event); listener.callback.call(event.currentTarget, event);
} }
@ -100,7 +98,7 @@ abstract class FakeEventTarget {
public readonly onupgradeneeded: EventCallback | null | undefined; public readonly onupgradeneeded: EventCallback | null | undefined;
public readonly onversionchange: EventCallback | null | undefined; public readonly onversionchange: EventCallback | null | undefined;
static enableTracing: boolean = true; static enableTracing: boolean = false;
public addEventListener( public addEventListener(
type: EventType, type: EventType,

View File

@ -18,7 +18,7 @@ export function makeStoreKeyValue(
autoIncrement: boolean, autoIncrement: boolean,
keyPath: KeyPath | null, keyPath: KeyPath | null,
): StoreKeyResult { ): StoreKeyResult {
const haveKey = key !== undefined && key !== null; const haveKey = key !== null && key !== undefined;
const haveKeyPath = keyPath !== null && keyPath !== undefined; const haveKeyPath = keyPath !== null && keyPath !== undefined;
// This models a decision table on (haveKey, haveKeyPath, autoIncrement) // This models a decision table on (haveKey, haveKeyPath, autoIncrement)