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