idb: tests working

This commit is contained in:
Florian Dold 2019-06-21 19:18:36 +02:00
parent 2ee9431f1b
commit a4e4125cca
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
16 changed files with 1666 additions and 734 deletions

View File

@ -13,7 +13,7 @@
"test": "tsc && ava"
},
"devDependencies": {
"ava": "^1.4.1",
"ava": "2.1.0",
"typescript": "^3.4.5"
}
}

View File

@ -18,7 +18,7 @@
import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
import BridgeIDBObjectStore from "./BridgeIDBObjectStore";
import BridgeIDBRequest from "./BridgeIDBRequest";
import cmp from "./util/cmp";
import compareKeys from "./util/cmp";
import {
DataError,
InvalidAccessError,
@ -233,7 +233,7 @@ class BridgeIDBCursor {
if (key !== undefined) {
key = valueToKey(key);
const cmpResult = cmp(key, this._position);
const cmpResult = compareKeys(key, this._position);
if (
(cmpResult <= 0 &&

View File

@ -144,7 +144,7 @@ class BridgeIDBDatabase extends FakeEventTarget {
validateKeyPath(keyPath);
}
if (!Object.keys(this._schema.objectStores).includes(name)) {
if (Object.keys(this._schema.objectStores).includes(name)) {
throw new ConstraintError();
}
@ -156,7 +156,7 @@ class BridgeIDBDatabase extends FakeEventTarget {
this._schema = this._backend.getSchema(this._backendConnection);
return transaction.objectStore("name");
return transaction.objectStore(name);
}
public deleteObjectStore(name: string): void {
@ -214,6 +214,7 @@ class BridgeIDBDatabase extends FakeEventTarget {
const tx = new BridgeIDBTransaction(storeNames, mode, this, backendTransaction);
this._transactions.push(tx);
queueTask(() => tx._start());
return tx;
}

View File

@ -31,6 +31,7 @@ class BridgeIDBFactory {
public cmp = compareKeys;
private backend: Backend;
private connections: BridgeIDBDatabase[] = [];
static enableTracing: boolean = true;
public constructor(backend: Backend) {
this.backend = backend;
@ -165,7 +166,17 @@ class BridgeIDBFactory {
await transaction._waitDone();
// We don't explicitly exit the versionchange transaction,
// since this is already done by the BridgeIDBTransaction.
db._runningVersionchangeTransaction = false;
const event2 = new FakeEvent("success", {
bubbles: false,
cancelable: false,
});
event2.eventPath = [request];
request.dispatchEvent(event2);
}
this.connections.push(db);

View File

@ -47,6 +47,7 @@ import {
RecordGetRequest,
ResultLevel,
} from "./backend-interface";
import BridgeIDBFactory from "./BridgeIDBFactory";
// 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) {
if (BridgeIDBFactory.enableTracing) {
console.log(`TRACE: IDBObjectStore._store`);
}
if (this.transaction.mode === "readonly") {
throw new ReadOnlyError();
}

View File

@ -20,6 +20,7 @@ import queueTask from "./util/queueTask";
import openPromise from "./util/openPromise";
import { DatabaseTransaction, Backend } from "./backend-interface";
import { array } from "prop-types";
import BridgeIDBFactory from "./BridgeIDBFactory";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#transaction
class BridgeIDBTransaction extends FakeEventTarget {
@ -113,7 +114,6 @@ class BridgeIDBTransaction extends FakeEventTarget {
event.eventPath = [this.db];
this.dispatchEvent(event);
});
}
public abort() {
@ -169,9 +169,17 @@ class BridgeIDBTransaction extends FakeEventTarget {
return request;
}
/**
* Actually execute the scheduled work for this transaction.
*/
public async _start() {
if (BridgeIDBFactory.enableTracing) {
console.log(`TRACE: IDBTransaction._start, ${this._requests.length} queued`);
}
this._started = true;
console.log("beginning transaction");
if (!this._backendTransaction) {
this._backendTransaction = await this._backend.beginTransaction(
this.db._backendConnection,
@ -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
let operation;
let request;
@ -198,9 +208,10 @@ class BridgeIDBTransaction extends FakeEventTarget {
if (!request.source) {
// Special requests like indexes that just need to run some code, with error handling already built into
// operation
console.log("running operation without source");
await operation();
} else {
let defaultAction;
console.log("running operation with source");
let event;
try {
const result = await operation();
@ -216,7 +227,20 @@ class BridgeIDBTransaction extends FakeEventTarget {
bubbles: 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) {
if (BridgeIDBFactory.enableTracing) {
console.log("TRACING: error during operation: ", err);
}
request.readyState = "done";
request.result = undefined;
request.error = err;
@ -230,23 +254,17 @@ class BridgeIDBTransaction extends FakeEventTarget {
cancelable: true,
});
defaultAction = this._abort.bind(this, err.name);
}
try {
event.eventPath = [this.db, this];
request.dispatchEvent(event);
} catch (err) {
if (this._state !== "committing") {
this._abort("AbortError");
try {
event.eventPath = [this.db, this];
request.dispatchEvent(event);
} catch (err) {
if (this._state !== "committing") {
this._abort("AbortError");
}
throw err;
}
throw err;
}
// Default action of event
if (!event.canceled) {
if (defaultAction) {
defaultAction();
if (!event.canceled) {
this._abort(err.name);
}
}
}
@ -261,13 +279,23 @@ class BridgeIDBTransaction extends FakeEventTarget {
return;
}
// Check if transaction complete event needs to be fired
if (this._state !== "finished") {
// Either aborted or committed already
if (this._state !== "finished" && this._state !== "committing") {
if (BridgeIDBFactory.enableTracing) {
console.log("finishing transaction");
}
this._state = "committing";
await this._backend.commit(this._backendTransaction);
this._state = "finished";
if (!this.error) {
if (BridgeIDBFactory.enableTracing) {
console.log("dispatching 'complete' event");
}
const event = new FakeEvent("complete");
event.eventPath = [this, this.db];
this.dispatchEvent(event);
}
@ -287,6 +315,7 @@ class BridgeIDBTransaction extends FakeEventTarget {
}
this._state = "committing";
// We now just wait for auto-commit ...
}
public toString() {

View File

@ -1,31 +1,126 @@
import test from 'ava';
import MemoryBackend from './MemoryBackend';
import BridgeIDBFactory from './BridgeIDBFactory';
import test from "ava";
import MemoryBackend from "./MemoryBackend";
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 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 store = db.createObjectStore("books", { keyPath: "isbn" });
const titleIndex = store.createIndex("by_title", "title", { unique: true });
const authorIndex = store.createIndex("by_author", "author");
// Populate with initial data.
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});
};
request.onsuccess = () => {
t.end();
};
request.onerror = () => {
t.fail();
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 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();
});

View File

@ -5,14 +5,26 @@ import {
Schema,
RecordStoreRequest,
IndexProperties,
RecordGetRequest,
RecordGetResponse,
ResultLevel,
} from "./backend-interface";
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 BridgeIDBFactory from "./BridgeIDBFactory";
import compareKeys from "./util/cmp";
import extractKey from "./util/extractKey";
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 {
Disconnected = 0,
@ -25,8 +37,8 @@ enum TransactionLevel {
interface ObjectStore {
originalName: string;
modifiedName: string | undefined;
originalData: ISortedMapF;
modifiedData: ISortedMapF | undefined;
originalData: ISortedMapF<Key, ObjectStoreRecord>;
modifiedData: ISortedMapF<Key, ObjectStoreRecord> | undefined;
deleted: boolean;
originalKeyGenerator: number;
modifiedKeyGenerator: number | undefined;
@ -35,8 +47,8 @@ interface ObjectStore {
interface Index {
originalName: string;
modifiedName: string | undefined;
originalData: ISortedMapF;
modifiedData: ISortedMapF | undefined;
originalData: ISortedMapF<Key, IndexRecord>;
modifiedData: ISortedMapF<Key, IndexRecord> | undefined;
deleted: boolean;
}
@ -74,28 +86,77 @@ interface Connection {
indexMap: { [currentName: string]: Index };
}
interface IndexRecord {
indexKey: Key;
primaryKeys: Key[];
}
interface ObjectStoreRecord {
primaryKey: Key;
value: Value;
}
class AsyncCondition {
wait(): Promise<void> {
throw Error("not implemented");
_waitPromise: Promise<void>;
_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 insertIntoIndex(
index: Index,
value: Value,
indexProperties: IndexProperties,
) {
if (indexProperties.multiEntry) {
} else {
const key = extractKey(value, indexProperties.keyPath);
function furthestKey(forward: boolean, key1: Key | undefined, key2: Key | undefined) {
if (key1 === undefined) {
return key2;
}
if (key2 === undefined) {
return key1;
}
const cmpResult = compareKeys(key1, key2);
if (cmpResult === 0) {
// Same result
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();
enableTracing: boolean = true;
async getDatabases(): Promise<{ name: string; version: number }[]> {
if (this.enableTracing) {
console.log("TRACING: getDatabase");
}
const dbList = [];
for (const name in this.databases) {
dbList.push({
@ -141,6 +207,9 @@ export class MemoryBackend implements Backend {
}
async deleteDatabase(tx: DatabaseTransaction, name: string): Promise<void> {
if (this.enableTracing) {
console.log("TRACING: deleteDatabase");
}
const myConn = this.connectionsByTransaction[tx.transactionCookie];
if (!myConn) {
throw Error("no connection associated with transaction");
@ -162,6 +231,9 @@ export class MemoryBackend implements Backend {
}
async connectDatabase(name: string): Promise<DatabaseConnection> {
if (this.enableTracing) {
console.log(`TRACING: connectDatabase(${name})`);
}
const connectionId = this.connectionIdCounter++;
const connectionCookie = `connection-${connectionId}`;
@ -193,6 +265,16 @@ export class MemoryBackend implements Backend {
database.txLevel = TransactionLevel.Connected;
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 };
}
@ -201,6 +283,9 @@ export class MemoryBackend implements Backend {
objectStores: string[],
mode: import("./util/types").TransactionMode,
): Promise<DatabaseTransaction> {
if (this.enableTracing) {
console.log(`TRACING: beginTransaction`);
}
const transactionCookie = `tx-${this.transactionIdCounter++}`;
const myConn = this.connections[conn.connectionCookie];
if (!myConn) {
@ -212,6 +297,9 @@ export class MemoryBackend implements Backend {
}
while (myDb.txLevel !== TransactionLevel.Connected) {
if (this.enableTracing) {
console.log(`TRACING: beginTransaction -- waiting for others to close`);
}
await this.transactionDoneCond.wait();
}
@ -232,6 +320,9 @@ export class MemoryBackend implements Backend {
conn: DatabaseConnection,
newVersion: number,
): Promise<DatabaseTransaction> {
if (this.enableTracing) {
console.log(`TRACING: enterVersionChange`);
}
const transactionCookie = `tx-vc-${this.transactionIdCounter++}`;
const myConn = this.connections[conn.connectionCookie];
if (!myConn) {
@ -254,6 +345,9 @@ export class MemoryBackend implements Backend {
}
async close(conn: DatabaseConnection): Promise<void> {
if (this.enableTracing) {
console.log(`TRACING: close`);
}
const myConn = this.connections[conn.connectionCookie];
if (!myConn) {
throw Error("connection not found - already closed?");
@ -266,9 +360,13 @@ export class MemoryBackend implements Backend {
myDb.txLevel = TransactionLevel.Disconnected;
}
delete this.connections[conn.connectionCookie];
this.disconnectCond.trigger();
}
getSchema(dbConn: DatabaseConnection): Schema {
if (this.enableTracing) {
console.log(`TRACING: getSchema`);
}
const myConn = this.connections[dbConn.connectionCookie];
if (!myConn) {
throw Error("unknown connection");
@ -288,7 +386,10 @@ export class MemoryBackend implements Backend {
oldName: string,
newName: string,
): void {
const myConn = this.connections[btx.transactionCookie];
if (this.enableTracing) {
console.log(`TRACING: renameIndex(?, ${oldName}, ${newName})`);
}
const myConn = this.connectionsByTransaction[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
@ -331,6 +432,9 @@ export class MemoryBackend implements Backend {
}
deleteIndex(btx: DatabaseTransaction, indexName: string): void {
if (this.enableTracing) {
console.log(`TRACING: deleteIndex(${indexName})`);
}
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
@ -365,6 +469,9 @@ export class MemoryBackend implements Backend {
}
deleteObjectStore(btx: DatabaseTransaction, name: string): void {
if (this.enableTracing) {
console.log(`TRACING: deleteObjectStore(${name})`);
}
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
@ -403,6 +510,10 @@ export class MemoryBackend implements Backend {
oldName: string,
newName: string,
): void {
if (this.enableTracing) {
console.log(`TRACING: renameObjectStore(?, ${oldName}, ${newName})`);
}
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
@ -441,7 +552,12 @@ export class MemoryBackend implements Backend {
keyPath: string | string[] | null,
autoIncrement: boolean,
): 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) {
throw Error("unknown connection");
}
@ -482,7 +598,10 @@ export class MemoryBackend implements Backend {
multiEntry: boolean,
unique: boolean,
): void {
const myConn = this.connections[btx.transactionCookie];
if (this.enableTracing) {
console.log(`TRACING: createIndex(${indexName})`);
}
const myConn = this.connectionsByTransaction[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
@ -526,7 +645,10 @@ export class MemoryBackend implements Backend {
objectStoreName: string,
range: import("./BridgeIDBKeyRange").default,
): Promise<void> {
const myConn = this.connections[btx.transactionCookie];
if (this.enableTracing) {
console.log(`TRACING: deleteRecord`);
}
const myConn = this.connectionsByTransaction[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
@ -537,13 +659,17 @@ export class MemoryBackend implements Backend {
if (db.txLevel < TransactionLevel.Write) {
throw Error("only allowed in write transaction");
}
throw Error("not implemented");
}
async getRecords(
btx: DatabaseTransaction,
req: import("./backend-interface").RecordGetRequest,
): Promise<import("./backend-interface").RecordGetResponse> {
const myConn = this.connections[btx.transactionCookie];
req: RecordGetRequest,
): Promise<RecordGetResponse> {
if (this.enableTracing) {
console.log(`TRACING: getRecords`);
}
const myConn = this.connectionsByTransaction[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
@ -551,17 +677,242 @@ export class MemoryBackend implements Backend {
if (!db) {
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("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(
btx: DatabaseTransaction,
storeReq: RecordStoreRequest,
): Promise<void> {
const myConn = this.connections[btx.transactionCookie];
if (this.enableTracing) {
console.log(`TRACING: storeRecord`);
}
const myConn = this.connectionsByTransaction[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
@ -578,7 +929,7 @@ export class MemoryBackend implements Backend {
const objectStore = myConn.objectStoreMap[storeReq.objectStoreName];
const storeKeyResult: StoreKeyResult = getStoreKey(
const storeKeyResult: StoreKeyResult = makeStoreKeyValue(
storeReq.value,
storeReq.key,
objectStore.modifiedKeyGenerator || objectStore.originalKeyGenerator,
@ -607,12 +958,54 @@ export class MemoryBackend implements Backend {
throw Error("index referenced by object store does not exist");
}
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> {
const myConn = this.connections[btx.transactionCookie];
if (this.enableTracing) {
console.log(`TRACING: rollback`);
}
const myConn = this.connectionsByTransaction[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
@ -642,10 +1035,15 @@ export class MemoryBackend implements Backend {
objectStore.modifiedName = undefined;
objectStore.modifiedKeyGenerator = undefined;
}
delete this.connectionsByTransaction[btx.transactionCookie];
this.transactionDoneCond.trigger();
}
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) {
throw Error("unknown connection");
}
@ -656,6 +1054,41 @@ export class MemoryBackend implements Backend {
if (db.txLevel < TransactionLevel.Read) {
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();
}
}

View File

@ -45,18 +45,47 @@ export interface RecordGetRequest {
direction: BridgeIDBCursorDirection;
objectStoreName: string;
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;
/**
* 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;
/**
* Last position in terms of the object store 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;
/**
* If specified, the primary key of the results must be greater
* or equal to advancePrimaryKey.
*/
advancePrimaryKey?: Key;
/**
* Maximum number of resuts to return.
* If -1, return all available results
*/
limit: number;
resultLevel: ResultLevel;
}
export interface RecordGetResponse {
values: Value[] | undefined;
keys: Key[] | undefined;
indexKeys: Key[] | undefined;
primaryKeys: Key[] | undefined;
count: number;
}

View File

@ -14,164 +14,172 @@
permissions and limitations under the License.
*/
import { InvalidStateError } from "./errors";
import FakeEvent from "./FakeEvent";
import { EventCallback, EventType } from "./types";
type EventTypeProp =
| "onabort"
| "onblocked"
| "oncomplete"
| "onerror"
| "onsuccess"
| "onupgradeneeded"
| "onversionchange";
| "onabort"
| "onblocked"
| "oncomplete"
| "onerror"
| "onsuccess"
| "onupgradeneeded"
| "onversionchange";
interface Listener {
callback: EventCallback;
capture: boolean;
type: EventType;
callback: EventCallback;
capture: boolean;
type: EventType;
}
const stopped = (event: FakeEvent, listener: Listener) => {
return (
event.immediatePropagationStopped ||
(event.eventPhase === event.CAPTURING_PHASE &&
listener.capture === false) ||
(event.eventPhase === event.BUBBLING_PHASE && listener.capture === true)
);
return (
event.immediatePropagationStopped ||
(event.eventPhase === event.CAPTURING_PHASE &&
listener.capture === false) ||
(event.eventPhase === event.BUBBLING_PHASE && listener.capture === true)
);
};
// http://www.w3.org/TR/dom/#concept-event-listener-invoke
const invokeEventListeners = (event: FakeEvent, obj: FakeEventTarget) => {
event.currentTarget = obj;
event.currentTarget = obj;
// 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
// our place.
for (const listener of obj.listeners.slice()) {
if (event.type !== listener.type || stopped(event, listener)) {
continue;
}
// @ts-ignore
listener.callback.call(event.currentTarget, event);
// 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
// our place.
for (const listener of obj.listeners.slice()) {
if (event.type !== listener.type || stopped(event, listener)) {
continue;
}
const typeToProp: { [key in EventType]: EventTypeProp } = {
abort: "onabort",
blocked: "onblocked",
complete: "oncomplete",
error: "onerror",
success: "onsuccess",
upgradeneeded: "onupgradeneeded",
versionchange: "onversionchange",
console.log(`invoking ${event.type} event listener`, listener);
// @ts-ignore
listener.callback.call(event.currentTarget, event);
}
const typeToProp: { [key in EventType]: EventTypeProp } = {
abort: "onabort",
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 (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,
};
if (!stopped(event, listener)) {
// @ts-ignore
listener.callback.call(event.currentTarget, event);
}
if (!stopped(event, listener)) {
console.log(`invoking on${event.type} event listener`, listener);
// @ts-ignore
listener.callback.call(event.currentTarget, event);
}
}
};
abstract class FakeEventTarget {
public readonly listeners: Listener[] = [];
public readonly listeners: Listener[] = [];
// These will be overridden in individual subclasses and made not readonly
public readonly onabort: EventCallback | null | undefined;
public readonly onblocked: EventCallback | null | undefined;
public readonly oncomplete: EventCallback | null | undefined;
public readonly onerror: EventCallback | null | undefined;
public readonly onsuccess: EventCallback | null | undefined;
public readonly onupgradeneeded: EventCallback | null | undefined;
public readonly onversionchange: EventCallback | null | undefined;
// These will be overridden in individual subclasses and made not readonly
public readonly onabort: EventCallback | null | undefined;
public readonly onblocked: EventCallback | null | undefined;
public readonly oncomplete: EventCallback | null | undefined;
public readonly onerror: EventCallback | null | undefined;
public readonly onsuccess: EventCallback | null | undefined;
public readonly onupgradeneeded: EventCallback | null | undefined;
public readonly onversionchange: EventCallback | null | undefined;
public addEventListener(
type: EventType,
callback: EventCallback,
capture = false,
) {
this.listeners.push({
callback,
capture,
type,
});
static enableTracing: boolean = true;
public addEventListener(
type: EventType,
callback: EventCallback,
capture = false,
) {
this.listeners.push({
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(
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);
event.eventPhase = event.AT_TARGET;
if (!event.propagationStopped) {
invokeEventListeners(event, event.target);
}
// 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;
for (const obj of event.eventPath) {
if (!event.propagationStopped) {
invokeEventListeners(event, obj);
}
}
event.eventPhase = event.AT_TARGET;
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, 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;

View 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);
});
});

View 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;

View 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);
});

View File

@ -63,10 +63,14 @@ export function makeStoreKeyValue(
updatedKeyGenerator = currentKeyGenerator + 1;
} else if (typeof maybeInlineKey === "number") {
key = maybeInlineKey;
updatedKeyGenerator = maybeInlineKey;
if (maybeInlineKey >= currentKeyGenerator) {
updatedKeyGenerator = maybeInlineKey + 1;
} else {
updatedKeyGenerator = currentKeyGenerator;
}
} else {
key = maybeInlineKey;
updatedKeyGenerator = currentKeyGenerator + 1;
updatedKeyGenerator = currentKeyGenerator;
}
return {
key: key,
@ -84,9 +88,17 @@ export function makeStoreKeyValue(
};
}
} else {
// (no, no, yes)
// (no, no, no)
throw new DataError();
if (autoIncrement) {
// (no, no, yes)
return {
key: currentKeyGenerator,
value: value,
updatedKeyGenerator: currentKeyGenerator + 1,
}
} else {
// (no, no, no)
throw new DataError();
}
}
}
}

View File

@ -8,7 +8,8 @@
"outDir": "build",
"noEmitOnError": true,
"strict": true,
"incremental": true
"incremental": true,
"sourceMap": true
},
"include": ["src/**/*"]
}

File diff suppressed because it is too large Load Diff