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