idb: tests working
This commit is contained in:
parent
2ee9431f1b
commit
a4e4125cca
@ -13,7 +13,7 @@
|
||||
"test": "tsc && ava"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^1.4.1",
|
||||
"ava": "2.1.0",
|
||||
"typescript": "^3.4.5"
|
||||
}
|
||||
}
|
||||
|
@ -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 &&
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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,9 +254,6 @@ class BridgeIDBTransaction extends FakeEventTarget {
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
defaultAction = this._abort.bind(this, err.name);
|
||||
}
|
||||
|
||||
try {
|
||||
event.eventPath = [this.db, this];
|
||||
request.dispatchEvent(event);
|
||||
@ -242,11 +263,8 @@ class BridgeIDBTransaction extends FakeEventTarget {
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Default action of event
|
||||
if (!event.canceled) {
|
||||
if (defaultAction) {
|
||||
defaultAction();
|
||||
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() {
|
||||
|
@ -1,9 +1,36 @@
|
||||
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);
|
||||
|
||||
@ -20,12 +47,80 @@ test.cb("basics", (t) => {
|
||||
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,
|
||||
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 {
|
||||
_waitPromise: Promise<void>;
|
||||
_resolveWaitPromise: () => void;
|
||||
constructor() {
|
||||
const op = openPromise<void>();
|
||||
this._waitPromise = op.promise;
|
||||
this._resolveWaitPromise = op.resolve;
|
||||
}
|
||||
|
||||
wait(): Promise<void> {
|
||||
throw Error("not implemented");
|
||||
return this._waitPromise;
|
||||
}
|
||||
|
||||
trigger(): void {}
|
||||
trigger(): void {
|
||||
this._resolveWaitPromise();
|
||||
const op = openPromise<void>();
|
||||
this._waitPromise = op.promise;
|
||||
this._resolveWaitPromise = op.resolve;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function insertIntoIndex(
|
||||
index: Index,
|
||||
value: Value,
|
||||
indexProperties: IndexProperties,
|
||||
function nextStoreKey<T>(
|
||||
forward: boolean,
|
||||
data: ISortedMapF<Key, ObjectStoreRecord>,
|
||||
k: Key | undefined,
|
||||
) {
|
||||
if (indexProperties.multiEntry) {
|
||||
|
||||
} else {
|
||||
const key = extractKey(value, indexProperties.keyPath);
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
import { InvalidStateError } from "./errors";
|
||||
import FakeEvent from "./FakeEvent";
|
||||
import { EventCallback, EventType } from "./types";
|
||||
@ -55,6 +54,7 @@ const invokeEventListeners = (event: FakeEvent, obj: FakeEventTarget) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`invoking ${event.type} event listener`, listener);
|
||||
// @ts-ignore
|
||||
listener.callback.call(event.currentTarget, event);
|
||||
}
|
||||
@ -81,6 +81,7 @@ const invokeEventListeners = (event: FakeEvent, obj: FakeEventTarget) => {
|
||||
type: event.type,
|
||||
};
|
||||
if (!stopped(event, listener)) {
|
||||
console.log(`invoking on${event.type} event listener`, listener);
|
||||
// @ts-ignore
|
||||
listener.callback.call(event.currentTarget, event);
|
||||
}
|
||||
@ -99,6 +100,8 @@ abstract class FakeEventTarget {
|
||||
public readonly onupgradeneeded: EventCallback | null | undefined;
|
||||
public readonly onversionchange: EventCallback | null | undefined;
|
||||
|
||||
static enableTracing: boolean = true;
|
||||
|
||||
public addEventListener(
|
||||
type: EventType,
|
||||
callback: EventCallback,
|
||||
@ -139,6 +142,11 @@ abstract class FakeEventTarget {
|
||||
// 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);
|
||||
|
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;
|
||||
} 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 {
|
||||
if (autoIncrement) {
|
||||
// (no, no, yes)
|
||||
return {
|
||||
key: currentKeyGenerator,
|
||||
value: value,
|
||||
updatedKeyGenerator: currentKeyGenerator + 1,
|
||||
}
|
||||
} else {
|
||||
// (no, no, no)
|
||||
throw new DataError();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user