This commit is contained in:
Florian Dold 2019-06-15 22:44:54 +02:00
parent 65eb8b96f8
commit 2ee9431f1b
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
38 changed files with 9467 additions and 0 deletions

View File

@ -0,0 +1,6 @@
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": false
}

View File

@ -0,0 +1,18 @@
# idb-bridge
The `idb-bridge` package implements the IndexedDB API with multiple backends.
Currently available backends are:
* sqlite: A SQLite3 database. Can be backed by a file or in memory.
* memdb: An unoptimized in-memory storage backend. Useful for environments
that do not have sqlite.
## Known Issues
IndexedDB assumes that after a database has been opened, the set of object stores and indices does not change,
even when there is no transaction active. We cannot guarantee this with SQLite.
## Acknowledgements
This library is based on the fakeIndexedDB library
(https://github.com/dumbmatter/fakeIndexedDB).

View File

@ -0,0 +1,19 @@
{
"name": "idb-bridge",
"version": "0.0.1",
"description": "IndexedDB implementation that uses SQLite3 as storage",
"main": "index.js",
"author": "Florian Dold",
"license": "AGPL-3.0-or-later",
"private": false,
"dependencies": {
"sqlite3": "^4.0.8"
},
"scripts": {
"test": "tsc && ava"
},
"devDependencies": {
"ava": "^1.4.1",
"typescript": "^3.4.5"
}
}

View File

@ -0,0 +1,315 @@
/*
Copyright 2019 Florian Dold
Copyright 2017 Jeremy Scheff
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
import BridgeIDBObjectStore from "./BridgeIDBObjectStore";
import BridgeIDBRequest from "./BridgeIDBRequest";
import cmp from "./util/cmp";
import {
DataError,
InvalidAccessError,
InvalidStateError,
ReadOnlyError,
TransactionInactiveError,
} from "./util/errors";
import extractKey from "./util/extractKey";
import structuredClone from "./util/structuredClone";
import {
CursorRange,
CursorSource,
Key,
Value,
BridgeIDBCursorDirection,
} from "./util/types";
import valueToKey from "./util/valueToKey";
import {
RecordGetRequest,
ResultLevel,
Backend,
DatabaseTransaction,
RecordStoreRequest,
} from "./backend-interface";
/**
* http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#cursor
*/
class BridgeIDBCursor {
_request: BridgeIDBRequest | undefined;
private _gotValue: boolean = false;
private _range: CursorRange;
private _position = undefined; // Key of previously returned record
private _objectStorePosition = undefined;
private _keyOnly: boolean = false;
private _source: CursorSource;
private _direction: BridgeIDBCursorDirection;
private _key = undefined;
private _primaryKey: Key | undefined = undefined;
private _indexName: string | undefined;
private _objectStoreName: string;
constructor(
source: CursorSource,
objectStoreName: string,
indexName: string | undefined,
range: CursorRange,
direction: BridgeIDBCursorDirection,
request: BridgeIDBRequest,
keyOnly: boolean,
) {
this._indexName = indexName;
this._objectStoreName = objectStoreName;
this._range = range;
this._source = source;
this._direction = direction;
this._request = request;
this._keyOnly = keyOnly;
}
get _effectiveObjectStore(): BridgeIDBObjectStore {
if (this.source instanceof BridgeIDBObjectStore) {
return this.source;
}
return this.source.objectStore;
}
get _backend(): Backend {
return this._source._backend;
}
// Read only properties
get source() {
return this._source;
}
set source(val) {
/* For babel */
}
get direction() {
return this._direction;
}
set direction(val) {
/* For babel */
}
get key() {
return this._key;
}
set key(val) {
/* For babel */
}
get primaryKey() {
return this._primaryKey;
}
set primaryKey(val) {
/* For babel */
}
/**
* https://w3c.github.io/IndexedDB/#iterate-a-cursor
*/
async _iterate(key?: Key, primaryKey?: Key): Promise<any> {
const recordGetRequest: RecordGetRequest = {
direction: this.direction,
indexName: this._indexName,
lastIndexPosition: this._position,
lastObjectStorePosition: this._objectStorePosition,
limit: 1,
range: this._range,
objectStoreName: this._objectStoreName,
advanceIndexKey: key,
advancePrimaryKey: primaryKey,
resultLevel: this._keyOnly ? ResultLevel.OnlyKeys : ResultLevel.Full,
};
const { btx } = this.source._confirmActiveTransaction();
let response = await this._backend.getRecords(
btx,
recordGetRequest,
);
if (response.count === 0) {
return null;
}
return this;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-update-IDBRequest-any-value
public update(value: Value) {
if (value === undefined) {
throw new TypeError();
}
const transaction = this._effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (transaction.mode === "readonly") {
throw new ReadOnlyError();
}
if (this._effectiveObjectStore._deleted) {
throw new InvalidStateError();
}
if (
!(this.source instanceof BridgeIDBObjectStore) &&
this.source._deleted
) {
throw new InvalidStateError();
}
if (!this._gotValue || !this.hasOwnProperty("value")) {
throw new InvalidStateError();
}
const storeReq: RecordStoreRequest = {
overwrite: true,
key: this._primaryKey,
value: value,
objectStoreName: this._objectStoreName,
};
const operation = async () => {
const { btx } = this.source._confirmActiveTransaction();
this._backend.storeRecord(btx, storeReq);
};
return transaction._execRequestAsync({
operation,
source: this,
});
}
/**
* http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-advance-void-unsigned-long-count
*/
public advance(count: number) {
throw Error("not implemented");
}
/**
* http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-continue-void-any-key
*/
public continue(key?: Key) {
const transaction = this._effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (this._effectiveObjectStore._deleted) {
throw new InvalidStateError();
}
if (
!(this.source instanceof BridgeIDBObjectStore) &&
this.source._deleted
) {
throw new InvalidStateError();
}
if (!this._gotValue) {
throw new InvalidStateError();
}
if (key !== undefined) {
key = valueToKey(key);
const cmpResult = cmp(key, this._position);
if (
(cmpResult <= 0 &&
(this.direction === "next" || this.direction === "nextunique")) ||
(cmpResult >= 0 &&
(this.direction === "prev" || this.direction === "prevunique"))
) {
throw new DataError();
}
}
if (this._request) {
this._request.readyState = "pending";
}
const operation = async () => {
this._iterate(key);
};
transaction._execRequestAsync({
operation,
request: this._request,
source: this.source,
});
this._gotValue = false;
}
// https://w3c.github.io/IndexedDB/#dom-idbcursor-continueprimarykey
public continuePrimaryKey(key: Key, primaryKey: Key) {
throw Error("not implemented");
}
public delete() {
const transaction = this._effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (transaction.mode === "readonly") {
throw new ReadOnlyError();
}
if (this._effectiveObjectStore._deleted) {
throw new InvalidStateError();
}
if (
!(this.source instanceof BridgeIDBObjectStore) &&
this.source._deleted
) {
throw new InvalidStateError();
}
if (!this._gotValue || !this.hasOwnProperty("value")) {
throw new InvalidStateError();
}
const operation = async () => {
const { btx } = this.source._confirmActiveTransaction();
this._backend.deleteRecord(
btx,
this._objectStoreName,
BridgeIDBKeyRange._valueToKeyRange(this._primaryKey),
);
};
return transaction._execRequestAsync({
operation,
source: this,
});
}
public toString() {
return "[object IDBCursor]";
}
}
export default BridgeIDBCursor;

View File

@ -0,0 +1,44 @@
/*
Copyright 2017 Jeremy Scheff
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
import BridgeIDBCursor from "./BridgeIDBCursor";
import {
CursorRange,
CursorSource,
BridgeIDBCursorDirection,
Value,
} from "./util/types";
class FDBCursorWithValue extends BridgeIDBCursor {
public value: Value = undefined;
constructor(
source: CursorSource,
objectStoreName: string,
indexName: string | undefined,
range: CursorRange,
direction: BridgeIDBCursorDirection,
request?: any,
) {
super(source, objectStoreName, indexName, range, direction, request, true);
}
public toString() {
return "[object IDBCursorWithValue]";
}
}
export default FDBCursorWithValue;

View File

@ -0,0 +1,239 @@
/*
* Copyright 2017 Jeremy Scheff
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import BridgeIDBTransaction from "./BridgeIDBTransaction";
import {
ConstraintError,
InvalidAccessError,
InvalidStateError,
NotFoundError,
TransactionInactiveError,
} from "./util/errors";
import fakeDOMStringList from "./util/fakeDOMStringList";
import FakeEventTarget from "./util/FakeEventTarget";
import { FakeDOMStringList, KeyPath, TransactionMode } from "./util/types";
import validateKeyPath from "./util/validateKeyPath";
import queueTask from "./util/queueTask";
import {
Backend,
DatabaseConnection,
Schema,
DatabaseTransaction,
} from "./backend-interface";
/**
* Ensure that an active version change transaction is currently running.
*/
const confirmActiveVersionchangeTransaction = (database: BridgeIDBDatabase) => {
if (!database._runningVersionchangeTransaction) {
throw new InvalidStateError();
}
// Find the latest versionchange transaction
const transactions = database._transactions.filter(
(tx: BridgeIDBTransaction) => {
return tx.mode === "versionchange";
},
);
const transaction = transactions[transactions.length - 1];
if (!transaction || transaction._state === "finished") {
throw new InvalidStateError();
}
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
return transaction;
};
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#database-interface
class BridgeIDBDatabase extends FakeEventTarget {
_closePending = false;
_closed = false;
_runningVersionchangeTransaction = false;
_transactions: Array<BridgeIDBTransaction> = [];
_backendConnection: DatabaseConnection;
_backend: Backend;
_schema: Schema;
get name(): string {
return this._schema.databaseName;
}
get version(): number {
return this._schema.databaseVersion;
}
get objectStoreNames(): FakeDOMStringList {
return fakeDOMStringList(Object.keys(this._schema.objectStores)).sort();
}
/**
* http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#database-closing-steps
*/
_closeConnection() {
this._closePending = true;
const transactionsComplete = this._transactions.every(
(transaction: BridgeIDBTransaction) => {
return transaction._state === "finished";
},
);
if (transactionsComplete) {
this._closed = true;
this._backend.close(this._backendConnection);
} else {
queueTask(() => {
this._closeConnection();
});
}
}
constructor(backend: Backend, backendConnection: DatabaseConnection) {
super();
this._schema = backend.getSchema(backendConnection);
this._backend = backend;
this._backendConnection = backendConnection;
}
// http://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore
public createObjectStore(
name: string,
options: { autoIncrement?: boolean; keyPath?: KeyPath } | null = {},
) {
if (name === undefined) {
throw new TypeError();
}
const transaction = confirmActiveVersionchangeTransaction(this);
const backendTx = transaction._backendTransaction;
if (!backendTx) {
throw Error("invariant violated");
}
const keyPath =
options !== null && options.keyPath !== undefined
? options.keyPath
: null;
const autoIncrement =
options !== null && options.autoIncrement !== undefined
? options.autoIncrement
: false;
if (keyPath !== null) {
validateKeyPath(keyPath);
}
if (!Object.keys(this._schema.objectStores).includes(name)) {
throw new ConstraintError();
}
if (autoIncrement && (keyPath === "" || Array.isArray(keyPath))) {
throw new InvalidAccessError();
}
transaction._backend.createObjectStore(backendTx, name, keyPath, autoIncrement);
this._schema = this._backend.getSchema(this._backendConnection);
return transaction.objectStore("name");
}
public deleteObjectStore(name: string): void {
if (name === undefined) {
throw new TypeError();
}
const transaction = confirmActiveVersionchangeTransaction(this);
transaction._objectStoresCache.delete(name);
}
public _internalTransaction(
storeNames: string | string[],
mode?: TransactionMode,
backendTransaction?: DatabaseTransaction,
): BridgeIDBTransaction {
mode = mode !== undefined ? mode : "readonly";
if (
mode !== "readonly" &&
mode !== "readwrite" &&
mode !== "versionchange"
) {
throw new TypeError("Invalid mode: " + mode);
}
const hasActiveVersionchange = this._transactions.some(
(transaction: BridgeIDBTransaction) => {
return (
transaction._state === "active" &&
transaction.mode === "versionchange" &&
transaction.db === this
);
},
);
if (hasActiveVersionchange) {
throw new InvalidStateError();
}
if (this._closePending) {
throw new InvalidStateError();
}
if (!Array.isArray(storeNames)) {
storeNames = [storeNames];
}
if (storeNames.length === 0 && mode !== "versionchange") {
throw new InvalidAccessError();
}
for (const storeName of storeNames) {
if (this.objectStoreNames.indexOf(storeName) < 0) {
throw new NotFoundError(
"No objectStore named " + storeName + " in this database",
);
}
}
const tx = new BridgeIDBTransaction(storeNames, mode, this, backendTransaction);
this._transactions.push(tx);
return tx;
}
public transaction(
storeNames: string | string[],
mode?: TransactionMode,
): BridgeIDBTransaction {
if (mode === "versionchange") {
throw new TypeError("Invalid mode: " + mode);
}
return this._internalTransaction(storeNames, mode);
}
public close() {
this._closeConnection();
}
public toString() {
return "[object IDBDatabase]";
}
}
export default BridgeIDBDatabase;

View File

@ -0,0 +1,192 @@
/*
* Copyright 2019 Florian Dold
* Copyright 2017 Jeremy Scheff
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import BridgeIDBDatabase from "./BridgeIDBDatabase";
import BridgeIDBOpenDBRequest from "./BridgeIDBOpenDBRequest";
import BridgeIDBVersionChangeEvent from "./BridgeIDBVersionChangeEvent";
import compareKeys from "./util/cmp";
import enforceRange from "./util/enforceRange";
import { AbortError, VersionError } from "./util/errors";
import FakeEvent from "./util/FakeEvent";
import { Backend, DatabaseConnection } from "./backend-interface";
import queueTask from "./util/queueTask";
type DatabaseList = Array<{ name: string; version: number }>;
class BridgeIDBFactory {
public cmp = compareKeys;
private backend: Backend;
private connections: BridgeIDBDatabase[] = [];
public constructor(backend: Backend) {
this.backend = backend;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBFactory-deleteDatabase-IDBOpenDBRequest-DOMString-name
public deleteDatabase(name: string): BridgeIDBOpenDBRequest {
const request = new BridgeIDBOpenDBRequest();
request.source = null;
queueTask(async () => {
const databases = await this.backend.getDatabases();
const dbInfo = databases.find((x) => x.name == name);
if (!dbInfo) {
// Database already doesn't exist, success!
const event = new BridgeIDBVersionChangeEvent("success", {
newVersion: null,
oldVersion: 0,
});
request.dispatchEvent(event);
return;
}
const oldVersion = dbInfo.version;
try {
const dbconn = await this.backend.connectDatabase(name);
const backendTransaction = await this.backend.enterVersionChange(dbconn, 0);
await this.backend.deleteDatabase(backendTransaction, name);
await this.backend.commit(backendTransaction);
await this.backend.close(dbconn);
request.result = undefined;
request.readyState = "done";
const event2 = new BridgeIDBVersionChangeEvent("success", {
newVersion: null,
oldVersion,
});
request.dispatchEvent(event2);
} catch (err) {
request.error = new Error();
request.error.name = err.name;
request.readyState = "done";
const event = new FakeEvent("error", {
bubbles: true,
cancelable: true,
});
event.eventPath = [];
request.dispatchEvent(event);
}
});
return request;
}
// tslint:disable-next-line max-line-length
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBFactory-open-IDBOpenDBRequest-DOMString-name-unsigned-long-long-version
public open(name: string, version?: number) {
if (arguments.length > 1 && version !== undefined) {
// Based on spec, not sure why "MAX_SAFE_INTEGER" instead of "unsigned long long", but it's needed to pass
// tests
version = enforceRange(version, "MAX_SAFE_INTEGER");
}
if (version === 0) {
throw new TypeError();
}
const request = new BridgeIDBOpenDBRequest();
queueTask(async () => {
let dbconn: DatabaseConnection;
try {
dbconn = await this.backend.connectDatabase(name);
} catch (err) {
request._finishWithError(err);
return;
}
const schema = this.backend.getSchema(dbconn);
const existingVersion = schema.databaseVersion;
if (version === undefined) {
version = existingVersion !== 0 ? existingVersion : 1;
}
const requestedVersion = version;
if (existingVersion > requestedVersion) {
request._finishWithError(new VersionError());
return;
}
const db = new BridgeIDBDatabase(this.backend, dbconn);
if (existingVersion < requestedVersion) {
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-running-a-versionchange-transaction
for (const otherConn of this.connections) {
const event = new BridgeIDBVersionChangeEvent("versionchange", {
newVersion: version,
oldVersion: existingVersion,
});
otherConn.dispatchEvent(event);
}
if (this._anyOpen()) {
const event = new BridgeIDBVersionChangeEvent("blocked", {
newVersion: version,
oldVersion: existingVersion,
});
request.dispatchEvent(event);
}
const backendTransaction = await this.backend.enterVersionChange(dbconn, requestedVersion);
db._runningVersionchangeTransaction = true;
const transaction = db._internalTransaction(
[],
"versionchange",
backendTransaction,
);
const event = new BridgeIDBVersionChangeEvent("upgradeneeded", {
newVersion: version,
oldVersion: existingVersion,
});
request.result = db;
request.readyState = "done";
request.transaction = transaction;
request.dispatchEvent(event);
await transaction._waitDone();
db._runningVersionchangeTransaction = false;
}
this.connections.push(db);
return db;
});
return request;
}
// https://w3c.github.io/IndexedDB/#dom-idbfactory-databases
public databases(): Promise<DatabaseList> {
return this.backend.getDatabases();
}
public toString(): string {
return "[object IDBFactory]";
}
private _anyOpen(): boolean {
return this.connections.some(c => !c._closed && !c._closePending);
}
}
export default BridgeIDBFactory;

View File

@ -0,0 +1,316 @@
/*
Copyright 2017 Jeremy Scheff
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
import BridgeIDBCursor from "./BridgeIDBCursor";
import BridgeIDBCursorWithValue from "./BridgeIDBCursorWithValue";
import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
import BridgeIDBObjectStore from "./BridgeIDBObjectStore";
import BridgeIDBRequest from "./BridgeIDBRequest";
import enforceRange from "./util/enforceRange";
import {
ConstraintError,
InvalidStateError,
TransactionInactiveError,
} from "./util/errors";
import { BridgeIDBCursorDirection, Key, KeyPath } from "./util/types";
import valueToKey from "./util/valueToKey";
import BridgeIDBTransaction from "./BridgeIDBTransaction";
import {
Schema,
Backend,
DatabaseTransaction,
RecordGetRequest,
ResultLevel,
} from "./backend-interface";
const confirmActiveTransaction = (
index: BridgeIDBIndex,
): BridgeIDBTransaction => {
if (index._deleted || index.objectStore._deleted) {
throw new InvalidStateError();
}
if (index.objectStore.transaction._state !== "active") {
throw new TransactionInactiveError();
}
return index.objectStore.transaction;
};
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#idl-def-IDBIndex
class BridgeIDBIndex {
objectStore: BridgeIDBObjectStore;
get _schema(): Schema {
return this.objectStore.transaction.db._schema;
}
get keyPath(): KeyPath {
return this._schema.indexes[this._name].keyPath;
}
get multiEntry(): boolean {
return this._schema.indexes[this._name].multiEntry;
}
get unique(): boolean {
return this._schema.indexes[this._name].unique;
}
get _backend(): Backend {
return this.objectStore._backend;
}
_confirmActiveTransaction(): { btx: DatabaseTransaction } {
return this.objectStore._confirmActiveTransaction();
}
private _name: string;
public _deleted: boolean = false;
constructor(objectStore: BridgeIDBObjectStore, name: string) {
this._name = name;
this.objectStore = objectStore;
}
get name() {
return this._name;
}
// https://w3c.github.io/IndexedDB/#dom-idbindex-name
set name(name: any) {
const transaction = this.objectStore.transaction;
if (!transaction.db._runningVersionchangeTransaction) {
throw new InvalidStateError();
}
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
const { btx } = this._confirmActiveTransaction();
const oldName = this._name;
const newName = String(name);
if (newName === oldName) {
return;
}
this._backend.renameIndex(btx, oldName, newName);
if (this.objectStore.indexNames.indexOf(name) >= 0) {
throw new ConstraintError();
}
}
// tslint:disable-next-line max-line-length
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-openCursor-IDBRequest-any-range-IDBCursorDirection-direction
public openCursor(
range?: BridgeIDBKeyRange | Key | null | undefined,
direction: BridgeIDBCursorDirection = "next",
) {
confirmActiveTransaction(this);
if (range === null) {
range = undefined;
}
if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) {
range = BridgeIDBKeyRange.only(valueToKey(range));
}
const request = new BridgeIDBRequest();
request.source = this;
request.transaction = this.objectStore.transaction;
const cursor = new BridgeIDBCursorWithValue(
this,
this.objectStore.name,
this._name,
range,
direction,
request,
);
const operation = async () => {
return cursor._iterate();
};
return this.objectStore.transaction._execRequestAsync({
operation,
request,
source: this,
});
}
// tslint:disable-next-line max-line-length
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-openKeyCursor-IDBRequest-any-range-IDBCursorDirection-direction
public openKeyCursor(
range?: BridgeIDBKeyRange | Key | null | undefined,
direction: BridgeIDBCursorDirection = "next",
) {
confirmActiveTransaction(this);
if (range === null) {
range = undefined;
}
if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) {
range = BridgeIDBKeyRange.only(valueToKey(range));
}
const request = new BridgeIDBRequest();
request.source = this;
request.transaction = this.objectStore.transaction;
const cursor = new BridgeIDBCursor(
this,
this.objectStore.name,
this._name,
range,
direction,
request,
true,
);
return this.objectStore.transaction._execRequestAsync({
operation: cursor._iterate.bind(cursor),
request,
source: this,
});
}
public get(key: BridgeIDBKeyRange | Key) {
confirmActiveTransaction(this);
if (!(key instanceof BridgeIDBKeyRange)) {
key = BridgeIDBKeyRange._valueToKeyRange(key);
}
const getReq: RecordGetRequest = {
direction: "next",
indexName: this._name,
limit: 1,
range: key,
objectStoreName: this.objectStore._name,
resultLevel: ResultLevel.Full,
};
const operation = async () => {
const { btx } = this._confirmActiveTransaction();
const result = await this._backend.getRecords(btx, getReq);
if (result.count == 0) {
return undefined;
}
const values = result.values;
if (!values) {
throw Error("invariant violated");
}
return values[0];
};
return this.objectStore.transaction._execRequestAsync({
operation,
source: this,
});
}
// http://w3c.github.io/IndexedDB/#dom-idbindex-getall
public getAll(query?: BridgeIDBKeyRange | Key, count?: number) {
throw Error("not implemented");
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-getKey-IDBRequest-any-key
public getKey(key: BridgeIDBKeyRange | Key) {
confirmActiveTransaction(this);
if (!(key instanceof BridgeIDBKeyRange)) {
key = BridgeIDBKeyRange._valueToKeyRange(key);
}
const getReq: RecordGetRequest = {
direction: "next",
indexName: this._name,
limit: 1,
range: key,
objectStoreName: this.objectStore._name,
resultLevel: ResultLevel.OnlyKeys,
};
const operation = async () => {
const { btx } = this._confirmActiveTransaction();
const result = await this._backend.getRecords(btx, getReq);
if (result.count == 0) {
return undefined;
}
const primaryKeys = result.primaryKeys;
if (!primaryKeys) {
throw Error("invariant violated");
}
return primaryKeys[0];
};
return this.objectStore.transaction._execRequestAsync({
operation,
source: this,
});
}
// http://w3c.github.io/IndexedDB/#dom-idbindex-getallkeys
public getAllKeys(query?: BridgeIDBKeyRange | Key, count?: number) {
throw Error("not implemented");
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-count-IDBRequest-any-key
public count(key: BridgeIDBKeyRange | Key | null | undefined) {
confirmActiveTransaction(this);
if (key === null) {
key = undefined;
}
if (key !== undefined && !(key instanceof BridgeIDBKeyRange)) {
key = BridgeIDBKeyRange.only(valueToKey(key));
}
const getReq: RecordGetRequest = {
direction: "next",
indexName: this._name,
limit: 1,
range: key,
objectStoreName: this.objectStore._name,
resultLevel: ResultLevel.OnlyCount,
};
const operation = async () => {
const { btx } = this._confirmActiveTransaction();
const result = await this._backend.getRecords(btx, getReq);
return result.count;
};
return this.objectStore.transaction._execRequestAsync({
operation,
source: this,
});
}
public toString() {
return "[object IDBIndex]";
}
}
export default BridgeIDBIndex;

View File

@ -0,0 +1,133 @@
/*
Copyright 2019 Florian Dold
Copyright 2017 Jeremy Scheff
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
import compareKeys from "./util/cmp";
import { DataError } from "./util/errors";
import { Key } from "./util/types";
import valueToKey from "./util/valueToKey";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#range-concept
class BridgeIDBKeyRange {
public static only(value: Key) {
if (arguments.length === 0) {
throw new TypeError();
}
value = valueToKey(value);
return new BridgeIDBKeyRange(value, value, false, false);
}
static lowerBound(lower: Key, open: boolean = false) {
if (arguments.length === 0) {
throw new TypeError();
}
lower = valueToKey(lower);
return new BridgeIDBKeyRange(lower, undefined, open, true);
}
static upperBound(upper: Key, open: boolean = false) {
if (arguments.length === 0) {
throw new TypeError();
}
upper = valueToKey(upper);
return new BridgeIDBKeyRange(undefined, upper, true, open);
}
static bound(
lower: Key,
upper: Key,
lowerOpen: boolean = false,
upperOpen: boolean = false,
) {
if (arguments.length < 2) {
throw new TypeError();
}
const cmpResult = compareKeys(lower, upper);
if (cmpResult === 1 || (cmpResult === 0 && (lowerOpen || upperOpen))) {
throw new DataError();
}
lower = valueToKey(lower);
upper = valueToKey(upper);
return new BridgeIDBKeyRange(lower, upper, lowerOpen, upperOpen);
}
readonly lower: Key | undefined;
readonly upper: Key | undefined;
readonly lowerOpen: boolean;
readonly upperOpen: boolean;
constructor(
lower: Key | undefined,
upper: Key | undefined,
lowerOpen: boolean,
upperOpen: boolean,
) {
this.lower = lower;
this.upper = upper;
this.lowerOpen = lowerOpen;
this.upperOpen = upperOpen;
}
// https://w3c.github.io/IndexedDB/#dom-idbkeyrange-includes
includes(key: Key) {
if (arguments.length === 0) {
throw new TypeError();
}
key = valueToKey(key);
if (this.lower !== undefined) {
const cmpResult = compareKeys(this.lower, key);
if (cmpResult === 1 || (cmpResult === 0 && this.lowerOpen)) {
return false;
}
}
if (this.upper !== undefined) {
const cmpResult = compareKeys(this.upper, key);
if (cmpResult === -1 || (cmpResult === 0 && this.upperOpen)) {
return false;
}
}
return true;
}
toString() {
return "[object IDBKeyRange]";
}
static _valueToKeyRange(value: any, nullDisallowedFlag: boolean = false) {
if (value instanceof BridgeIDBKeyRange) {
return value;
}
if (value === null || value === undefined) {
if (nullDisallowedFlag) {
throw new DataError();
}
return new BridgeIDBKeyRange(undefined, undefined, false, false);
}
const key = valueToKey(value);
return BridgeIDBKeyRange.only(key);
}
}
export default BridgeIDBKeyRange;

View File

@ -0,0 +1,441 @@
/*
Copyright 2017 Jeremy Scheff
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
import BridgeIDBCursor from "./BridgeIDBCursor";
import BridgeIDBCursorWithValue from "./BridgeIDBCursorWithValue";
import BridgeIDBIndex from "./BridgeIDBIndex";
import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
import BridgeIDBRequest from "./BridgeIDBRequest";
import BridgeIDBTransaction from "./BridgeIDBTransaction";
import {
ConstraintError,
DataError,
InvalidAccessError,
InvalidStateError,
NotFoundError,
ReadOnlyError,
TransactionInactiveError,
} from "./util/errors";
import extractKey from "./util/extractKey";
import fakeDOMStringList from "./util/fakeDOMStringList";
import structuredClone from "./util/structuredClone";
import {
FakeDOMStringList,
BridgeIDBCursorDirection,
Key,
KeyPath,
Value,
} from "./util/types";
import validateKeyPath from "./util/validateKeyPath";
import valueToKey from "./util/valueToKey";
import {
DatabaseTransaction,
RecordGetRequest,
ResultLevel,
} from "./backend-interface";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#object-store
class BridgeIDBObjectStore {
_indexesCache: Map<string, BridgeIDBIndex> = new Map();
transaction: BridgeIDBTransaction;
get autoIncrement(): boolean {
return this._schema.objectStores[this._name].autoIncrement;
}
get indexNames(): FakeDOMStringList {
return fakeDOMStringList(this._schema.objectStores[this._name].indexes).sort();
}
get keyPath(): KeyPath | null {
return this._schema.objectStores[this._name].keyPath;
}
_name: string;
get _schema() {
return this.transaction.db._schema;
}
_deleted: boolean = false;
constructor(transaction: BridgeIDBTransaction, name: string) {
this._name = name;
this.transaction = transaction;
}
get name() {
return this._name;
}
get _backend() {
return this.transaction.db._backend;
}
get _backendConnection() {
return this.transaction.db._backendConnection;
}
_confirmActiveTransaction(): { btx: DatabaseTransaction } {
const btx = this.transaction._backendTransaction;
if (!btx) {
throw new InvalidStateError();
}
return { btx };
}
// http://w3c.github.io/IndexedDB/#dom-idbobjectstore-name
set name(newName: any) {
const transaction = this.transaction;
if (!transaction.db._runningVersionchangeTransaction) {
throw new InvalidStateError();
}
let { btx } = this._confirmActiveTransaction();
newName = String(newName);
const oldName = this._name;
if (newName === oldName) {
return;
}
this._backend.renameObjectStore(btx, oldName, newName);
this.transaction.db._schema = this._backend.getSchema(this._backendConnection);
}
public _store(value: Value, key: Key | undefined, overwrite: boolean) {
if (this.transaction.mode === "readonly") {
throw new ReadOnlyError();
}
const operation = async () => {
const { btx } = this._confirmActiveTransaction();
return this._backend.storeRecord(btx, {
objectStoreName: this._name,
key: key,
value: value,
overwrite,
});
};
return this.transaction._execRequestAsync({ operation, source: this });
}
public put(value: Value, key?: Key) {
if (arguments.length === 0) {
throw new TypeError();
}
return this._store(value, key, true);
}
public add(value: Value, key?: Key) {
if (arguments.length === 0) {
throw new TypeError();
}
return this._store(value, key, false);
}
public delete(key: Key) {
if (arguments.length === 0) {
throw new TypeError();
}
if (this.transaction.mode === "readonly") {
throw new ReadOnlyError();
}
if (!(key instanceof BridgeIDBKeyRange)) {
key = valueToKey(key);
}
const operation = async () => {
const { btx } = this._confirmActiveTransaction();
return this._backend.deleteRecord(btx, this._name, key);
}
return this.transaction._execRequestAsync({
operation,
source: this,
});
}
public get(key?: BridgeIDBKeyRange | Key) {
if (arguments.length === 0) {
throw new TypeError();
}
if (!(key instanceof BridgeIDBKeyRange)) {
key = valueToKey(key);
}
const recordRequest: RecordGetRequest = {
objectStoreName: this._name,
indexName: undefined,
lastIndexPosition: undefined,
lastObjectStorePosition: undefined,
direction: "next",
limit: 1,
resultLevel: ResultLevel.Full,
range: key,
};
const operation = async () => {
const { btx } = this._confirmActiveTransaction();
const result = await this._backend.getRecords(
btx,
recordRequest,
);
if (result.count == 0) {
return undefined;
}
const values = result.values;
if (!values) {
throw Error("invariant violated");
}
return values[0];
};
return this.transaction._execRequestAsync({
operation,
source: this,
});
}
// http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getall
public getAll(query?: BridgeIDBKeyRange | Key, count?: number) {
throw Error("not implemented");
}
// http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getkey
public getKey(key?: BridgeIDBKeyRange | Key) {
throw Error("not implemented");
}
// http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getallkeys
public getAllKeys(query?: BridgeIDBKeyRange | Key, count?: number) {
throw Error("not implemented");
}
public clear() {
throw Error("not implemented");
}
public openCursor(
range?: BridgeIDBKeyRange | Key,
direction: BridgeIDBCursorDirection = "next",
) {
if (range === null) {
range = undefined;
}
if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) {
range = BridgeIDBKeyRange.only(valueToKey(range));
}
const request = new BridgeIDBRequest();
request.source = this;
request.transaction = this.transaction;
const cursor = new BridgeIDBCursorWithValue(
this,
this._name,
undefined,
range,
direction,
request,
);
return this.transaction._execRequestAsync({
operation: () => cursor._iterate(),
request,
source: this,
});
}
public openKeyCursor(
range?: BridgeIDBKeyRange | Key,
direction?: BridgeIDBCursorDirection,
) {
if (range === null) {
range = undefined;
}
if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) {
range = BridgeIDBKeyRange.only(valueToKey(range));
}
if (!direction) {
direction = "next";
}
const request = new BridgeIDBRequest();
request.source = this;
request.transaction = this.transaction;
const cursor = new BridgeIDBCursor(
this,
this._name,
undefined,
range,
direction,
request,
true,
);
return this.transaction._execRequestAsync({
operation: cursor._iterate.bind(cursor),
request,
source: this,
});
}
// tslint:disable-next-line max-line-length
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBObjectStore-createIndex-IDBIndex-DOMString-name-DOMString-sequence-DOMString--keyPath-IDBIndexParameters-optionalParameters
public createIndex(
indexName: string,
keyPath: KeyPath,
optionalParameters: { multiEntry?: boolean; unique?: boolean } = {},
) {
if (arguments.length < 2) {
throw new TypeError();
}
if (!this.transaction.db._runningVersionchangeTransaction) {
throw new InvalidStateError();
}
const { btx } = this._confirmActiveTransaction();
const multiEntry =
optionalParameters.multiEntry !== undefined
? optionalParameters.multiEntry
: false;
const unique =
optionalParameters.unique !== undefined
? optionalParameters.unique
: false;
if (this.transaction.mode !== "versionchange") {
throw new InvalidStateError();
}
if (this.indexNames.indexOf(indexName) >= 0) {
throw new ConstraintError();
}
validateKeyPath(keyPath);
if (Array.isArray(keyPath) && multiEntry) {
throw new InvalidAccessError();
}
this._backend.createIndex(
btx,
indexName,
this._name,
keyPath,
multiEntry,
unique,
);
return new BridgeIDBIndex(this, indexName);
}
// https://w3c.github.io/IndexedDB/#dom-idbobjectstore-index
public index(name: string) {
if (arguments.length === 0) {
throw new TypeError();
}
if (this.transaction._state === "finished") {
throw new InvalidStateError();
}
const index = this._indexesCache.get(name);
if (index !== undefined) {
return index;
}
return new BridgeIDBIndex(this, name);
}
public deleteIndex(name: string) {
if (arguments.length === 0) {
throw new TypeError();
}
if (this.transaction.mode !== "versionchange") {
throw new InvalidStateError();
}
if (!this.transaction.db._runningVersionchangeTransaction) {
throw new InvalidStateError();
}
const { btx } = this._confirmActiveTransaction();
const index = this._indexesCache.get(name);
if (index !== undefined) {
index._deleted = true;
}
this._backend.deleteIndex(btx, name);
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBObjectStore-count-IDBRequest-any-key
public count(key?: Key | BridgeIDBKeyRange) {
if (key === null) {
key = undefined;
}
if (key !== undefined && !(key instanceof BridgeIDBKeyRange)) {
key = BridgeIDBKeyRange.only(valueToKey(key));
}
const recordGetRequest: RecordGetRequest = {
direction: "next",
indexName: undefined,
lastIndexPosition: undefined,
limit: -1,
objectStoreName: this._name,
lastObjectStorePosition: undefined,
range: key,
resultLevel: ResultLevel.OnlyCount,
};
const operation = async () => {
const { btx } = this._confirmActiveTransaction();
const result = await this._backend.getRecords(
btx,
recordGetRequest,
);
return result.count;
};
return this.transaction._execRequestAsync({ operation, source: this });
}
public toString() {
return "[object IDBObjectStore]";
}
}
export default BridgeIDBObjectStore;

View File

@ -0,0 +1,36 @@
/*
Copyright 2019 Florian Dold
Copyright 2017 Jeremy Scheff
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
import BridgeIDBRequest from "./BridgeIDBRequest";
import { EventCallback } from "./util/types";
class BridgeIDBOpenDBRequest extends BridgeIDBRequest {
public onupgradeneeded: EventCallback | null = null;
public onblocked: EventCallback | null = null;
constructor() {
super();
// https://www.w3.org/TR/IndexedDB/#open-requests
this.source = null;
}
public toString() {
return "[object IDBOpenDBRequest]";
}
}
export default BridgeIDBOpenDBRequest;

View File

@ -0,0 +1,86 @@
/*
* Copyright 2017 Jeremy Scheff
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import BridgeFDBCursor from "./BridgeIDBCursor";
import BridgeIDBIndex from "./BridgeIDBIndex";
import BridgeIDBObjectStore from "./BridgeIDBObjectStore";
import BridgeIDBTransaction from "./BridgeIDBTransaction";
import { InvalidStateError } from "./util/errors";
import FakeEventTarget from "./util/FakeEventTarget";
import { EventCallback } from "./util/types";
import FakeEvent from "./util/FakeEvent";
class BridgeIDBRequest extends FakeEventTarget {
_result: any = null;
_error: Error | null | undefined = null;
source: BridgeFDBCursor | BridgeIDBIndex | BridgeIDBObjectStore | null = null;
transaction: BridgeIDBTransaction | null = null;
readyState: "done" | "pending" = "pending";
onsuccess: EventCallback | null = null;
onerror: EventCallback | null = null;
get error() {
if (this.readyState === "pending") {
throw new InvalidStateError();
}
return this._error;
}
set error(value: any) {
this._error = value;
}
get result() {
if (this.readyState === "pending") {
throw new InvalidStateError();
}
return this._result;
}
set result(value: any) {
this._result = value;
}
toString() {
return "[object IDBRequest]";
}
_finishWithError(err: Error) {
this.result = undefined;
this.readyState = "done";
this.error = new Error(err.message);
this.error.name = err.name;
const event = new FakeEvent("error", {
bubbles: true,
cancelable: true,
});
event.eventPath = [];
this.dispatchEvent(event);
}
_finishWithResult(result: any) {
this.result = result;
this.readyState = "done";
const event = new FakeEvent("success");
event.eventPath = [];
this.dispatchEvent(event);
}
}
export default BridgeIDBRequest;

View File

@ -0,0 +1,301 @@
import BridgeIDBDatabase from "./BridgeIDBDatabase";
import BridgeIDBObjectStore from "./BridgeIDBObjectStore";
import BridgeIDBRequest from "./BridgeIDBRequest";
import {
AbortError,
InvalidStateError,
NotFoundError,
TransactionInactiveError,
} from "./util/errors";
import fakeDOMStringList from "./util/fakeDOMStringList";
import FakeEvent from "./util/FakeEvent";
import FakeEventTarget from "./util/FakeEventTarget";
import {
EventCallback,
FakeDOMStringList,
RequestObj,
TransactionMode,
} from "./util/types";
import queueTask from "./util/queueTask";
import openPromise from "./util/openPromise";
import { DatabaseTransaction, Backend } from "./backend-interface";
import { array } from "prop-types";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#transaction
class BridgeIDBTransaction extends FakeEventTarget {
public _state: "active" | "inactive" | "committing" | "finished" = "active";
public _started = false;
public _objectStoresCache: Map<string, BridgeIDBObjectStore> = new Map();
public _backendTransaction?: DatabaseTransaction;
public objectStoreNames: FakeDOMStringList;
public mode: TransactionMode;
public db: BridgeIDBDatabase;
public error: Error | null = null;
public onabort: EventCallback | null = null;
public oncomplete: EventCallback | null = null;
public onerror: EventCallback | null = null;
private _waitPromise: Promise<void>;
private _resolveWait: () => void;
public _scope: Set<string>;
private _requests: Array<{
operation: () => void;
request: BridgeIDBRequest;
}> = [];
get _backend(): Backend {
return this.db._backend;
}
constructor(
storeNames: string[],
mode: TransactionMode,
db: BridgeIDBDatabase,
backendTransaction?: DatabaseTransaction,
) {
super();
const myOpenPromise = openPromise<void>();
this._waitPromise = myOpenPromise.promise;
this._resolveWait = myOpenPromise.resolve;
this._scope = new Set(storeNames);
this._backendTransaction = backendTransaction;
this.mode = mode;
this.db = db;
this.objectStoreNames = fakeDOMStringList(Array.from(this._scope).sort());
this.db._transactions.push(this);
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-aborting-a-transaction
async _abort(errName: string | null) {
this._state = "finished";
if (errName !== null) {
const e = new Error();
e.name = errName;
this.error = e;
}
// Should this directly remove from _requests?
for (const { request } of this._requests) {
if (request.readyState !== "done") {
request.readyState = "done"; // This will cancel execution of this request's operation
if (request.source) {
request.result = undefined;
request.error = new AbortError();
const event = new FakeEvent("error", {
bubbles: true,
cancelable: true,
});
event.eventPath = [this.db, this];
request.dispatchEvent(event);
}
}
}
// Only roll back if we actually executed the scheduled operations.
const maybeBtx = this._backendTransaction;
if (maybeBtx) {
await this._backend.rollback(maybeBtx);
}
queueTask(() => {
const event = new FakeEvent("abort", {
bubbles: true,
cancelable: false,
});
event.eventPath = [this.db];
this.dispatchEvent(event);
});
}
public abort() {
if (this._state === "committing" || this._state === "finished") {
throw new InvalidStateError();
}
this._state = "active";
this._abort(null);
}
// http://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstore
public objectStore(name: string) {
if (this._state !== "active") {
throw new InvalidStateError();
}
const objectStore = this._objectStoresCache.get(name);
if (objectStore !== undefined) {
return objectStore;
}
return new BridgeIDBObjectStore(this, name);
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-asynchronously-executing-a-request
public _execRequestAsync(obj: RequestObj) {
const source = obj.source;
const operation = obj.operation;
let request = obj.hasOwnProperty("request") ? obj.request : null;
if (this._state !== "active") {
throw new TransactionInactiveError();
}
// Request should only be passed for cursors
if (!request) {
if (!source) {
// Special requests like indexes that just need to run some code
request = new BridgeIDBRequest();
} else {
request = new BridgeIDBRequest();
request.source = source;
request.transaction = (source as any).transaction;
}
}
this._requests.push({
operation,
request,
});
return request;
}
public async _start() {
this._started = true;
if (!this._backendTransaction) {
this._backendTransaction = await this._backend.beginTransaction(
this.db._backendConnection,
Array.from(this._scope),
this.mode,
);
}
// Remove from request queue - cursor ones will be added back if necessary by cursor.continue and such
let operation;
let request;
while (this._requests.length > 0) {
const r = this._requests.shift();
// This should only be false if transaction was aborted
if (r && r.request.readyState !== "done") {
request = r.request;
operation = r.operation;
break;
}
}
if (request && operation) {
if (!request.source) {
// Special requests like indexes that just need to run some code, with error handling already built into
// operation
await operation();
} else {
let defaultAction;
let event;
try {
const result = await operation();
request.readyState = "done";
request.result = result;
request.error = undefined;
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-fire-a-success-event
if (this._state === "inactive") {
this._state = "active";
}
event = new FakeEvent("success", {
bubbles: false,
cancelable: false,
});
} catch (err) {
request.readyState = "done";
request.result = undefined;
request.error = err;
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-fire-an-error-event
if (this._state === "inactive") {
this._state = "active";
}
event = new FakeEvent("error", {
bubbles: true,
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");
}
throw err;
}
// Default action of event
if (!event.canceled) {
if (defaultAction) {
defaultAction();
}
}
}
// On to the next one
if (this._requests.length > 0) {
this._start();
} else {
// Give it another chance for new handlers to be set before finishing
queueTask(() => this._start());
}
return;
}
// Check if transaction complete event needs to be fired
if (this._state !== "finished") {
// Either aborted or committed already
this._state = "finished";
if (!this.error) {
const event = new FakeEvent("complete");
this.dispatchEvent(event);
}
const idx = this.db._transactions.indexOf(this);
if (idx < 0) {
throw Error("invariant failed");
}
this.db._transactions.splice(idx, 1);
this._resolveWait();
}
}
public commit() {
if (this._state !== "active") {
throw new InvalidStateError();
}
this._state = "committing";
}
public toString() {
return "[object IDBRequest]";
}
_waitDone(): Promise<void> {
return this._waitPromise;
}
}
export default BridgeIDBTransaction;

View File

@ -0,0 +1,41 @@
/*
Copyright 2019 Florian Dold
Copyright 2017 Jeremy Scheff
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
import FakeEvent from "./util/FakeEvent";
class BridgeIDBVersionChangeEvent extends FakeEvent {
public newVersion: number | null;
public oldVersion: number;
constructor(
type: "blocked" | "success" | "upgradeneeded" | "versionchange",
parameters: { newVersion?: number | null; oldVersion?: number } = {},
) {
super(type);
this.newVersion =
parameters.newVersion !== undefined ? parameters.newVersion : null;
this.oldVersion =
parameters.oldVersion !== undefined ? parameters.oldVersion : 0;
}
public toString() {
return "[object IDBVersionChangeEvent]";
}
}
export default BridgeIDBVersionChangeEvent;

View File

@ -0,0 +1,31 @@
import test from 'ava';
import MemoryBackend from './MemoryBackend';
import BridgeIDBFactory from './BridgeIDBFactory';
test.cb("basics", (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");
// 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();
};
});

View File

@ -0,0 +1,662 @@
import {
Backend,
DatabaseConnection,
DatabaseTransaction,
Schema,
RecordStoreRequest,
IndexProperties,
} from "./backend-interface";
import structuredClone from "./util/structuredClone";
import { InvalidStateError, InvalidAccessError } 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";
enum TransactionLevel {
Disconnected = 0,
Connected = 1,
Read = 2,
Write = 3,
VersionChange = 4,
}
interface ObjectStore {
originalName: string;
modifiedName: string | undefined;
originalData: ISortedMapF;
modifiedData: ISortedMapF | undefined;
deleted: boolean;
originalKeyGenerator: number;
modifiedKeyGenerator: number | undefined;
}
interface Index {
originalName: string;
modifiedName: string | undefined;
originalData: ISortedMapF;
modifiedData: ISortedMapF | undefined;
deleted: boolean;
}
interface Database {
committedObjectStores: { [name: string]: ObjectStore };
modifiedObjectStores: { [name: string]: ObjectStore };
committedIndexes: { [name: string]: Index };
modifiedIndexes: { [name: string]: Index };
committedSchema: Schema;
/**
* Was the transaction deleted during the running transaction?
*/
deleted: boolean;
txLevel: TransactionLevel;
connectionCookie: string | undefined;
}
interface Connection {
dbName: string;
modifiedSchema: Schema | undefined;
/**
* Has the underlying database been deleted?
*/
deleted: boolean;
/**
* Map from the effective name of an object store during
* the transaction to the real name.
*/
objectStoreMap: { [currentName: string]: ObjectStore };
indexMap: { [currentName: string]: Index };
}
class AsyncCondition {
wait(): Promise<void> {
throw Error("not implemented");
}
trigger(): void {}
}
function insertIntoIndex(
index: Index,
value: Value,
indexProperties: IndexProperties,
) {
if (indexProperties.multiEntry) {
} else {
const key = extractKey(value, indexProperties.keyPath);
}
throw Error("not implemented");
}
/**
* Primitive in-memory backend.
*/
export class MemoryBackend implements Backend {
databases: { [name: string]: Database } = {};
connectionIdCounter = 1;
transactionIdCounter = 1;
/**
* Connections by connection cookie.
*/
connections: { [name: string]: Connection } = {};
/**
* Connections by transaction (!!) cookie. In this implementation,
* at most one transaction can run at the same time per connection.
*/
connectionsByTransaction: { [tx: string]: Connection } = {};
/**
* Condition that is triggered whenever a client disconnects.
*/
disconnectCond: AsyncCondition = new AsyncCondition();
/**
* Conditation that is triggered whenever a transaction finishes.
*/
transactionDoneCond: AsyncCondition = new AsyncCondition();
async getDatabases(): Promise<{ name: string; version: number }[]> {
const dbList = [];
for (const name in this.databases) {
dbList.push({
name,
version: this.databases[name].committedSchema.databaseVersion,
});
}
return dbList;
}
async deleteDatabase(tx: DatabaseTransaction, name: string): Promise<void> {
const myConn = this.connectionsByTransaction[tx.transactionCookie];
if (!myConn) {
throw Error("no connection associated with transaction");
}
const myDb = this.databases[name];
if (!myDb) {
throw Error("db not found");
}
if (myDb.committedSchema.databaseName !== name) {
throw Error("name does not match");
}
if (myDb.txLevel < TransactionLevel.VersionChange) {
throw new InvalidStateError();
}
if (myDb.connectionCookie !== tx.transactionCookie) {
throw new InvalidAccessError();
}
myDb.deleted = true;
}
async connectDatabase(name: string): Promise<DatabaseConnection> {
const connectionId = this.connectionIdCounter++;
const connectionCookie = `connection-${connectionId}`;
let database = this.databases[name];
if (!database) {
const schema: Schema = {
databaseName: name,
indexes: {},
databaseVersion: 0,
objectStores: {},
};
database = {
committedSchema: schema,
deleted: false,
modifiedIndexes: {},
committedIndexes: {},
committedObjectStores: {},
modifiedObjectStores: {},
txLevel: TransactionLevel.Disconnected,
connectionCookie: undefined,
};
this.databases[name] = database;
}
while (database.txLevel !== TransactionLevel.Disconnected) {
await this.disconnectCond.wait();
}
database.txLevel = TransactionLevel.Connected;
database.connectionCookie = connectionCookie;
return { connectionCookie };
}
async beginTransaction(
conn: DatabaseConnection,
objectStores: string[],
mode: import("./util/types").TransactionMode,
): Promise<DatabaseTransaction> {
const transactionCookie = `tx-${this.transactionIdCounter++}`;
const myConn = this.connections[conn.connectionCookie];
if (!myConn) {
throw Error("connection not found");
}
const myDb = this.databases[myConn.dbName];
if (!myDb) {
throw Error("db not found");
}
while (myDb.txLevel !== TransactionLevel.Connected) {
await this.transactionDoneCond.wait();
}
if (mode === "readonly") {
myDb.txLevel = TransactionLevel.Read;
} else if (mode === "readwrite") {
myDb.txLevel = TransactionLevel.Write;
} else {
throw Error("unsupported transaction mode");
}
this.connectionsByTransaction[transactionCookie] = myConn;
return { transactionCookie };
}
async enterVersionChange(
conn: DatabaseConnection,
newVersion: number,
): Promise<DatabaseTransaction> {
const transactionCookie = `tx-vc-${this.transactionIdCounter++}`;
const myConn = this.connections[conn.connectionCookie];
if (!myConn) {
throw Error("connection not found");
}
const myDb = this.databases[myConn.dbName];
if (!myDb) {
throw Error("db not found");
}
while (myDb.txLevel !== TransactionLevel.Connected) {
await this.transactionDoneCond.wait();
}
myDb.txLevel = TransactionLevel.VersionChange;
this.connectionsByTransaction[transactionCookie] = myConn;
return { transactionCookie };
}
async close(conn: DatabaseConnection): Promise<void> {
const myConn = this.connections[conn.connectionCookie];
if (!myConn) {
throw Error("connection not found - already closed?");
}
if (!myConn.deleted) {
const myDb = this.databases[myConn.dbName];
if (myDb.txLevel != TransactionLevel.Connected) {
throw Error("invalid state");
}
myDb.txLevel = TransactionLevel.Disconnected;
}
delete this.connections[conn.connectionCookie];
}
getSchema(dbConn: DatabaseConnection): Schema {
const myConn = this.connections[dbConn.connectionCookie];
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
}
if (myConn.modifiedSchema) {
return myConn.modifiedSchema;
}
return db.committedSchema;
}
renameIndex(
btx: DatabaseTransaction,
oldName: string,
newName: string,
): void {
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
}
if (db.txLevel < TransactionLevel.VersionChange) {
throw Error("only allowed in versionchange transaction");
}
let schema = myConn.modifiedSchema;
if (!schema) {
throw Error();
}
if (schema.indexes[newName]) {
throw new Error("new index name already used");
}
if (!schema.indexes[oldName]) {
throw new Error("new index name already used");
}
const index: Index = myConn.indexMap[oldName];
if (!index) {
throw Error("old index missing in connection's index map");
}
schema.indexes[newName] = schema.indexes[newName];
delete schema.indexes[oldName];
for (const storeName in schema.objectStores) {
const store = schema.objectStores[storeName];
store.indexes = store.indexes.map(x => {
if (x == oldName) {
return newName;
} else {
return x;
}
});
}
myConn.indexMap[newName] = index;
delete myConn.indexMap[oldName];
index.modifiedName = newName;
}
deleteIndex(btx: DatabaseTransaction, indexName: string): void {
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
}
if (db.txLevel < TransactionLevel.VersionChange) {
throw Error("only allowed in versionchange transaction");
}
let schema = myConn.modifiedSchema;
if (!schema) {
throw Error();
}
if (!schema.indexes[indexName]) {
throw new Error("index does not exist");
}
const index: Index = myConn.indexMap[indexName];
if (!index) {
throw Error("old index missing in connection's index map");
}
index.deleted = true;
delete schema.indexes[indexName];
delete myConn.indexMap[indexName];
for (const storeName in schema.objectStores) {
const store = schema.objectStores[storeName];
store.indexes = store.indexes.filter(x => {
return x !== indexName;
});
}
}
deleteObjectStore(btx: DatabaseTransaction, name: string): void {
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
}
if (db.txLevel < TransactionLevel.VersionChange) {
throw Error("only allowed in versionchange transaction");
}
const schema = myConn.modifiedSchema;
if (!schema) {
throw Error();
}
const objectStoreProperties = schema.objectStores[name];
if (!objectStoreProperties) {
throw Error("object store not found");
}
const objectStore = myConn.objectStoreMap[name];
if (!objectStore) {
throw Error("object store not found in map");
}
const indexNames = objectStoreProperties.indexes;
for (const indexName of indexNames) {
this.deleteIndex(btx, indexName);
}
objectStore.deleted = true;
delete myConn.objectStoreMap[name];
delete schema.objectStores[name];
}
renameObjectStore(
btx: DatabaseTransaction,
oldName: string,
newName: string,
): void {
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
}
if (db.txLevel < TransactionLevel.VersionChange) {
throw Error("only allowed in versionchange transaction");
}
const schema = myConn.modifiedSchema;
if (!schema) {
throw Error();
}
if (!schema.objectStores[oldName]) {
throw Error("object store not found");
}
if (schema.objectStores[newName]) {
throw Error("new object store already exists");
}
const objectStore = myConn.objectStoreMap[oldName];
if (!objectStore) {
throw Error("object store not found in map");
}
objectStore.modifiedName = newName;
schema.objectStores[newName] = schema.objectStores[oldName];
delete schema.objectStores[oldName];
delete myConn.objectStoreMap[oldName];
myConn.objectStoreMap[newName] = objectStore;
}
createObjectStore(
btx: DatabaseTransaction,
name: string,
keyPath: string | string[] | null,
autoIncrement: boolean,
): void {
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
}
if (db.txLevel < TransactionLevel.VersionChange) {
throw Error("only allowed in versionchange transaction");
}
const newObjectStore: ObjectStore = {
deleted: false,
modifiedName: undefined,
originalName: name,
modifiedData: undefined,
originalData: new BTree([], compareKeys),
modifiedKeyGenerator: undefined,
originalKeyGenerator: 1,
};
const schema = myConn.modifiedSchema;
if (!schema) {
throw Error("no schema for versionchange tx");
}
schema.objectStores[name] = {
autoIncrement,
keyPath,
indexes: [],
};
myConn.objectStoreMap[name] = newObjectStore;
db.modifiedObjectStores[name] = newObjectStore;
}
createIndex(
btx: DatabaseTransaction,
indexName: string,
objectStoreName: string,
keyPath: import("./util/types").KeyPath,
multiEntry: boolean,
unique: boolean,
): void {
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
}
if (db.txLevel < TransactionLevel.VersionChange) {
throw Error("only allowed in versionchange transaction");
}
const indexProperties: IndexProperties = {
keyPath,
multiEntry,
unique,
};
const newIndex: Index = {
deleted: false,
modifiedData: undefined,
modifiedName: undefined,
originalData: new BTree([], compareKeys),
originalName: indexName,
};
myConn.indexMap[indexName] = newIndex;
db.modifiedIndexes[indexName] = newIndex;
const schema = myConn.modifiedSchema;
if (!schema) {
throw Error("no schema in versionchange tx");
}
const objectStoreProperties = schema.objectStores[objectStoreName];
if (!objectStoreProperties) {
throw Error("object store not found");
}
objectStoreProperties.indexes.push(indexName);
schema.indexes[indexName] = indexProperties;
// FIXME: build index from existing object store!
}
async deleteRecord(
btx: DatabaseTransaction,
objectStoreName: string,
range: import("./BridgeIDBKeyRange").default,
): Promise<void> {
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
}
if (db.txLevel < TransactionLevel.Write) {
throw Error("only allowed in write transaction");
}
}
async getRecords(
btx: DatabaseTransaction,
req: import("./backend-interface").RecordGetRequest,
): Promise<import("./backend-interface").RecordGetResponse> {
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
}
if (db.txLevel < TransactionLevel.Write) {
throw Error("only allowed while running a transaction");
}
throw Error("not implemented");
}
async storeRecord(
btx: DatabaseTransaction,
storeReq: RecordStoreRequest,
): Promise<void> {
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
}
if (db.txLevel < TransactionLevel.Write) {
throw Error("only allowed while running a transaction");
}
const schema = myConn.modifiedSchema
? myConn.modifiedSchema
: db.committedSchema;
const objectStore = myConn.objectStoreMap[storeReq.objectStoreName];
const storeKeyResult: StoreKeyResult = getStoreKey(
storeReq.value,
storeReq.key,
objectStore.modifiedKeyGenerator || objectStore.originalKeyGenerator,
schema.objectStores[storeReq.objectStoreName].autoIncrement,
schema.objectStores[storeReq.objectStoreName].keyPath,
);
let key = storeKeyResult.key;
let value = storeKeyResult.value;
objectStore.modifiedKeyGenerator = storeKeyResult.updatedKeyGenerator;
if (!objectStore.modifiedData) {
objectStore.modifiedData = objectStore.originalData;
}
const modifiedData = objectStore.modifiedData;
const hasKey = modifiedData.has(key);
if (hasKey && !storeReq.overwrite) {
throw Error("refusing to overwrite");
}
objectStore.modifiedData = modifiedData.with(key, value, true);
for (const indexName of schema.objectStores[storeReq.objectStoreName]
.indexes) {
const index = myConn.indexMap[indexName];
if (!index) {
throw Error("index referenced by object store does not exist");
}
const indexProperties = schema.indexes[indexName];
insertIntoIndex(index, value, indexProperties);
}
}
async rollback(btx: DatabaseTransaction): Promise<void> {
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
}
if (db.txLevel < TransactionLevel.Read) {
throw Error("only allowed while running a transaction");
}
db.modifiedIndexes = {};
db.modifiedObjectStores = {};
db.txLevel = TransactionLevel.Connected;
myConn.modifiedSchema = structuredClone(db.committedSchema);
myConn.indexMap = Object.assign({}, db.committedIndexes);
myConn.objectStoreMap = Object.assign({}, db.committedObjectStores);
for (const indexName in db.committedIndexes) {
const index = db.committedIndexes[indexName];
index.deleted = false;
index.modifiedData = undefined;
index.modifiedName = undefined;
}
for (const objectStoreName in db.committedObjectStores) {
const objectStore = db.committedObjectStores[objectStoreName];
objectStore.deleted = false;
objectStore.modifiedData = undefined;
objectStore.modifiedName = undefined;
objectStore.modifiedKeyGenerator = undefined;
}
}
async commit(btx: DatabaseTransaction): Promise<void> {
const myConn = this.connections[btx.transactionCookie];
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
}
if (db.txLevel < TransactionLevel.Read) {
throw Error("only allowed while running a transaction");
}
}
}
export default MemoryBackend;

View File

@ -0,0 +1,145 @@
import {
TransactionMode,
Value,
BridgeIDBCursorDirection,
Key,
KeyPath,
BridgeIDBDatabaseInfo,
} from "./util/types";
import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
export interface ObjectStoreProperties {
keyPath: KeyPath | null;
autoIncrement: boolean;
indexes: string[];
}
export interface IndexProperties {
keyPath: KeyPath;
multiEntry: boolean;
unique: boolean;
}
export interface Schema {
databaseName: string;
databaseVersion: number;
objectStores: { [name: string]: ObjectStoreProperties };
indexes: { [name: string]: IndexProperties };
}
export interface DatabaseConnection {
connectionCookie: string;
}
export interface DatabaseTransaction {
transactionCookie: string;
}
export enum ResultLevel {
Full,
OnlyKeys,
OnlyCount,
}
export interface RecordGetRequest {
direction: BridgeIDBCursorDirection;
objectStoreName: string;
indexName: string | undefined;
range: BridgeIDBKeyRange | undefined;
lastIndexPosition?: Key;
lastObjectStorePosition?: Key;
advanceIndexKey?: Key;
advancePrimaryKey?: Key;
limit: number;
resultLevel: ResultLevel;
}
export interface RecordGetResponse {
values: Value[] | undefined;
keys: Key[] | undefined;
primaryKeys: Key[] | undefined;
count: number;
}
export interface RecordStoreRequest {
objectStoreName: string;
value: Value;
key: Key | undefined;
overwrite: boolean;
}
export interface Backend {
getDatabases(): Promise<BridgeIDBDatabaseInfo[]>;
connectDatabase(name: string): Promise<DatabaseConnection>;
beginTransaction(
conn: DatabaseConnection,
objectStores: string[],
mode: TransactionMode,
): Promise<DatabaseTransaction>;
enterVersionChange(
conn: DatabaseConnection,
newVersion: number,
): Promise<DatabaseTransaction>;
/**
* Even though the standard interface for indexedDB doesn't require
* the client to run deleteDatabase in a version transaction, there is
* implicitly one running.
*/
deleteDatabase(btx: DatabaseTransaction, name: string): Promise<void>;
close(db: DatabaseConnection): Promise<void>;
getSchema(db: DatabaseConnection): Schema;
renameIndex(btx: DatabaseTransaction, oldName: string, newName: string): void;
deleteIndex(btx: DatabaseTransaction, indexName: string): void;
rollback(btx: DatabaseTransaction): Promise<void>;
commit(btx: DatabaseTransaction): Promise<void>;
deleteObjectStore(btx: DatabaseTransaction, name: string): void;
createObjectStore(
btx: DatabaseTransaction,
name: string,
keyPath: string | string[] | null,
autoIncrement: boolean,
): void;
renameObjectStore(
btx: DatabaseTransaction,
oldName: string,
newName: string,
): void;
createIndex(
btx: DatabaseTransaction,
indexName: string,
objectStoreName: string,
keyPath: KeyPath,
multiEntry: boolean,
unique: boolean,
): void;
deleteRecord(
btx: DatabaseTransaction,
objectStoreName: string,
range: BridgeIDBKeyRange,
): Promise<void>;
getRecords(
btx: DatabaseTransaction,
req: RecordGetRequest,
): Promise<RecordGetResponse>;
storeRecord(
btx: DatabaseTransaction,
storeReq: RecordStoreRequest,
): Promise<void>;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,329 @@
/*
Copyright (c) 2018 David Piepgrass
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SPDX-License-Identifier: MIT
*/
// Original repository: https://github.com/qwertie/btree-typescript
/** Read-only set interface (subinterface of IMapSource<K,any>).
* The word "set" usually means that each item in the collection is unique
* (appears only once, based on a definition of equality used by the
* collection.) Objects conforming to this interface aren't guaranteed not
* to contain duplicates, but as an example, BTree<K,V> implements this
* interface and does not allow duplicates. */
export interface ISetSource<K=any>
{
/** Returns the number of key/value pairs in the map object. */
size: number;
/** Returns a boolean asserting whether the key exists in the map object or not. */
has(key: K): boolean;
/** Returns a new iterator for iterating the items in the set (the order is implementation-dependent). */
keys(): IterableIterator<K>;
}
/** Read-only map interface (i.e. a source of key-value pairs). */
export interface IMapSource<K=any, V=any> extends ISetSource<K>
{
/** Returns the number of key/value pairs in the map object. */
size: number;
/** Returns the value associated to the key, or undefined if there is none. */
get(key: K): V|undefined;
/** Returns a boolean asserting whether the key exists in the map object or not. */
has(key: K): boolean;
/** Calls callbackFn once for each key-value pair present in the map object.
* The ES6 Map class sends the value to the callback before the key, so
* this interface must do likewise. */
forEach(callbackFn: (v:V, k:K, map:IMapSource<K,V>) => void, thisArg: any): void;
/** Returns an iterator that provides all key-value pairs from the collection (as arrays of length 2). */
entries(): IterableIterator<[K,V]>;
/** Returns a new iterator for iterating the keys of each pair. */
keys(): IterableIterator<K>;
/** Returns a new iterator for iterating the values of each pair. */
values(): IterableIterator<V>;
// TypeScript compiler decided Symbol.iterator has type 'any'
//[Symbol.iterator](): IterableIterator<[K,V]>;
}
/** Write-only set interface (the set cannot be queried, but items can be added to it.)
* @description Note: BTree<K,V> does not officially implement this interface,
* but BTree<K> can be used as an instance of ISetSink<K>. */
export interface ISetSink<K=any>
{
/** Adds the specified item to the set, if it was not in the set already. */
add(key: K): any;
/** Returns true if an element in the map object existed and has been
* removed, or false if the element did not exist. */
delete(key: K): boolean;
/** Removes everything so that the set is empty. */
clear(): void;
}
/** Write-only map interface (i.e. a drain into which key-value pairs can be "sunk") */
export interface IMapSink<K=any, V=any>
{
/** Returns true if an element in the map object existed and has been
* removed, or false if the element did not exist. */
delete(key: K): boolean;
/** Sets the value for the key in the map object (the return value is
* boolean in BTree but Map returns the Map itself.) */
set(key: K, value: V): any;
/** Removes all key/value pairs from the IMap object. */
clear(): void;
}
/** Set interface.
* @description Note: BTree<K,V> does not officially implement this interface,
* but BTree<K> can be used as an instance of ISet<K>. */
export interface ISet<K=any> extends ISetSource<K>, ISetSink<K> { }
/** An interface compatible with ES6 Map and BTree. This interface does not
* describe the complete interface of either class, but merely the common
* interface shared by both. */
export interface IMap<K=any, V=any> extends IMapSource<K, V>, IMapSink<K, V> { }
/** An data source that provides read-only access to a set of items called
* "keys" in sorted order. This is a subinterface of ISortedMapSource. */
export interface ISortedSetSource<K=any> extends ISetSource<K>
{
/** Gets the lowest key in the collection. */
minKey(): K | undefined;
/** Gets the highest key in the collection. */
maxKey(): K | undefined;
/** Returns the next key larger than the specified key (or undefined if there is none) */
nextHigherKey(key: K): K|undefined;
/** Returns the next key smaller than the specified key (or undefined if there is none) */
nextLowerKey(key: K): K|undefined;
/** Calls `callback` on the specified range of keys, in ascending order by key.
* @param low The first key scanned will be greater than or equal to `low`.
* @param high Scanning stops when a key larger than this is reached.
* @param includeHigh If the `high` key is present in the map, `onFound` is called
* for that final pair if and only if this parameter is true.
* @param onFound A function that is called for each key pair. Because this
* is a subinterface of ISortedMapSource, if there is a value
* associated with the key, it is passed as the second parameter.
* @param initialCounter Initial third argument of `onFound`. This value
* increases by one each time `onFound` is called. Default: 0
* @returns Number of pairs found and the number of times `onFound` was called.
*/
forRange(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:any,counter:number) => void, initialCounter?: number): number;
/** Returns a new iterator for iterating the keys of each pair in ascending order.
* @param firstKey: Minimum key to include in the output. */
keys(firstKey?: K): IterableIterator<K>;
}
/** An data source that provides read-only access to items in sorted order. */
export interface ISortedMapSource<K=any, V=any> extends IMapSource<K, V>, ISortedSetSource<K>
{
/** Returns the next pair whose key is larger than the specified key (or undefined if there is none) */
nextHigherPair(key: K): [K,V]|undefined;
/** Returns the next pair whose key is smaller than the specified key (or undefined if there is none) */
nextLowerPair(key: K): [K,V]|undefined;
/** Builds an array of pairs from the specified range of keys, sorted by key.
* Each returned pair is also an array: pair[0] is the key, pair[1] is the value.
* @param low The first key in the array will be greater than or equal to `low`.
* @param high This method returns when a key larger than this is reached.
* @param includeHigh If the `high` key is present in the map, its pair will be
* included in the output if and only if this parameter is true. Note:
* if the `low` key is present, it is always included in the output.
* @param maxLength Maximum length of the returned array (default: unlimited)
* @description Computational complexity: O(result.length + log size)
*/
getRange(low: K, high: K, includeHigh?: boolean, maxLength?: number): [K,V][];
/** Calls `callback` on the specified range of keys, in ascending order by key.
* @param low The first key scanned will be greater than or equal to `low`.
* @param high Scanning stops when a key larger than this is reached.
* @param includeHigh If the `high` key is present in the map, `onFound` is called
* for that final pair if and only if this parameter is true.
* @param onFound A function that is called for each key-value pair.
* @param initialCounter Initial third argument of onFound. This value
* increases by one each time `onFound` is called. Default: 0
* @returns Number of pairs found and the number of times `callback` was called.
*/
forRange(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:V,counter:number) => void, initialCounter?: number): number;
/** Returns an iterator that provides items in order by key.
* @param firstKey: Minimum key to include in the output. */
entries(firstKey?: K): IterableIterator<[K,V]>;
/** Returns a new iterator for iterating the keys of each pair in ascending order.
* @param firstKey: Minimum key to include in the output. */
keys(firstKey?: K): IterableIterator<K>;
/** Returns a new iterator for iterating the values of each pair in order by key.
* @param firstKey: Minimum key whose associated value is included in the output. */
values(firstKey?: K): IterableIterator<V>;
// This method should logically be in IMapSource but is not supported by ES6 Map
/** Performs a reduce operation like the `reduce` method of `Array`.
* It is used to combine all pairs into a single value, or perform conversions. */
reduce<R>(callback: (previous:R,currentPair:[K,V],counter:number,tree:IMapF<K,V>) => R, initialValue: R): R;
/** Performs a reduce operation like the `reduce` method of `Array`.
* It is used to combine all pairs into a single value, or perform conversions. */
reduce<R>(callback: (previous:R|undefined,currentPair:[K,V],counter:number,tree:IMapF<K,V>) => R): R|undefined;
}
/** An interface for a set of keys (the combination of ISortedSetSource<K> and ISetSink<K>) */
export interface ISortedSet<K=any> extends ISortedSetSource<K>, ISetSink<K> { }
/** An interface for a sorted map (dictionary),
* not including functional/persistent methods. */
export interface ISortedMap<K=any, V=any> extends IMap<K,V>, ISortedMapSource<K, V>
{
// All of the following methods should be in IMap but are left out of IMap
// so that IMap is compatible with ES6 Map.
/** Adds or overwrites a key-value pair in the sorted map.
* @param key the key is used to determine the sort order of data in the tree.
* @param value data to associate with the key
* @param overwrite Whether to overwrite an existing key-value pair
* (default: true). If this is false and there is an existing
* key-value pair then the call to this method has no effect.
* @returns true if a new key-value pair was added, false if the key
* already existed. */
set(key: K, value: V, overwrite?: boolean): boolean;
/** Adds all pairs from a list of key-value pairs.
* @param pairs Pairs to add to this tree. If there are duplicate keys,
* later pairs currently overwrite earlier ones (e.g. [[0,1],[0,7]]
* associates 0 with 7.)
* @param overwrite Whether to overwrite pairs that already exist (if false,
* pairs[i] is ignored when the key pairs[i][0] already exists.)
* @returns The number of pairs added to the collection.
*/
setPairs(pairs: [K,V][], overwrite?: boolean): number;
/** Deletes a series of keys from the collection. */
deleteKeys(keys: K[]): number;
/** Removes a range of key-value pairs from the B+ tree.
* @param low The first key deleted will be greater than or equal to `low`.
* @param high Deleting stops when a key larger than this is reached.
* @param includeHigh Specifies whether the `high` key, if present, is deleted.
* @returns The number of key-value pairs that were deleted. */
deleteRange(low: K, high: K, includeHigh: boolean): number;
// TypeScript requires these methods of ISortedMapSource to be repeated
entries(firstKey?: K): IterableIterator<[K,V]>;
keys(firstKey?: K): IterableIterator<K>;
values(firstKey?: K): IterableIterator<V>;
}
/** An interface for a functional set, in which the set object could be read-only
* but new versions of the set can be created by calling "with" or "without"
* methods to add or remove keys. This is a subinterface of IMapF<K,V>,
* so the items in the set may be referred to as "keys". */
export interface ISetF<K=any> extends ISetSource<K> {
/** Returns a copy of the set with the specified key included.
* @description You might wonder why this method accepts only one key
* instead of `...keys: K[]`. The reason is that the derived interface
* IMapF expects the second parameter to be a value. Therefore
* withKeys() is provided to set multiple keys at once. */
with(key: K): ISetF<K>;
/** Returns a copy of the set with the specified key removed. */
without(key: K): ISetF<K>;
/** Returns a copy of the tree with all the keys in the specified array present.
* @param keys The keys to add.
* @param returnThisIfUnchanged If true, the method returns `this` when
* all of the keys are already present in the collection. The
* default value may be true or false depending on the concrete
* implementation of the interface (in BTree, the default is false.) */
withKeys(keys: K[], returnThisIfUnchanged?: boolean): ISetF<K>;
/** Returns a copy of the tree with all the keys in the specified array removed. */
withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): ISetF<K>;
/** Returns a copy of the tree with items removed whenever the callback
* function returns false.
* @param callback A function to call for each item in the set.
* The second parameter to `callback` exists because ISetF
* is a subinterface of IMapF. If the object is a map, v
* is the value associated with the key, otherwise v could be
* undefined or another copy of the third parameter (counter). */
filter(callback: (k:K,v:any,counter:number) => boolean, returnThisIfUnchanged?: boolean): ISetF<K>;
}
/** An interface for a functional map, in which the map object could be read-only
* but new versions of the map can be created by calling "with" or "without"
* methods to add or remove keys or key-value pairs.
*/
export interface IMapF<K=any, V=any> extends IMapSource<K, V>, ISetF<K> {
/** Returns a copy of the tree with the specified key set (the value is undefined). */
with(key: K): IMapF<K,V|undefined>;
/** Returns a copy of the tree with the specified key-value pair set. */
with<V2>(key: K, value: V2, overwrite?: boolean): IMapF<K,V|V2>;
/** Returns a copy of the tree with the specified key-value pairs set. */
withPairs<V2>(pairs: [K,V|V2][], overwrite: boolean): IMapF<K,V|V2>;
/** Returns a copy of the tree with all the keys in the specified array present.
* @param keys The keys to add. If a key is already present in the tree,
* neither the existing key nor the existing value is modified.
* @param returnThisIfUnchanged If true, the method returns `this` when
* all of the keys are already present in the collection. The
* default value may be true or false depending on the concrete
* implementation of the interface (in BTree, the default is false.) */
withKeys(keys: K[], returnThisIfUnchanged?: boolean): IMapF<K,V|undefined>;
/** Returns a copy of the tree with all values altered by a callback function. */
mapValues<R>(callback: (v:V,k:K,counter:number) => R): IMapF<K,R>;
/** Performs a reduce operation like the `reduce` method of `Array`.
* It is used to combine all pairs into a single value, or perform conversions. */
reduce<R>(callback: (previous:R,currentPair:[K,V],counter:number,tree:IMapF<K,V>) => R, initialValue: R): R;
/** Performs a reduce operation like the `reduce` method of `Array`.
* It is used to combine all pairs into a single value, or perform conversions. */
reduce<R>(callback: (previous:R|undefined,currentPair:[K,V],counter:number,tree:IMapF<K,V>) => R): R|undefined;
// Update return types in ISetF
without(key: K): IMapF<K,V>;
withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): IMapF<K,V>;
/** Returns a copy of the tree with pairs removed whenever the callback
* function returns false. */
filter(callback: (k:K,v:V,counter:number) => boolean, returnThisIfUnchanged?: boolean): IMapF<K,V>;
}
/** An interface for a functional sorted set: a functional set in which the
* keys (items) are sorted. This is a subinterface of ISortedMapF. */
export interface ISortedSetF<K=any> extends ISetF<K>, ISortedSetSource<K>
{
// TypeScript requires this method of ISortedSetSource to be repeated
keys(firstKey?: K): IterableIterator<K>;
}
export interface ISortedMapF<K=any,V=any> extends ISortedSetF<K>, IMapF<K,V>, ISortedMapSource<K,V>
{
/** Returns a copy of the tree with the specified range of keys removed. */
withoutRange(low: K, high: K, includeHigh: boolean, returnThisIfUnchanged?: boolean): ISortedMapF<K,V>;
// TypeScript requires these methods of ISortedSetF and ISortedMapSource to be repeated
entries(firstKey?: K): IterableIterator<[K,V]>;
keys(firstKey?: K): IterableIterator<K>;
values(firstKey?: K): IterableIterator<V>;
forRange(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:V,counter:number) => void, initialCounter?: number): number;
// Update the return value of methods from base interfaces
with(key: K): ISortedMapF<K,V|undefined>;
with<V2>(key: K, value: V2, overwrite?: boolean): ISortedMapF<K,V|V2>;
withKeys(keys: K[], returnThisIfUnchanged?: boolean): ISortedMapF<K,V|undefined>;
withPairs<V2>(pairs: [K,V|V2][], overwrite: boolean): ISortedMapF<K,V|V2>;
mapValues<R>(callback: (v:V,k:K,counter:number) => R): ISortedMapF<K,R>;
without(key: K): ISortedMapF<K,V>;
withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): ISortedMapF<K,V>;
filter(callback: (k:K,v:any,counter:number) => boolean, returnThisIfUnchanged?: boolean): ISortedMapF<K,V>;
}
export interface ISortedMapConstructor<K,V> {
new (entries?: [K,V][], compare?: (a: K, b: K) => number): ISortedMap<K,V>;
}
export interface ISortedMapFConstructor<K,V> {
new (entries?: [K,V][], compare?: (a: K, b: K) => number): ISortedMapF<K,V>;
}

View File

@ -0,0 +1,80 @@
/*
Copyright 2017 Jeremy Scheff
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
import FakeEventTarget from "./FakeEventTarget";
import { EventType } from "./types";
class Event {
public eventPath: FakeEventTarget[] = [];
public type: EventType;
public readonly NONE = 0;
public readonly CAPTURING_PHASE = 1;
public readonly AT_TARGET = 2;
public readonly BUBBLING_PHASE = 3;
// Flags
public propagationStopped = false;
public immediatePropagationStopped = false;
public canceled = false;
public initialized = true;
public dispatched = false;
public target: FakeEventTarget | null = null;
public currentTarget: FakeEventTarget | null = null;
public eventPhase: 0 | 1 | 2 | 3 = 0;
public defaultPrevented = false;
public isTrusted = false;
public timeStamp = Date.now();
public bubbles: boolean;
public cancelable: boolean;
constructor(
type: EventType,
eventInitDict: { bubbles?: boolean; cancelable?: boolean } = {},
) {
this.type = type;
this.bubbles =
eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false;
this.cancelable =
eventInitDict.cancelable !== undefined
? eventInitDict.cancelable
: false;
}
public preventDefault() {
if (this.cancelable) {
this.canceled = true;
}
}
public stopPropagation() {
this.propagationStopped = true;
}
public stopImmediatePropagation() {
this.propagationStopped = true;
this.immediatePropagationStopped = true;
}
}
export default Event;

View File

@ -0,0 +1,177 @@
/*
Copyright 2017 Jeremy Scheff
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
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";
interface Listener {
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)
);
};
// http://www.w3.org/TR/dom/#concept-event-listener-invoke
const invokeEventListeners = (event: FakeEvent, obj: FakeEventTarget) => {
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);
}
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,
};
if (!stopped(event, listener)) {
// @ts-ignore
listener.callback.call(event.currentTarget, event);
}
}
};
abstract class FakeEventTarget {
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;
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;
for (const obj of event.eventPath) {
if (!event.propagationStopped) {
invokeEventListeners(event, obj);
}
}
event.eventPhase = event.AT_TARGET;
if (!event.propagationStopped) {
invokeEventListeners(event, event.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, obj);
}
}
}
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,34 @@
import { KeyPath, Value } from "./types";
// http://w3c.github.io/IndexedDB/#check-that-a-key-could-be-injected-into-a-value
const canInjectKey = (keyPath: KeyPath, value: Value) => {
if (Array.isArray(keyPath)) {
// tslint:disable-next-line max-line-length
throw new Error(
"The key paths used in this section are always strings and never sequences, since it is not possible to create a object store which has a key generator and also has a key path that is a sequence.",
);
}
const identifiers = keyPath.split(".");
if (identifiers.length === 0) {
throw new Error("Assert: identifiers is not empty");
}
identifiers.pop();
for (const identifier of identifiers) {
if (typeof value !== "object" && !Array.isArray(value)) {
return false;
}
const hop = value.hasOwnProperty(identifier);
if (!hop) {
return true;
}
value = value[identifier];
}
return typeof value === "object" || Array.isArray(value);
};
export default canInjectKey;

View File

@ -0,0 +1,108 @@
/*
Copyright 2017 Jeremy Scheff
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
import { DataError } from "./errors";
import valueToKey from "./valueToKey";
const getType = (x: any) => {
if (typeof x === "number") {
return "Number";
}
if (x instanceof Date) {
return "Date";
}
if (Array.isArray(x)) {
return "Array";
}
if (typeof x === "string") {
return "String";
}
if (x instanceof ArrayBuffer) {
return "Binary";
}
throw new DataError();
};
// https://w3c.github.io/IndexedDB/#compare-two-keys
const compareKeys = (first: any, second: any): -1 | 0 | 1 => {
if (second === undefined) {
throw new TypeError();
}
first = valueToKey(first);
second = valueToKey(second);
const t1 = getType(first);
const t2 = getType(second);
if (t1 !== t2) {
if (t1 === "Array") {
return 1;
}
if (
t1 === "Binary" &&
(t2 === "String" || t2 === "Date" || t2 === "Number")
) {
return 1;
}
if (t1 === "String" && (t2 === "Date" || t2 === "Number")) {
return 1;
}
if (t1 === "Date" && t2 === "Number") {
return 1;
}
return -1;
}
if (t1 === "Binary") {
first = new Uint8Array(first);
second = new Uint8Array(second);
}
if (t1 === "Array" || t1 === "Binary") {
const length = Math.min(first.length, second.length);
for (let i = 0; i < length; i++) {
const result = compareKeys(first[i], second[i]);
if (result !== 0) {
return result;
}
}
if (first.length > second.length) {
return 1;
}
if (first.length < second.length) {
return -1;
}
return 0;
}
if (t1 === "Date") {
if (first.getTime() === second.getTime()) {
return 0;
}
} else {
if (first === second) {
return 0;
}
}
return first > second ? 1 : -1;
};
export default compareKeys;

View File

@ -0,0 +1,75 @@
/*
Copyright (c) 2017 Evgeny Poberezkin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
const isArray = Array.isArray;
const keyList = Object.keys;
const hasProp = Object.prototype.hasOwnProperty;
function deepEquals(a: any, b: any): boolean {
if (a === b) return true;
if (a && b && typeof a == "object" && typeof b == "object") {
const arrA = isArray(a);
const arrB = isArray(b);
let i;
let length;
let key;
if (arrA && arrB) {
length = a.length;
if (length != b.length) return false;
for (i = length; i-- !== 0; ) if (!deepEquals(a[i], b[i])) return false;
return true;
}
if (arrA != arrB) return false;
const dateA = a instanceof Date;
const dateB = b instanceof Date;
if (dateA != dateB) return false;
if (dateA && dateB) return a.getTime() == b.getTime();
const regexpA = a instanceof RegExp;
const regexpB = b instanceof RegExp;
if (regexpA != regexpB) return false;
if (regexpA && regexpB) return a.toString() == b.toString();
const keys = keyList(a);
length = keys.length;
if (length !== keyList(b).length) return false;
for (i = length; i-- !== 0; ) if (!hasProp.call(b, keys[i])) return false;
for (i = length; i-- !== 0; ) {
key = keys[i];
if (!deepEquals(a[key], b[key])) return false;
}
return true;
}
return a !== a && b !== b;
}

View File

@ -0,0 +1,18 @@
// https://heycam.github.io/webidl/#EnforceRange
const enforceRange = (
num: number,
type: "MAX_SAFE_INTEGER" | "unsigned long",
) => {
const min = 0;
const max = type === "unsigned long" ? 4294967295 : 9007199254740991;
if (isNaN(num) || num < min || num > max) {
throw new TypeError();
}
if (num >= 0) {
return Math.floor(num);
}
};
export default enforceRange;

View File

@ -0,0 +1,120 @@
/*
Copyright 2017 Jeremy Scheff
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
/* tslint:disable: max-classes-per-file max-line-length */
const messages = {
AbortError:
"A request was aborted, for example through a call to IDBTransaction.abort.",
ConstraintError:
"A mutation operation in the transaction failed because a constraint was not satisfied. For example, an object such as an object store or index already exists and a request attempted to create a new one.",
DataCloneError:
"The data being stored could not be cloned by the internal structured cloning algorithm.",
DataError: "Data provided to an operation does not meet requirements.",
InvalidAccessError:
"An invalid operation was performed on an object. For example transaction creation attempt was made, but an empty scope was provided.",
InvalidStateError:
"An operation was called on an object on which it is not allowed or at a time when it is not allowed. Also occurs if a request is made on a source object that has been deleted or removed. Use TransactionInactiveError or ReadOnlyError when possible, as they are more specific variations of InvalidStateError.",
NotFoundError:
"The operation failed because the requested database object could not be found. For example, an object store did not exist but was being opened.",
ReadOnlyError:
'The mutating operation was attempted in a "readonly" transaction.',
TransactionInactiveError:
"A request was placed against a transaction which is currently not active, or which is finished.",
VersionError:
"An attempt was made to open a database using a lower version than the existing version.",
};
export class AbortError extends Error {
constructor(message = messages.AbortError) {
super();
this.name = "AbortError";
this.message = message;
}
}
export class ConstraintError extends Error {
constructor(message = messages.ConstraintError) {
super();
this.name = "ConstraintError";
this.message = message;
}
}
export class DataCloneError extends Error {
constructor(message = messages.DataCloneError) {
super();
this.name = "DataCloneError";
this.message = message;
}
}
export class DataError extends Error {
constructor(message = messages.DataError) {
super();
this.name = "DataError";
this.message = message;
}
}
export class InvalidAccessError extends Error {
constructor(message = messages.InvalidAccessError) {
super();
this.name = "InvalidAccessError";
this.message = message;
}
}
export class InvalidStateError extends Error {
constructor(message = messages.InvalidStateError) {
super();
this.name = "InvalidStateError";
this.message = message;
}
}
export class NotFoundError extends Error {
constructor(message = messages.NotFoundError) {
super();
this.name = "NotFoundError";
this.message = message;
}
}
export class ReadOnlyError extends Error {
constructor(message = messages.ReadOnlyError) {
super();
this.name = "ReadOnlyError";
this.message = message;
}
}
export class TransactionInactiveError extends Error {
constructor(message = messages.TransactionInactiveError) {
super();
this.name = "TransactionInactiveError";
this.message = message;
}
}
export class VersionError extends Error {
constructor(message = messages.VersionError) {
super();
this.name = "VersionError";
this.message = message;
}
}

View File

@ -0,0 +1,55 @@
import { Key, KeyPath, Value } from "./types";
import valueToKey from "./valueToKey";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-extracting-a-key-from-a-value-using-a-key-path
const extractKey = (keyPath: KeyPath, value: Value) => {
if (Array.isArray(keyPath)) {
const result: Key[] = [];
for (let item of keyPath) {
// This doesn't make sense to me based on the spec, but it is needed to pass the W3C KeyPath tests (see same
// comment in validateKeyPath)
if (
item !== undefined &&
item !== null &&
typeof item !== "string" &&
(item as any).toString
) {
item = (item as any).toString();
}
result.push(valueToKey(extractKey(item, value)));
}
return result;
}
if (keyPath === "") {
return value;
}
let remainingKeyPath: string | null = keyPath;
let object = value;
while (remainingKeyPath !== null) {
let identifier;
const i = remainingKeyPath.indexOf(".");
if (i >= 0) {
identifier = remainingKeyPath.slice(0, i);
remainingKeyPath = remainingKeyPath.slice(i + 1);
} else {
identifier = remainingKeyPath;
remainingKeyPath = null;
}
if (!object.hasOwnProperty(identifier)) {
return;
}
object = object[identifier];
}
return object;
};
export default extractKey;

View File

@ -0,0 +1,37 @@
/*
* Copyright 2017 Jeremy Scheff
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import { FakeDOMStringList } from "./types";
// Would be nicer to sublcass Array, but I'd have to sacrifice Node 4 support to do that.
const fakeDOMStringList = (arr: string[]): FakeDOMStringList => {
const arr2 = arr.slice();
Object.defineProperty(arr2, "contains", {
// tslint:disable-next-line object-literal-shorthand
value: (value: string) => arr2.indexOf(value) >= 0,
});
Object.defineProperty(arr2, "item", {
// tslint:disable-next-line object-literal-shorthand
value: (i: number) => arr2[i],
});
return arr2 as FakeDOMStringList;
};
export default fakeDOMStringList;

View File

@ -0,0 +1,48 @@
import { KeyPath, Value, Key } from "./types";
import canInjectKey from "./canInjectKey";
import { DataError } from "./errors";
import structuredClone from "./structuredClone";
export function injectKey(keyPath: KeyPath, value: Value, key: Key): Value {
if (Array.isArray(keyPath)) {
// tslint:disable-next-line max-line-length
throw new Error(
"The key paths used in this section are always strings and never sequences, since it is not possible to create a object store which has a key generator and also has a key path that is a sequence.",
);
}
const identifiers = keyPath.split(".");
if (identifiers.length === 0) {
throw new Error("Assert: identifiers is not empty");
}
const lastIdentifier = identifiers.pop();
if (lastIdentifier === null || lastIdentifier === undefined) {
throw Error();
}
for (const identifier of identifiers) {
if (typeof value !== "object" && !Array.isArray(value)) {
return false;
}
const hop = value.hasOwnProperty(identifier);
if (!hop) {
return true;
}
value = value[identifier];
}
if (!(typeof value === "object" || Array.isArray(value))) {
throw new Error("can't inject key");
}
const newValue = structuredClone(value);
newValue[lastIdentifier] = structuredClone(key);
return newValue;
}
export default injectKey;

View File

@ -0,0 +1,92 @@
import { Value, Key, KeyPath } from "./types";
import extractKey from "./extractKey";
import { DataError } from "./errors";
import valueToKey from "./valueToKey";
import structuredClone from "./structuredClone";
import injectKey from "./injectKey";
export interface StoreKeyResult {
updatedKeyGenerator: number;
key: Key;
value: Value;
}
export function makeStoreKeyValue(
value: Value,
key: Key | undefined,
currentKeyGenerator: number,
autoIncrement: boolean,
keyPath: KeyPath | null,
): StoreKeyResult {
const haveKey = key !== undefined && key !== null;
const haveKeyPath = keyPath !== null && keyPath !== undefined;
// This models a decision table on (haveKey, haveKeyPath, autoIncrement)
value = structuredClone(value);
if (haveKey) {
if (haveKeyPath) {
// (yes, yes, no)
// (yes, yes, yes)
throw new DataError();
} else {
if (autoIncrement) {
// (yes, no, yes)
key = valueToKey(key)!;
let updatedKeyGenerator: number;
if (typeof key !== "number") {
updatedKeyGenerator = currentKeyGenerator;
} else {
updatedKeyGenerator = key;
}
return {
key: key!,
value: value,
updatedKeyGenerator,
};
} else {
// (yes, no, no)
throw new DataError();
}
}
} else {
if (haveKeyPath) {
if (autoIncrement) {
// (no, yes, yes)
let updatedKeyGenerator: number;
const maybeInlineKey = extractKey(keyPath!, value);
if (maybeInlineKey === undefined) {
value = injectKey(keyPath!, value, currentKeyGenerator);
key = currentKeyGenerator;
updatedKeyGenerator = currentKeyGenerator + 1;
} else if (typeof maybeInlineKey === "number") {
key = maybeInlineKey;
updatedKeyGenerator = maybeInlineKey;
} else {
key = maybeInlineKey;
updatedKeyGenerator = currentKeyGenerator + 1;
}
return {
key: key,
value: value,
updatedKeyGenerator,
}
} else {
// (no, yes, no)
key = extractKey(keyPath!, value);
key = valueToKey(key);
return {
key: key!,
value: value,
updatedKeyGenerator: currentKeyGenerator,
};
}
} else {
// (no, no, yes)
// (no, no, no)
throw new DataError();
}
}
}

View File

@ -0,0 +1,22 @@
function openPromise<T>(): {
promise: Promise<T>;
resolve: (v?: T | PromiseLike<T>) => void;
reject: (err?: any) => void;
} {
let resolve;
let reject;
const promise = new Promise<T>((resolve2, reject2) => {
resolve = resolve2;
reject = reject2;
});
if (!resolve) {
throw Error("broken invariant");
}
if (!reject) {
throw Error("broken invariant");
}
return { promise, resolve, reject };
}
export default openPromise;

View File

@ -0,0 +1,21 @@
/*
Copyright 2019 Florian Dold
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
export function queueTask(fn: () => void) {
setImmediate(fn);
}
export default queueTask;

View File

@ -0,0 +1,15 @@
function structuredCloneImpl(val: any, visited: WeakMap<any, boolean>): any {
// FIXME: replace with real implementation!
return JSON.parse(JSON.stringify(val));
}
/**
* Structured clone for IndexedDB.
*/
export function structuredClone(val: any): any {
const visited: WeakMap<any, boolean> = new WeakMap<any, boolean>();
return structuredCloneImpl(val, visited);
}
export default structuredClone;

View File

@ -0,0 +1,73 @@
/*
Copyright 2017 Jeremy Scheff
Copyright 2019 Florian Dold
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
import BridgeIDBRequest from "../BridgeIDBRequest";
import BridgeIDBKeyRange from "../BridgeIDBKeyRange";
import BridgeIDBIndex from "../BridgeIDBIndex";
import BridgeIBObjectStore from "../BridgeIDBObjectStore";
interface EventInCallback extends Event {
target: any;
error: Error | null;
}
export type EventCallback = (event: EventInCallback) => void;
export type EventType =
| "abort"
| "blocked"
| "complete"
| "error"
| "success"
| "upgradeneeded"
| "versionchange";
export type CursorSource = BridgeIDBIndex | BridgeIBObjectStore;
export interface FakeDOMStringList extends Array<string> {
contains: (value: string) => boolean;
item: (i: number) => string | undefined;
}
export type BridgeIDBCursorDirection = "next" | "nextunique" | "prev" | "prevunique";
export type KeyPath = string | string[];
export type Key = any;
export type CursorRange = Key | BridgeIDBKeyRange | undefined;
export type Value = any;
export interface Record {
key: Key;
value: Key | Value; // For indexes, will be Key. For object stores, will be Value.
}
export type TransactionMode = "readonly" | "readwrite" | "versionchange";
export interface BridgeIDBDatabaseInfo {
name: string;
version: number
};
export interface RequestObj {
operation: () => Promise<any>;
request?: BridgeIDBRequest | undefined;
source?: any;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,70 @@
/*
Copyright 2017 Jeremy Scheff
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing
permissions and limitations under the License.
*/
import { DataError } from "./errors";
import { Key } from "./types";
// https://w3c.github.io/IndexedDB/#convert-a-value-to-a-input
function valueToKey(input: any, seen?: Set<object>): Key | Key[] {
if (typeof input === "number") {
if (isNaN(input)) {
throw new DataError();
}
return input;
} else if (input instanceof Date) {
const ms = input.valueOf();
if (isNaN(ms)) {
throw new DataError();
}
return new Date(ms);
} else if (typeof input === "string") {
return input;
} else if (
input instanceof ArrayBuffer ||
(typeof ArrayBuffer !== "undefined" &&
ArrayBuffer.isView &&
ArrayBuffer.isView(input))
) {
if (input instanceof ArrayBuffer) {
return new Uint8Array(input).buffer;
}
return new Uint8Array(input.buffer).buffer;
} else if (Array.isArray(input)) {
if (seen === undefined) {
seen = new Set();
} else if (seen.has(input)) {
throw new DataError();
}
seen.add(input);
const keys = [];
for (let i = 0; i < input.length; i++) {
const hop = input.hasOwnProperty(i);
if (!hop) {
throw new DataError();
}
const entry = input[i];
const key = valueToKey(entry, seen);
keys.push(key);
}
return keys;
} else {
throw new DataError();
}
};
export default valueToKey;

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"lib": ["es6"],
"module": "commonjs",
"target": "es5",
"noImplicitAny": true,
"sourceMap": false,
"outDir": "build",
"noEmitOnError": true,
"strict": true,
"incremental": true
},
"include": ["src/**/*"]
}

File diff suppressed because it is too large Load Diff