idb wip
This commit is contained in:
parent
65eb8b96f8
commit
2ee9431f1b
6
packages/idb-bridge/.prettierrc
Normal file
6
packages/idb-bridge/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
}
|
18
packages/idb-bridge/README.md
Normal file
18
packages/idb-bridge/README.md
Normal 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).
|
19
packages/idb-bridge/package.json
Normal file
19
packages/idb-bridge/package.json
Normal 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"
|
||||
}
|
||||
}
|
315
packages/idb-bridge/src/BridgeIDBCursor.ts
Normal file
315
packages/idb-bridge/src/BridgeIDBCursor.ts
Normal 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;
|
44
packages/idb-bridge/src/BridgeIDBCursorWithValue.ts
Normal file
44
packages/idb-bridge/src/BridgeIDBCursorWithValue.ts
Normal 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;
|
239
packages/idb-bridge/src/BridgeIDBDatabase.ts
Normal file
239
packages/idb-bridge/src/BridgeIDBDatabase.ts
Normal 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;
|
192
packages/idb-bridge/src/BridgeIDBFactory.ts
Normal file
192
packages/idb-bridge/src/BridgeIDBFactory.ts
Normal 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;
|
316
packages/idb-bridge/src/BridgeIDBIndex.ts
Normal file
316
packages/idb-bridge/src/BridgeIDBIndex.ts
Normal 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;
|
133
packages/idb-bridge/src/BridgeIDBKeyRange.ts
Normal file
133
packages/idb-bridge/src/BridgeIDBKeyRange.ts
Normal 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;
|
441
packages/idb-bridge/src/BridgeIDBObjectStore.ts
Normal file
441
packages/idb-bridge/src/BridgeIDBObjectStore.ts
Normal 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;
|
36
packages/idb-bridge/src/BridgeIDBOpenDBRequest.ts
Normal file
36
packages/idb-bridge/src/BridgeIDBOpenDBRequest.ts
Normal 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;
|
86
packages/idb-bridge/src/BridgeIDBRequest.ts
Normal file
86
packages/idb-bridge/src/BridgeIDBRequest.ts
Normal 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;
|
301
packages/idb-bridge/src/BridgeIDBTransaction.ts
Normal file
301
packages/idb-bridge/src/BridgeIDBTransaction.ts
Normal 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;
|
41
packages/idb-bridge/src/BridgeIDBVersionChangeEvent.ts
Normal file
41
packages/idb-bridge/src/BridgeIDBVersionChangeEvent.ts
Normal 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;
|
31
packages/idb-bridge/src/MemoryBackend.test.ts
Normal file
31
packages/idb-bridge/src/MemoryBackend.test.ts
Normal 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();
|
||||
};
|
||||
|
||||
});
|
662
packages/idb-bridge/src/MemoryBackend.ts
Normal file
662
packages/idb-bridge/src/MemoryBackend.ts
Normal 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;
|
145
packages/idb-bridge/src/backend-interface.ts
Normal file
145
packages/idb-bridge/src/backend-interface.ts
Normal 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>;
|
||||
}
|
1351
packages/idb-bridge/src/tree/b+tree.ts
Normal file
1351
packages/idb-bridge/src/tree/b+tree.ts
Normal file
File diff suppressed because it is too large
Load Diff
329
packages/idb-bridge/src/tree/interfaces.ts
Normal file
329
packages/idb-bridge/src/tree/interfaces.ts
Normal 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>;
|
||||
}
|
80
packages/idb-bridge/src/util/FakeEvent.ts
Normal file
80
packages/idb-bridge/src/util/FakeEvent.ts
Normal 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;
|
177
packages/idb-bridge/src/util/FakeEventTarget.ts
Normal file
177
packages/idb-bridge/src/util/FakeEventTarget.ts
Normal 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;
|
34
packages/idb-bridge/src/util/canInjectKey.ts
Normal file
34
packages/idb-bridge/src/util/canInjectKey.ts
Normal 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;
|
108
packages/idb-bridge/src/util/cmp.ts
Normal file
108
packages/idb-bridge/src/util/cmp.ts
Normal 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;
|
75
packages/idb-bridge/src/util/deepEquals.ts
Normal file
75
packages/idb-bridge/src/util/deepEquals.ts
Normal 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;
|
||||
}
|
18
packages/idb-bridge/src/util/enforceRange.ts
Normal file
18
packages/idb-bridge/src/util/enforceRange.ts
Normal 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;
|
120
packages/idb-bridge/src/util/errors.ts
Normal file
120
packages/idb-bridge/src/util/errors.ts
Normal 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;
|
||||
}
|
||||
}
|
55
packages/idb-bridge/src/util/extractKey.ts
Normal file
55
packages/idb-bridge/src/util/extractKey.ts
Normal 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;
|
37
packages/idb-bridge/src/util/fakeDOMStringList.ts
Normal file
37
packages/idb-bridge/src/util/fakeDOMStringList.ts
Normal 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;
|
48
packages/idb-bridge/src/util/injectKey.ts
Normal file
48
packages/idb-bridge/src/util/injectKey.ts
Normal 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;
|
92
packages/idb-bridge/src/util/makeStoreKeyValue.ts
Normal file
92
packages/idb-bridge/src/util/makeStoreKeyValue.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
22
packages/idb-bridge/src/util/openPromise.ts
Normal file
22
packages/idb-bridge/src/util/openPromise.ts
Normal 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;
|
21
packages/idb-bridge/src/util/queueTask.ts
Normal file
21
packages/idb-bridge/src/util/queueTask.ts
Normal 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;
|
15
packages/idb-bridge/src/util/structuredClone.ts
Normal file
15
packages/idb-bridge/src/util/structuredClone.ts
Normal 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;
|
73
packages/idb-bridge/src/util/types.ts
Normal file
73
packages/idb-bridge/src/util/types.ts
Normal 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;
|
||||
}
|
77
packages/idb-bridge/src/util/validateKeyPath.ts
Normal file
77
packages/idb-bridge/src/util/validateKeyPath.ts
Normal file
File diff suppressed because one or more lines are too long
70
packages/idb-bridge/src/util/valueToKey.ts
Normal file
70
packages/idb-bridge/src/util/valueToKey.ts
Normal 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;
|
14
packages/idb-bridge/tsconfig.json
Normal file
14
packages/idb-bridge/tsconfig.json
Normal 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/**/*"]
|
||||
}
|
3626
packages/idb-bridge/yarn.lock
Normal file
3626
packages/idb-bridge/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user