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,
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
constructor(
|
||||||
source: CursorSource,
|
source: CursorSource,
|
||||||
objectStoreName: string,
|
objectStoreName: string,
|
||||||
indexName: string | undefined,
|
indexName: string | undefined,
|
||||||
range: CursorRange,
|
range: CursorRange,
|
||||||
direction: BridgeIDBCursorDirection,
|
direction: BridgeIDBCursorDirection,
|
||||||
request?: any,
|
request?: any,
|
||||||
) {
|
) {
|
||||||
super(source, objectStoreName, indexName, range, direction, request, false);
|
super(source, objectStoreName, indexName, range, direction, request, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public toString() {
|
public toString() {
|
||||||
return "[object IDBCursorWithValue]";
|
return "[object IDBCursorWithValue]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BridgeIDBCursorWithValue;
|
export default BridgeIDBCursorWithValue;
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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];
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {
|
||||||
|
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;
|
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,
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user