idb-bridge: test cases, package structure and missing functionality
This commit is contained in:
parent
16ecbc9f17
commit
bcefbd7aab
3
packages/idb-bridge/.vscode/settings.json
vendored
Normal file
3
packages/idb-bridge/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.tabSize": 2
|
||||
}
|
@ -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<any> {
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
protected get _isValueCursor(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return "[object IDBCursorWithValue]";
|
||||
}
|
||||
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;
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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];
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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<void> {
|
||||
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) {
|
||||
|
@ -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 {
|
||||
|
60
packages/idb-bridge/src/index.ts
Normal file
60
packages/idb-bridge/src/index.ts
Normal 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;
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user