2019-08-01 23:27:42 +02:00
|
|
|
/*
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2019-06-15 22:44:54 +02:00
|
|
|
import {
|
|
|
|
Backend,
|
|
|
|
DatabaseConnection,
|
|
|
|
DatabaseTransaction,
|
|
|
|
Schema,
|
|
|
|
RecordStoreRequest,
|
|
|
|
IndexProperties,
|
2019-06-21 19:18:36 +02:00
|
|
|
RecordGetRequest,
|
|
|
|
RecordGetResponse,
|
|
|
|
ResultLevel,
|
2019-07-31 01:33:23 +02:00
|
|
|
StoreLevel,
|
2019-08-01 23:21:05 +02:00
|
|
|
RecordStoreResponse,
|
2022-02-10 19:52:45 +01:00
|
|
|
} from "./backend-interface.js";
|
2019-06-21 19:18:36 +02:00
|
|
|
import {
|
2021-02-24 17:33:07 +01:00
|
|
|
structuredClone,
|
|
|
|
structuredEncapsulate,
|
|
|
|
structuredRevive,
|
2022-02-10 19:52:45 +01:00
|
|
|
} from "./util/structuredClone.js";
|
|
|
|
import { ConstraintError, DataError } from "./util/errors.js";
|
|
|
|
import BTree, { ISortedMapF, ISortedSetF } from "./tree/b+tree.js";
|
|
|
|
import { compareKeys } from "./util/cmp.js";
|
|
|
|
import { StoreKeyResult, makeStoreKeyValue } from "./util/makeStoreKeyValue.js";
|
|
|
|
import { getIndexKeys } from "./util/getIndexKeys.js";
|
|
|
|
import { openPromise } from "./util/openPromise.js";
|
|
|
|
import { IDBKeyRange, IDBTransactionMode, IDBValidKey } from "./idbtypes.js";
|
|
|
|
import { BridgeIDBKeyRange } from "./bridge-idb.js";
|
2021-02-08 15:23:44 +01:00
|
|
|
|
|
|
|
type Key = IDBValidKey;
|
|
|
|
type Value = unknown;
|
2019-06-15 22:44:54 +02:00
|
|
|
|
|
|
|
enum TransactionLevel {
|
2021-02-17 17:38:47 +01:00
|
|
|
None = 0,
|
|
|
|
Read = 1,
|
|
|
|
Write = 2,
|
|
|
|
VersionChange = 3,
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface ObjectStore {
|
|
|
|
originalName: string;
|
|
|
|
modifiedName: string | undefined;
|
2019-06-21 19:18:36 +02:00
|
|
|
originalData: ISortedMapF<Key, ObjectStoreRecord>;
|
|
|
|
modifiedData: ISortedMapF<Key, ObjectStoreRecord> | undefined;
|
2019-06-15 22:44:54 +02:00
|
|
|
deleted: boolean;
|
|
|
|
originalKeyGenerator: number;
|
|
|
|
modifiedKeyGenerator: number | undefined;
|
2019-08-17 01:03:55 +02:00
|
|
|
committedIndexes: { [name: string]: Index };
|
|
|
|
modifiedIndexes: { [name: string]: Index };
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface Index {
|
|
|
|
originalName: string;
|
|
|
|
modifiedName: string | undefined;
|
2019-06-21 19:18:36 +02:00
|
|
|
originalData: ISortedMapF<Key, IndexRecord>;
|
|
|
|
modifiedData: ISortedMapF<Key, IndexRecord> | undefined;
|
2019-06-15 22:44:54 +02:00
|
|
|
deleted: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Database {
|
|
|
|
committedObjectStores: { [name: string]: ObjectStore };
|
|
|
|
committedSchema: Schema;
|
|
|
|
/**
|
|
|
|
* Was the transaction deleted during the running transaction?
|
|
|
|
*/
|
|
|
|
deleted: boolean;
|
|
|
|
|
|
|
|
txLevel: TransactionLevel;
|
|
|
|
|
2021-02-17 17:38:47 +01:00
|
|
|
txOwnerConnectionCookie?: string;
|
|
|
|
txOwnerTransactionCookie?: string;
|
|
|
|
|
2019-11-29 19:25:48 +01:00
|
|
|
/**
|
|
|
|
* Object stores that the transaction is allowed to access.
|
|
|
|
*/
|
|
|
|
txRestrictObjectStores: string[] | undefined;
|
|
|
|
|
2021-02-17 17:38:47 +01:00
|
|
|
/**
|
|
|
|
* Connection cookies of current connections.
|
|
|
|
*/
|
|
|
|
connectionCookies: string[];
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
2020-08-10 11:07:20 +02:00
|
|
|
/** @public */
|
|
|
|
export interface ObjectStoreDump {
|
2019-08-16 19:05:48 +02:00
|
|
|
name: string;
|
2019-08-17 01:03:55 +02:00
|
|
|
keyGenerator: number;
|
|
|
|
records: ObjectStoreRecord[];
|
2019-08-16 19:05:48 +02:00
|
|
|
}
|
|
|
|
|
2020-08-10 11:07:20 +02:00
|
|
|
/** @public */
|
|
|
|
export interface DatabaseDump {
|
2019-08-16 19:05:48 +02:00
|
|
|
schema: Schema;
|
|
|
|
objectStores: { [name: string]: ObjectStoreDump };
|
|
|
|
}
|
|
|
|
|
2020-08-10 11:07:20 +02:00
|
|
|
/** @public */
|
|
|
|
export interface MemoryBackendDump {
|
2019-08-16 19:05:48 +02:00
|
|
|
databases: { [name: string]: DatabaseDump };
|
|
|
|
}
|
|
|
|
|
2019-08-17 01:03:55 +02:00
|
|
|
interface ObjectStoreMapEntry {
|
|
|
|
store: ObjectStore;
|
|
|
|
indexMap: { [currentName: string]: Index };
|
|
|
|
}
|
|
|
|
|
2019-06-15 22:44:54 +02:00
|
|
|
interface Connection {
|
|
|
|
dbName: string;
|
|
|
|
|
2019-08-16 23:06:51 +02:00
|
|
|
modifiedSchema: Schema;
|
2019-06-15 22:44:54 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Map from the effective name of an object store during
|
|
|
|
* the transaction to the real name.
|
|
|
|
*/
|
2019-08-17 01:03:55 +02:00
|
|
|
objectStoreMap: { [currentName: string]: ObjectStoreMapEntry };
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
2020-08-10 11:07:20 +02:00
|
|
|
/** @public */
|
|
|
|
export interface IndexRecord {
|
2019-06-21 19:18:36 +02:00
|
|
|
indexKey: Key;
|
2021-12-15 02:37:03 +01:00
|
|
|
primaryKeys: ISortedSetF<Key>;
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2020-08-10 11:07:20 +02:00
|
|
|
/** @public */
|
|
|
|
export interface ObjectStoreRecord {
|
2019-06-21 19:18:36 +02:00
|
|
|
primaryKey: Key;
|
|
|
|
value: Value;
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
class AsyncCondition {
|
|
|
|
_waitPromise: Promise<void>;
|
|
|
|
_resolveWaitPromise: () => void;
|
|
|
|
constructor() {
|
|
|
|
const op = openPromise<void>();
|
|
|
|
this._waitPromise = op.promise;
|
|
|
|
this._resolveWaitPromise = op.resolve;
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
wait(): Promise<void> {
|
|
|
|
return this._waitPromise;
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
trigger(): void {
|
|
|
|
this._resolveWaitPromise();
|
|
|
|
const op = openPromise<void>();
|
|
|
|
this._waitPromise = op.promise;
|
|
|
|
this._resolveWaitPromise = op.resolve;
|
|
|
|
}
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
function nextStoreKey<T>(
|
|
|
|
forward: boolean,
|
|
|
|
data: ISortedMapF<Key, ObjectStoreRecord>,
|
|
|
|
k: Key | undefined,
|
2019-06-15 22:44:54 +02:00
|
|
|
) {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (k === undefined || k === null) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const res = forward ? data.nextHigherPair(k) : data.nextLowerPair(k);
|
|
|
|
if (!res) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
return res[1].primaryKey;
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2021-12-15 02:37:03 +01:00
|
|
|
function assertInvariant(cond: boolean): asserts cond {
|
|
|
|
if (!cond) {
|
|
|
|
throw Error("invariant failed");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function nextKey(
|
|
|
|
forward: boolean,
|
|
|
|
tree: ISortedSetF<IDBValidKey>,
|
|
|
|
key: IDBValidKey | undefined,
|
|
|
|
): IDBValidKey | undefined {
|
|
|
|
if (key != null) {
|
|
|
|
return forward ? tree.nextHigherKey(key) : tree.nextLowerKey(key);
|
|
|
|
}
|
|
|
|
return forward ? tree.minKey() : tree.maxKey();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the key that is furthest in
|
|
|
|
* the direction indicated by the 'forward' flag.
|
|
|
|
*/
|
2019-06-23 22:16:03 +02:00
|
|
|
function furthestKey(
|
|
|
|
forward: boolean,
|
|
|
|
key1: Key | undefined,
|
|
|
|
key2: Key | undefined,
|
|
|
|
) {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (key1 === undefined) {
|
|
|
|
return key2;
|
|
|
|
}
|
|
|
|
if (key2 === undefined) {
|
|
|
|
return key1;
|
|
|
|
}
|
|
|
|
const cmpResult = compareKeys(key1, key2);
|
|
|
|
if (cmpResult === 0) {
|
|
|
|
// Same result
|
|
|
|
return key1;
|
|
|
|
}
|
|
|
|
if (forward && cmpResult === 1) {
|
|
|
|
return key1;
|
|
|
|
}
|
|
|
|
if (forward && cmpResult === -1) {
|
|
|
|
return key2;
|
|
|
|
}
|
|
|
|
if (!forward && cmpResult === 1) {
|
|
|
|
return key2;
|
|
|
|
}
|
|
|
|
if (!forward && cmpResult === -1) {
|
|
|
|
return key1;
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-11 21:00:12 +01:00
|
|
|
export interface AccessStats {
|
|
|
|
writeTransactions: number;
|
|
|
|
readTransactions: number;
|
|
|
|
writesPerStore: Record<string, number>;
|
|
|
|
readsPerStore: Record<string, number>;
|
|
|
|
readsPerIndex: Record<string, number>;
|
|
|
|
readItemsPerIndex: Record<string, number>;
|
|
|
|
readItemsPerStore: Record<string, number>;
|
|
|
|
}
|
|
|
|
|
2019-06-15 22:44:54 +02:00
|
|
|
/**
|
|
|
|
* Primitive in-memory backend.
|
2020-08-10 11:07:20 +02:00
|
|
|
*
|
|
|
|
* @public
|
2019-06-15 22:44:54 +02:00
|
|
|
*/
|
|
|
|
export class MemoryBackend implements Backend {
|
2019-08-16 19:05:48 +02:00
|
|
|
private databases: { [name: string]: Database } = {};
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-08-16 19:05:48 +02:00
|
|
|
private connectionIdCounter = 1;
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-08-16 19:05:48 +02:00
|
|
|
private transactionIdCounter = 1;
|
2019-06-15 22:44:54 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Connections by connection cookie.
|
|
|
|
*/
|
2019-08-16 19:05:48 +02:00
|
|
|
private connections: { [name: string]: Connection } = {};
|
2019-06-15 22:44:54 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Connections by transaction (!!) cookie. In this implementation,
|
|
|
|
* at most one transaction can run at the same time per connection.
|
|
|
|
*/
|
2019-08-16 19:05:48 +02:00
|
|
|
private connectionsByTransaction: { [tx: string]: Connection } = {};
|
2019-06-15 22:44:54 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Condition that is triggered whenever a client disconnects.
|
|
|
|
*/
|
2019-08-16 19:05:48 +02:00
|
|
|
private disconnectCond: AsyncCondition = new AsyncCondition();
|
2019-06-15 22:44:54 +02:00
|
|
|
|
|
|
|
/**
|
2021-02-17 17:38:47 +01:00
|
|
|
* Condition that is triggered whenever a transaction finishes.
|
2019-06-15 22:44:54 +02:00
|
|
|
*/
|
2019-08-16 19:05:48 +02:00
|
|
|
private transactionDoneCond: AsyncCondition = new AsyncCondition();
|
|
|
|
|
|
|
|
afterCommitCallback?: () => Promise<void>;
|
|
|
|
|
|
|
|
enableTracing: boolean = false;
|
|
|
|
|
2022-01-11 21:00:12 +01:00
|
|
|
trackStats: boolean = true;
|
|
|
|
|
|
|
|
accessStats: AccessStats = {
|
|
|
|
readTransactions: 0,
|
|
|
|
writeTransactions: 0,
|
|
|
|
readsPerStore: {},
|
|
|
|
readsPerIndex: {},
|
|
|
|
readItemsPerIndex: {},
|
|
|
|
readItemsPerStore: {},
|
|
|
|
writesPerStore: {},
|
|
|
|
};
|
|
|
|
|
2019-08-16 19:05:48 +02:00
|
|
|
/**
|
|
|
|
* Load the data in this IndexedDB backend from a dump in JSON format.
|
|
|
|
*
|
|
|
|
* Must be called before any connections to the database backend have
|
|
|
|
* been made.
|
|
|
|
*/
|
2021-12-15 02:37:03 +01:00
|
|
|
importDump(dataJson: any) {
|
2019-08-16 19:05:48 +02:00
|
|
|
if (this.transactionIdCounter != 1 || this.connectionIdCounter != 1) {
|
|
|
|
throw Error(
|
|
|
|
"data must be imported before first transaction or connection",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-12-15 02:37:03 +01:00
|
|
|
// FIXME: validate!
|
|
|
|
const data = structuredRevive(dataJson) as MemoryBackendDump;
|
|
|
|
|
2021-02-24 17:33:07 +01:00
|
|
|
if (typeof data !== "object") {
|
|
|
|
throw Error("db dump corrupt");
|
|
|
|
}
|
|
|
|
|
2019-08-16 19:05:48 +02:00
|
|
|
this.databases = {};
|
|
|
|
|
|
|
|
for (const dbName of Object.keys(data.databases)) {
|
|
|
|
const schema = data.databases[dbName].schema;
|
|
|
|
if (typeof schema !== "object") {
|
|
|
|
throw Error("DB dump corrupt");
|
|
|
|
}
|
|
|
|
const objectStores: { [name: string]: ObjectStore } = {};
|
2019-08-26 02:41:50 +02:00
|
|
|
for (const objectStoreName of Object.keys(
|
|
|
|
data.databases[dbName].objectStores,
|
|
|
|
)) {
|
2021-12-15 02:37:03 +01:00
|
|
|
const storeSchema = schema.objectStores[objectStoreName];
|
|
|
|
const dumpedObjectStore: ObjectStoreDump =
|
2019-08-26 02:41:50 +02:00
|
|
|
data.databases[dbName].objectStores[objectStoreName];
|
2019-08-17 01:03:55 +02:00
|
|
|
|
2019-08-16 19:05:48 +02:00
|
|
|
const pairs = dumpedObjectStore.records.map((r: any) => {
|
|
|
|
return structuredClone([r.primaryKey, r]);
|
|
|
|
});
|
2019-08-26 02:41:50 +02:00
|
|
|
const objectStoreData: ISortedMapF<Key, ObjectStoreRecord> = new BTree(
|
|
|
|
pairs,
|
|
|
|
compareKeys,
|
|
|
|
);
|
2019-08-16 19:05:48 +02:00
|
|
|
const objectStore: ObjectStore = {
|
|
|
|
deleted: false,
|
|
|
|
modifiedData: undefined,
|
|
|
|
modifiedName: undefined,
|
|
|
|
modifiedKeyGenerator: undefined,
|
|
|
|
originalData: objectStoreData,
|
|
|
|
originalName: objectStoreName,
|
|
|
|
originalKeyGenerator: dumpedObjectStore.keyGenerator,
|
2021-12-15 02:37:03 +01:00
|
|
|
committedIndexes: {},
|
2019-08-17 01:03:55 +02:00
|
|
|
modifiedIndexes: {},
|
2019-08-26 02:41:50 +02:00
|
|
|
};
|
2019-08-16 19:05:48 +02:00
|
|
|
objectStores[objectStoreName] = objectStore;
|
2021-12-15 02:37:03 +01:00
|
|
|
|
|
|
|
for (const indexName in storeSchema.indexes) {
|
|
|
|
const indexSchema = storeSchema.indexes[indexName];
|
|
|
|
const newIndex: Index = {
|
|
|
|
deleted: false,
|
|
|
|
modifiedData: undefined,
|
|
|
|
modifiedName: undefined,
|
|
|
|
originalData: new BTree([], compareKeys),
|
|
|
|
originalName: indexName,
|
|
|
|
};
|
2021-12-23 11:41:45 +01:00
|
|
|
objectStore.committedIndexes[indexName] = newIndex;
|
|
|
|
objectStoreData.forEach((v, k) => {
|
2021-12-15 02:37:03 +01:00
|
|
|
try {
|
|
|
|
this.insertIntoIndex(newIndex, k, v.value, indexSchema);
|
|
|
|
} catch (e) {
|
|
|
|
if (e instanceof DataError) {
|
|
|
|
// We don't propagate this error here.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2019-08-16 19:05:48 +02:00
|
|
|
}
|
|
|
|
const db: Database = {
|
|
|
|
deleted: false,
|
|
|
|
committedObjectStores: objectStores,
|
|
|
|
committedSchema: structuredClone(schema),
|
2021-02-17 17:38:47 +01:00
|
|
|
connectionCookies: [],
|
|
|
|
txLevel: TransactionLevel.None,
|
2019-11-29 19:25:48 +01:00
|
|
|
txRestrictObjectStores: undefined,
|
2019-08-16 19:05:48 +02:00
|
|
|
};
|
|
|
|
this.databases[dbName] = db;
|
|
|
|
}
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-08-26 02:41:50 +02:00
|
|
|
private makeObjectStoreMap(
|
|
|
|
database: Database,
|
|
|
|
): { [currentName: string]: ObjectStoreMapEntry } {
|
|
|
|
let map: { [currentName: string]: ObjectStoreMapEntry } = {};
|
2019-08-17 01:03:55 +02:00
|
|
|
for (let objectStoreName in database.committedObjectStores) {
|
|
|
|
const store = database.committedObjectStores[objectStoreName];
|
|
|
|
const entry: ObjectStoreMapEntry = {
|
|
|
|
store,
|
2019-08-26 02:41:50 +02:00
|
|
|
indexMap: Object.assign({}, store.committedIndexes),
|
2019-08-17 01:03:55 +02:00
|
|
|
};
|
|
|
|
map[objectStoreName] = entry;
|
|
|
|
}
|
|
|
|
return map;
|
|
|
|
}
|
|
|
|
|
2019-08-16 19:05:48 +02:00
|
|
|
/**
|
|
|
|
* Export the contents of the database to JSON.
|
|
|
|
*
|
|
|
|
* Only exports data that has been committed.
|
|
|
|
*/
|
|
|
|
exportDump(): MemoryBackendDump {
|
2019-08-17 01:50:51 +02:00
|
|
|
this.enableTracing && console.log("exporting dump");
|
2019-08-16 19:05:48 +02:00
|
|
|
const dbDumps: { [name: string]: DatabaseDump } = {};
|
|
|
|
for (const dbName of Object.keys(this.databases)) {
|
|
|
|
const db = this.databases[dbName];
|
|
|
|
const objectStores: { [name: string]: ObjectStoreDump } = {};
|
|
|
|
for (const objectStoreName of Object.keys(db.committedObjectStores)) {
|
|
|
|
const objectStore = db.committedObjectStores[objectStoreName];
|
|
|
|
const objectStoreRecords: ObjectStoreRecord[] = [];
|
|
|
|
objectStore.originalData.forEach((v: ObjectStoreRecord) => {
|
|
|
|
objectStoreRecords.push(structuredClone(v));
|
|
|
|
});
|
|
|
|
objectStores[objectStoreName] = {
|
|
|
|
name: objectStoreName,
|
|
|
|
records: objectStoreRecords,
|
|
|
|
keyGenerator: objectStore.originalKeyGenerator,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
const dbDump: DatabaseDump = {
|
|
|
|
objectStores,
|
|
|
|
schema: structuredClone(this.databases[dbName].committedSchema),
|
|
|
|
};
|
|
|
|
dbDumps[dbName] = dbDump;
|
|
|
|
}
|
2021-02-24 17:33:07 +01:00
|
|
|
return structuredEncapsulate({ databases: dbDumps });
|
2019-08-16 19:05:48 +02:00
|
|
|
}
|
2019-06-21 19:18:36 +02:00
|
|
|
|
2019-06-15 22:44:54 +02:00
|
|
|
async getDatabases(): Promise<{ name: string; version: number }[]> {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log("TRACING: getDatabase");
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
const dbList = [];
|
|
|
|
for (const name in this.databases) {
|
|
|
|
dbList.push({
|
|
|
|
name,
|
|
|
|
version: this.databases[name].committedSchema.databaseVersion,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return dbList;
|
|
|
|
}
|
|
|
|
|
2021-02-19 21:27:49 +01:00
|
|
|
async deleteDatabase(name: string): Promise<void> {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
2021-02-19 21:27:49 +01:00
|
|
|
console.log(`TRACING: deleteDatabase(${name})`);
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
const myDb = this.databases[name];
|
|
|
|
if (!myDb) {
|
|
|
|
throw Error("db not found");
|
|
|
|
}
|
|
|
|
if (myDb.committedSchema.databaseName !== name) {
|
|
|
|
throw Error("name does not match");
|
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
|
|
|
|
while (myDb.txLevel !== TransactionLevel.None) {
|
|
|
|
await this.transactionDoneCond.wait();
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
|
2019-06-15 22:44:54 +02:00
|
|
|
myDb.deleted = true;
|
2021-02-19 21:27:49 +01:00
|
|
|
delete this.databases[name];
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async connectDatabase(name: string): Promise<DatabaseConnection> {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(`TRACING: connectDatabase(${name})`);
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
const connectionId = this.connectionIdCounter++;
|
|
|
|
const connectionCookie = `connection-${connectionId}`;
|
|
|
|
|
|
|
|
let database = this.databases[name];
|
|
|
|
if (!database) {
|
|
|
|
const schema: Schema = {
|
|
|
|
databaseName: name,
|
|
|
|
databaseVersion: 0,
|
|
|
|
objectStores: {},
|
|
|
|
};
|
|
|
|
database = {
|
|
|
|
committedSchema: schema,
|
|
|
|
deleted: false,
|
|
|
|
committedObjectStores: {},
|
2021-02-17 17:38:47 +01:00
|
|
|
txLevel: TransactionLevel.None,
|
|
|
|
connectionCookies: [],
|
2019-11-29 19:25:48 +01:00
|
|
|
txRestrictObjectStores: undefined,
|
2019-06-15 22:44:54 +02:00
|
|
|
};
|
|
|
|
this.databases[name] = database;
|
|
|
|
}
|
|
|
|
|
2021-02-17 17:38:47 +01:00
|
|
|
if (database.connectionCookies.includes(connectionCookie)) {
|
|
|
|
throw Error("already connected");
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
2021-02-17 17:38:47 +01:00
|
|
|
database.connectionCookies.push(connectionCookie);
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
const myConn: Connection = {
|
|
|
|
dbName: name,
|
2019-08-17 01:03:55 +02:00
|
|
|
objectStoreMap: this.makeObjectStoreMap(database),
|
2019-06-21 19:18:36 +02:00
|
|
|
modifiedSchema: structuredClone(database.committedSchema),
|
|
|
|
};
|
|
|
|
|
|
|
|
this.connections[connectionCookie] = myConn;
|
|
|
|
|
2019-06-15 22:44:54 +02:00
|
|
|
return { connectionCookie };
|
|
|
|
}
|
|
|
|
|
|
|
|
async beginTransaction(
|
|
|
|
conn: DatabaseConnection,
|
|
|
|
objectStores: string[],
|
2021-02-08 15:23:44 +01:00
|
|
|
mode: IDBTransactionMode,
|
2019-06-15 22:44:54 +02:00
|
|
|
): Promise<DatabaseTransaction> {
|
2021-02-16 13:46:51 +01:00
|
|
|
const transactionCookie = `tx-${this.transactionIdCounter++}`;
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
2021-02-16 13:46:51 +01:00
|
|
|
console.log(`TRACING: beginTransaction ${transactionCookie}`);
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2021-02-17 17:38:47 +01:00
|
|
|
while (myDb.txLevel !== TransactionLevel.None) {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(`TRACING: beginTransaction -- waiting for others to close`);
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2022-01-11 21:00:12 +01:00
|
|
|
if (this.trackStats) {
|
|
|
|
if (mode === "readonly") {
|
|
|
|
this.accessStats.readTransactions++;
|
|
|
|
} else if (mode === "readwrite") {
|
|
|
|
this.accessStats.writeTransactions++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-29 19:25:48 +01:00
|
|
|
myDb.txRestrictObjectStores = [...objectStores];
|
|
|
|
|
2019-06-15 22:44:54 +02:00
|
|
|
this.connectionsByTransaction[transactionCookie] = myConn;
|
|
|
|
|
|
|
|
return { transactionCookie };
|
|
|
|
}
|
|
|
|
|
|
|
|
async enterVersionChange(
|
|
|
|
conn: DatabaseConnection,
|
|
|
|
newVersion: number,
|
|
|
|
): Promise<DatabaseTransaction> {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(`TRACING: enterVersionChange`);
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2021-02-17 17:38:47 +01:00
|
|
|
while (myDb.txLevel !== TransactionLevel.None) {
|
2019-06-15 22:44:54 +02:00
|
|
|
await this.transactionDoneCond.wait();
|
|
|
|
}
|
|
|
|
|
|
|
|
myDb.txLevel = TransactionLevel.VersionChange;
|
2021-02-17 17:38:47 +01:00
|
|
|
myDb.txOwnerConnectionCookie = conn.connectionCookie;
|
|
|
|
myDb.txOwnerTransactionCookie = transactionCookie;
|
2019-11-29 19:25:48 +01:00
|
|
|
myDb.txRestrictObjectStores = undefined;
|
2019-06-15 22:44:54 +02:00
|
|
|
|
|
|
|
this.connectionsByTransaction[transactionCookie] = myConn;
|
|
|
|
|
2019-08-16 23:06:51 +02:00
|
|
|
myConn.modifiedSchema.databaseVersion = newVersion;
|
|
|
|
|
2019-06-15 22:44:54 +02:00
|
|
|
return { transactionCookie };
|
|
|
|
}
|
|
|
|
|
|
|
|
async close(conn: DatabaseConnection): Promise<void> {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
2021-02-16 13:46:51 +01:00
|
|
|
console.log(`TRACING: close (${conn.connectionCookie})`);
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
const myConn = this.connections[conn.connectionCookie];
|
|
|
|
if (!myConn) {
|
|
|
|
throw Error("connection not found - already closed?");
|
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
const myDb = this.databases[myConn.dbName];
|
2021-12-15 02:58:36 +01:00
|
|
|
if (myDb) {
|
|
|
|
// FIXME: what if we're still in a transaction?
|
|
|
|
myDb.connectionCookies = myDb.connectionCookies.filter(
|
|
|
|
(x) => x != conn.connectionCookie,
|
|
|
|
);
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
delete this.connections[conn.connectionCookie];
|
2019-06-21 19:18:36 +02:00
|
|
|
this.disconnectCond.trigger();
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
2021-02-19 21:27:49 +01:00
|
|
|
private requireConnection(dbConn: DatabaseConnection): Connection {
|
|
|
|
const myConn = this.connections[dbConn.connectionCookie];
|
|
|
|
if (!myConn) {
|
|
|
|
throw Error(`unknown connection (${dbConn.connectionCookie})`);
|
|
|
|
}
|
|
|
|
return myConn;
|
|
|
|
}
|
|
|
|
|
|
|
|
private requireConnectionFromTransaction(
|
|
|
|
btx: DatabaseTransaction,
|
|
|
|
): Connection {
|
|
|
|
const myConn = this.connectionsByTransaction[btx.transactionCookie];
|
|
|
|
if (!myConn) {
|
|
|
|
throw Error(`unknown transaction (${btx.transactionCookie})`);
|
|
|
|
}
|
|
|
|
return myConn;
|
|
|
|
}
|
|
|
|
|
2019-06-15 22:44:54 +02:00
|
|
|
getSchema(dbConn: DatabaseConnection): Schema {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(`TRACING: getSchema`);
|
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnection(dbConn);
|
2019-06-15 22:44:54 +02:00
|
|
|
const db = this.databases[myConn.dbName];
|
|
|
|
if (!db) {
|
|
|
|
throw Error("db not found");
|
|
|
|
}
|
2021-02-16 14:49:38 +01:00
|
|
|
return db.committedSchema;
|
|
|
|
}
|
|
|
|
|
|
|
|
getCurrentTransactionSchema(btx: DatabaseTransaction): Schema {
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
2021-02-16 14:49:38 +01:00
|
|
|
const db = this.databases[myConn.dbName];
|
|
|
|
if (!db) {
|
|
|
|
throw Error("db not found");
|
|
|
|
}
|
2019-08-16 23:06:51 +02:00
|
|
|
return myConn.modifiedSchema;
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
2021-02-16 14:49:38 +01:00
|
|
|
getInitialTransactionSchema(btx: DatabaseTransaction): Schema {
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
2021-02-16 14:49:38 +01:00
|
|
|
const db = this.databases[myConn.dbName];
|
|
|
|
if (!db) {
|
|
|
|
throw Error("db not found");
|
|
|
|
}
|
|
|
|
return db.committedSchema;
|
|
|
|
}
|
|
|
|
|
2019-06-15 22:44:54 +02:00
|
|
|
renameIndex(
|
|
|
|
btx: DatabaseTransaction,
|
2019-08-17 01:03:55 +02:00
|
|
|
objectStoreName: string,
|
2019-06-15 22:44:54 +02:00
|
|
|
oldName: string,
|
|
|
|
newName: string,
|
|
|
|
): void {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(`TRACING: renameIndex(?, ${oldName}, ${newName})`);
|
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
2019-06-15 22:44:54 +02:00
|
|
|
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();
|
|
|
|
}
|
2019-08-17 01:03:55 +02:00
|
|
|
const indexesSchema = schema.objectStores[objectStoreName].indexes;
|
|
|
|
if (indexesSchema[newName]) {
|
2019-06-15 22:44:54 +02:00
|
|
|
throw new Error("new index name already used");
|
|
|
|
}
|
2019-08-17 01:03:55 +02:00
|
|
|
if (!indexesSchema) {
|
2019-06-15 22:44:54 +02:00
|
|
|
throw new Error("new index name already used");
|
|
|
|
}
|
2019-08-26 02:41:50 +02:00
|
|
|
const index: Index =
|
|
|
|
myConn.objectStoreMap[objectStoreName].indexMap[oldName];
|
2019-06-15 22:44:54 +02:00
|
|
|
if (!index) {
|
|
|
|
throw Error("old index missing in connection's index map");
|
|
|
|
}
|
2019-08-17 01:03:55 +02:00
|
|
|
indexesSchema[newName] = indexesSchema[newName];
|
|
|
|
delete indexesSchema[oldName];
|
|
|
|
myConn.objectStoreMap[objectStoreName].indexMap[newName] = index;
|
|
|
|
delete myConn.objectStoreMap[objectStoreName].indexMap[oldName];
|
2019-06-15 22:44:54 +02:00
|
|
|
index.modifiedName = newName;
|
|
|
|
}
|
|
|
|
|
2019-08-26 02:41:50 +02:00
|
|
|
deleteIndex(
|
|
|
|
btx: DatabaseTransaction,
|
|
|
|
objectStoreName: string,
|
|
|
|
indexName: string,
|
|
|
|
): void {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(`TRACING: deleteIndex(${indexName})`);
|
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
2019-06-15 22:44:54 +02:00
|
|
|
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();
|
|
|
|
}
|
2019-08-17 01:03:55 +02:00
|
|
|
if (!schema.objectStores[objectStoreName].indexes[indexName]) {
|
2019-06-15 22:44:54 +02:00
|
|
|
throw new Error("index does not exist");
|
|
|
|
}
|
2019-08-26 02:41:50 +02:00
|
|
|
const index: Index =
|
|
|
|
myConn.objectStoreMap[objectStoreName].indexMap[indexName];
|
2019-06-15 22:44:54 +02:00
|
|
|
if (!index) {
|
|
|
|
throw Error("old index missing in connection's index map");
|
|
|
|
}
|
|
|
|
index.deleted = true;
|
2019-08-17 01:03:55 +02:00
|
|
|
delete schema.objectStores[objectStoreName].indexes[indexName];
|
|
|
|
delete myConn.objectStoreMap[objectStoreName].indexMap[indexName];
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
deleteObjectStore(btx: DatabaseTransaction, name: string): void {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
2021-02-16 13:46:51 +01:00
|
|
|
console.log(
|
|
|
|
`TRACING: deleteObjectStore(${name}) in ${btx.transactionCookie}`,
|
|
|
|
);
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
2019-06-15 22:44:54 +02:00
|
|
|
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");
|
|
|
|
}
|
2019-08-17 01:03:55 +02:00
|
|
|
const objectStoreMapEntry = myConn.objectStoreMap[name];
|
|
|
|
if (!objectStoreMapEntry) {
|
2019-06-15 22:44:54 +02:00
|
|
|
throw Error("object store not found in map");
|
|
|
|
}
|
2019-08-17 01:03:55 +02:00
|
|
|
const indexNames = Object.keys(objectStoreProperties.indexes);
|
2019-06-15 22:44:54 +02:00
|
|
|
for (const indexName of indexNames) {
|
2019-08-17 01:03:55 +02:00
|
|
|
this.deleteIndex(btx, name, indexName);
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
2019-08-17 01:03:55 +02:00
|
|
|
objectStoreMapEntry.store.deleted = true;
|
2019-06-15 22:44:54 +02:00
|
|
|
delete myConn.objectStoreMap[name];
|
|
|
|
delete schema.objectStores[name];
|
|
|
|
}
|
|
|
|
|
|
|
|
renameObjectStore(
|
|
|
|
btx: DatabaseTransaction,
|
|
|
|
oldName: string,
|
|
|
|
newName: string,
|
|
|
|
): void {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(`TRACING: renameObjectStore(?, ${oldName}, ${newName})`);
|
|
|
|
}
|
|
|
|
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
2019-06-15 22:44:54 +02:00
|
|
|
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");
|
|
|
|
}
|
2019-08-17 01:03:55 +02:00
|
|
|
const objectStoreMapEntry = myConn.objectStoreMap[oldName];
|
|
|
|
if (!objectStoreMapEntry) {
|
2019-06-15 22:44:54 +02:00
|
|
|
throw Error("object store not found in map");
|
|
|
|
}
|
2019-08-17 01:03:55 +02:00
|
|
|
objectStoreMapEntry.store.modifiedName = newName;
|
2019-06-15 22:44:54 +02:00
|
|
|
schema.objectStores[newName] = schema.objectStores[oldName];
|
|
|
|
delete schema.objectStores[oldName];
|
|
|
|
delete myConn.objectStoreMap[oldName];
|
2019-08-17 01:03:55 +02:00
|
|
|
myConn.objectStoreMap[newName] = objectStoreMapEntry;
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
createObjectStore(
|
|
|
|
btx: DatabaseTransaction,
|
|
|
|
name: string,
|
2021-02-08 19:59:19 +01:00
|
|
|
keyPath: string[] | null,
|
2019-06-15 22:44:54 +02:00
|
|
|
autoIncrement: boolean,
|
|
|
|
): void {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(
|
|
|
|
`TRACING: createObjectStore(${btx.transactionCookie}, ${name})`,
|
|
|
|
);
|
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
2019-06-15 22:44:54 +02:00
|
|
|
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,
|
2019-08-17 01:03:55 +02:00
|
|
|
committedIndexes: {},
|
|
|
|
modifiedIndexes: {},
|
2019-06-15 22:44:54 +02:00
|
|
|
};
|
|
|
|
const schema = myConn.modifiedSchema;
|
|
|
|
if (!schema) {
|
|
|
|
throw Error("no schema for versionchange tx");
|
|
|
|
}
|
|
|
|
schema.objectStores[name] = {
|
|
|
|
autoIncrement,
|
|
|
|
keyPath,
|
2019-08-17 01:03:55 +02:00
|
|
|
indexes: {},
|
2019-06-15 22:44:54 +02:00
|
|
|
};
|
2019-08-17 01:03:55 +02:00
|
|
|
myConn.objectStoreMap[name] = { store: newObjectStore, indexMap: {} };
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
createIndex(
|
|
|
|
btx: DatabaseTransaction,
|
|
|
|
indexName: string,
|
|
|
|
objectStoreName: string,
|
2021-02-08 19:59:19 +01:00
|
|
|
keyPath: string[],
|
2019-06-15 22:44:54 +02:00
|
|
|
multiEntry: boolean,
|
|
|
|
unique: boolean,
|
|
|
|
): void {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(`TRACING: createIndex(${indexName})`);
|
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
2019-06-15 22:44:54 +02:00
|
|
|
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,
|
|
|
|
};
|
2019-08-17 01:03:55 +02:00
|
|
|
myConn.objectStoreMap[objectStoreName].indexMap[indexName] = newIndex;
|
2019-06-15 22:44:54 +02:00
|
|
|
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");
|
|
|
|
}
|
2019-08-17 01:03:55 +02:00
|
|
|
objectStoreProperties.indexes[indexName] = indexProperties;
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-08-17 01:03:55 +02:00
|
|
|
const objectStoreMapEntry = myConn.objectStoreMap[objectStoreName];
|
|
|
|
if (!objectStoreMapEntry) {
|
2019-06-25 13:44:03 +02:00
|
|
|
throw Error("object store does not exist");
|
|
|
|
}
|
|
|
|
|
2019-08-26 02:41:50 +02:00
|
|
|
const storeData =
|
|
|
|
objectStoreMapEntry.store.modifiedData ||
|
|
|
|
objectStoreMapEntry.store.originalData;
|
2019-06-25 13:44:03 +02:00
|
|
|
|
|
|
|
storeData.forEach((v, k) => {
|
2021-02-16 13:46:51 +01:00
|
|
|
try {
|
|
|
|
this.insertIntoIndex(newIndex, k, v.value, indexProperties);
|
|
|
|
} catch (e) {
|
|
|
|
if (e instanceof DataError) {
|
|
|
|
// We don't propagate this error here.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw e;
|
|
|
|
}
|
2019-06-25 13:44:03 +02:00
|
|
|
});
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
2021-02-23 20:16:10 +01:00
|
|
|
async clearObjectStore(
|
|
|
|
btx: DatabaseTransaction,
|
|
|
|
objectStoreName: string,
|
|
|
|
): Promise<void> {
|
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
|
|
|
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");
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
db.txRestrictObjectStores &&
|
|
|
|
!db.txRestrictObjectStores.includes(objectStoreName)
|
|
|
|
) {
|
|
|
|
throw Error(
|
|
|
|
`Not allowed to access store '${objectStoreName}', transaction is over ${JSON.stringify(
|
|
|
|
db.txRestrictObjectStores,
|
|
|
|
)}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const schema = myConn.modifiedSchema;
|
|
|
|
const objectStoreMapEntry = myConn.objectStoreMap[objectStoreName];
|
|
|
|
|
|
|
|
objectStoreMapEntry.store.modifiedData = new BTree([], compareKeys);
|
|
|
|
|
|
|
|
for (const indexName of Object.keys(
|
|
|
|
schema.objectStores[objectStoreName].indexes,
|
|
|
|
)) {
|
|
|
|
const index = myConn.objectStoreMap[objectStoreName].indexMap[indexName];
|
|
|
|
if (!index) {
|
|
|
|
throw Error("index referenced by object store does not exist");
|
|
|
|
}
|
|
|
|
index.modifiedData = new BTree([], compareKeys);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-15 22:44:54 +02:00
|
|
|
async deleteRecord(
|
|
|
|
btx: DatabaseTransaction,
|
|
|
|
objectStoreName: string,
|
2021-02-08 15:23:44 +01:00
|
|
|
range: IDBKeyRange,
|
2019-06-15 22:44:54 +02:00
|
|
|
): Promise<void> {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
2019-07-31 01:33:23 +02:00
|
|
|
console.log(`TRACING: deleteRecord from store ${objectStoreName}`);
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
2019-06-15 22:44:54 +02:00
|
|
|
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");
|
|
|
|
}
|
2019-11-29 19:25:48 +01:00
|
|
|
if (
|
|
|
|
db.txRestrictObjectStores &&
|
|
|
|
!db.txRestrictObjectStores.includes(objectStoreName)
|
|
|
|
) {
|
|
|
|
throw Error(
|
2020-08-03 09:30:48 +02:00
|
|
|
`Not allowed to access store '${objectStoreName}', transaction is over ${JSON.stringify(
|
|
|
|
db.txRestrictObjectStores,
|
|
|
|
)}`,
|
2019-11-29 19:25:48 +01:00
|
|
|
);
|
|
|
|
}
|
2019-07-31 01:33:23 +02:00
|
|
|
if (typeof range !== "object") {
|
|
|
|
throw Error("deleteRecord got invalid range (must be object)");
|
|
|
|
}
|
|
|
|
if (!("lowerOpen" in range)) {
|
2019-08-16 19:05:48 +02:00
|
|
|
throw Error(
|
|
|
|
"deleteRecord got invalid range (sanity check failed, 'lowerOpen' missing)",
|
|
|
|
);
|
2019-07-31 01:33:23 +02:00
|
|
|
}
|
|
|
|
|
2019-08-16 23:06:51 +02:00
|
|
|
const schema = myConn.modifiedSchema;
|
2019-08-17 01:03:55 +02:00
|
|
|
const objectStoreMapEntry = myConn.objectStoreMap[objectStoreName];
|
2019-07-31 01:33:23 +02:00
|
|
|
|
2019-08-17 01:03:55 +02:00
|
|
|
if (!objectStoreMapEntry.store.modifiedData) {
|
2019-08-26 02:41:50 +02:00
|
|
|
objectStoreMapEntry.store.modifiedData =
|
|
|
|
objectStoreMapEntry.store.originalData;
|
2019-07-31 01:33:23 +02:00
|
|
|
}
|
|
|
|
|
2019-08-17 01:03:55 +02:00
|
|
|
let modifiedData = objectStoreMapEntry.store.modifiedData;
|
2019-07-31 01:33:23 +02:00
|
|
|
let currKey: Key | undefined;
|
|
|
|
|
|
|
|
if (range.lower === undefined || range.lower === null) {
|
|
|
|
currKey = modifiedData.minKey();
|
|
|
|
} else {
|
|
|
|
currKey = range.lower;
|
|
|
|
// We have a range with an lowerOpen lower bound, so don't start
|
2019-11-21 20:39:53 +01:00
|
|
|
// deleting the lower bound. Instead start with the next higher key.
|
2019-07-31 01:33:23 +02:00
|
|
|
if (range.lowerOpen && currKey !== undefined) {
|
2019-08-16 19:05:48 +02:00
|
|
|
currKey = modifiedData.nextHigherKey(currKey);
|
2019-07-31 01:33:23 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-08 15:23:44 +01:00
|
|
|
if (currKey === undefined) {
|
|
|
|
throw Error("invariant violated");
|
|
|
|
}
|
|
|
|
|
2019-11-21 20:39:53 +01:00
|
|
|
// make sure that currKey is either undefined or pointing to an
|
|
|
|
// existing object.
|
|
|
|
let firstValue = modifiedData.get(currKey);
|
|
|
|
if (!firstValue) {
|
|
|
|
if (currKey !== undefined) {
|
|
|
|
currKey = modifiedData.nextHigherKey(currKey);
|
|
|
|
}
|
|
|
|
}
|
2019-07-31 01:33:23 +02:00
|
|
|
|
2019-11-21 20:39:53 +01:00
|
|
|
// loop invariant: (currKey is undefined) or (currKey is a valid key)
|
2019-07-31 01:33:23 +02:00
|
|
|
while (true) {
|
|
|
|
if (currKey === undefined) {
|
|
|
|
// nothing more to delete!
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (range.upper !== null && range.upper !== undefined) {
|
|
|
|
if (range.upperOpen && compareKeys(currKey, range.upper) === 0) {
|
|
|
|
// We have a range that's upperOpen, so stop before we delete the upper bound.
|
|
|
|
break;
|
|
|
|
}
|
2019-08-16 19:05:48 +02:00
|
|
|
if (!range.upperOpen && compareKeys(currKey, range.upper) > 0) {
|
2019-07-31 01:33:23 +02:00
|
|
|
// The upper range is inclusive, only stop if we're after the upper range.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const storeEntry = modifiedData.get(currKey);
|
|
|
|
if (!storeEntry) {
|
|
|
|
throw Error("assertion failed");
|
|
|
|
}
|
|
|
|
|
2019-08-26 02:41:50 +02:00
|
|
|
for (const indexName of Object.keys(
|
|
|
|
schema.objectStores[objectStoreName].indexes,
|
|
|
|
)) {
|
|
|
|
const index =
|
|
|
|
myConn.objectStoreMap[objectStoreName].indexMap[indexName];
|
2019-07-31 01:33:23 +02:00
|
|
|
if (!index) {
|
|
|
|
throw Error("index referenced by object store does not exist");
|
|
|
|
}
|
2019-08-26 02:41:50 +02:00
|
|
|
this.enableTracing &&
|
|
|
|
console.log(
|
|
|
|
`deleting from index ${indexName} for object store ${objectStoreName}`,
|
|
|
|
);
|
|
|
|
const indexProperties =
|
|
|
|
schema.objectStores[objectStoreName].indexes[indexName];
|
2019-08-16 19:05:48 +02:00
|
|
|
this.deleteFromIndex(
|
|
|
|
index,
|
|
|
|
storeEntry.primaryKey,
|
|
|
|
storeEntry.value,
|
|
|
|
indexProperties,
|
|
|
|
);
|
2019-07-31 01:33:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
modifiedData = modifiedData.without(currKey);
|
|
|
|
|
|
|
|
currKey = modifiedData.nextHigherKey(currKey);
|
|
|
|
}
|
|
|
|
|
2019-08-17 01:03:55 +02:00
|
|
|
objectStoreMapEntry.store.modifiedData = modifiedData;
|
2019-07-31 01:33:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private deleteFromIndex(
|
|
|
|
index: Index,
|
|
|
|
primaryKey: Key,
|
|
|
|
value: Value,
|
|
|
|
indexProperties: IndexProperties,
|
|
|
|
): void {
|
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(
|
|
|
|
`deleteFromIndex(${index.modifiedName || index.originalName})`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (value === undefined || value === null) {
|
|
|
|
throw Error("cannot delete null/undefined value from index");
|
|
|
|
}
|
|
|
|
let indexData = index.modifiedData || index.originalData;
|
|
|
|
const indexKeys = getIndexKeys(
|
|
|
|
value,
|
|
|
|
indexProperties.keyPath,
|
|
|
|
indexProperties.multiEntry,
|
|
|
|
);
|
|
|
|
for (const indexKey of indexKeys) {
|
2021-12-15 02:37:03 +01:00
|
|
|
const existingIndexRecord = indexData.get(indexKey);
|
|
|
|
if (!existingIndexRecord) {
|
2019-07-31 01:33:23 +02:00
|
|
|
throw Error("db inconsistent: expected index entry missing");
|
|
|
|
}
|
2021-12-15 02:37:03 +01:00
|
|
|
const newPrimaryKeys = existingIndexRecord.primaryKeys.without(
|
|
|
|
primaryKey,
|
2019-08-16 19:05:48 +02:00
|
|
|
);
|
2021-12-15 02:37:03 +01:00
|
|
|
if (newPrimaryKeys.size === 0) {
|
2019-08-17 01:50:51 +02:00
|
|
|
index.modifiedData = indexData.without(indexKey);
|
2019-07-31 01:33:23 +02:00
|
|
|
} else {
|
2021-12-15 02:37:03 +01:00
|
|
|
const newIndexRecord: IndexRecord = {
|
2019-07-31 01:33:23 +02:00
|
|
|
indexKey,
|
|
|
|
primaryKeys: newPrimaryKeys,
|
2019-08-16 19:05:48 +02:00
|
|
|
};
|
2019-07-31 01:33:23 +02:00
|
|
|
index.modifiedData = indexData.with(indexKey, newIndexRecord, true);
|
|
|
|
}
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async getRecords(
|
|
|
|
btx: DatabaseTransaction,
|
2019-06-21 19:18:36 +02:00
|
|
|
req: RecordGetRequest,
|
|
|
|
): Promise<RecordGetResponse> {
|
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(`TRACING: getRecords`);
|
2019-06-23 22:16:03 +02:00
|
|
|
console.log("query", req);
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
2019-06-15 22:44:54 +02:00
|
|
|
const db = this.databases[myConn.dbName];
|
|
|
|
if (!db) {
|
|
|
|
throw Error("db not found");
|
|
|
|
}
|
2019-06-21 19:18:36 +02:00
|
|
|
if (db.txLevel < TransactionLevel.Read) {
|
2019-06-15 22:44:54 +02:00
|
|
|
throw Error("only allowed while running a transaction");
|
|
|
|
}
|
2019-11-29 19:25:48 +01:00
|
|
|
if (
|
|
|
|
db.txRestrictObjectStores &&
|
|
|
|
!db.txRestrictObjectStores.includes(req.objectStoreName)
|
|
|
|
) {
|
|
|
|
throw Error(
|
|
|
|
`Not allowed to access store '${
|
|
|
|
req.objectStoreName
|
|
|
|
}', transaction is over ${JSON.stringify(db.txRestrictObjectStores)}`,
|
|
|
|
);
|
|
|
|
}
|
2019-08-17 01:03:55 +02:00
|
|
|
const objectStoreMapEntry = myConn.objectStoreMap[req.objectStoreName];
|
|
|
|
if (!objectStoreMapEntry) {
|
2019-06-21 19:18:36 +02:00
|
|
|
throw Error("object store not found");
|
|
|
|
}
|
|
|
|
|
|
|
|
let range;
|
|
|
|
if (req.range == null || req.range === undefined) {
|
2019-06-23 22:16:03 +02:00
|
|
|
range = new BridgeIDBKeyRange(undefined, undefined, true, true);
|
2019-06-21 19:18:36 +02:00
|
|
|
} else {
|
|
|
|
range = req.range;
|
|
|
|
}
|
|
|
|
|
2019-07-31 01:33:23 +02:00
|
|
|
if (typeof range !== "object") {
|
|
|
|
throw Error(
|
|
|
|
"getRecords was given an invalid range (sanity check failed, not an object)",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!("lowerOpen" in range)) {
|
|
|
|
throw Error(
|
|
|
|
"getRecords was given an invalid range (sanity check failed, lowerOpen missing)",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
const forward: boolean =
|
|
|
|
req.direction === "next" || req.direction === "nextunique";
|
|
|
|
const unique: boolean =
|
|
|
|
req.direction === "prevunique" || req.direction === "nextunique";
|
|
|
|
|
2019-08-26 02:41:50 +02:00
|
|
|
const storeData =
|
|
|
|
objectStoreMapEntry.store.modifiedData ||
|
|
|
|
objectStoreMapEntry.store.originalData;
|
2019-06-21 19:18:36 +02:00
|
|
|
|
|
|
|
const haveIndex = req.indexName !== undefined;
|
|
|
|
|
2021-12-15 02:37:03 +01:00
|
|
|
let resp: RecordGetResponse;
|
|
|
|
|
2019-06-21 19:18:36 +02:00
|
|
|
if (haveIndex) {
|
2019-08-26 02:41:50 +02:00
|
|
|
const index =
|
|
|
|
myConn.objectStoreMap[req.objectStoreName].indexMap[req.indexName!];
|
2019-06-21 19:18:36 +02:00
|
|
|
const indexData = index.modifiedData || index.originalData;
|
2021-12-15 02:37:03 +01:00
|
|
|
resp = getIndexRecords({
|
|
|
|
forward,
|
|
|
|
indexData,
|
|
|
|
storeData,
|
|
|
|
limit: req.limit,
|
|
|
|
unique,
|
|
|
|
range,
|
|
|
|
resultLevel: req.resultLevel,
|
|
|
|
advanceIndexKey: req.advanceIndexKey,
|
|
|
|
advancePrimaryKey: req.advancePrimaryKey,
|
|
|
|
lastIndexPosition: req.lastIndexPosition,
|
|
|
|
lastObjectStorePosition: req.lastObjectStorePosition,
|
|
|
|
});
|
2022-01-11 21:00:12 +01:00
|
|
|
if (this.trackStats) {
|
|
|
|
const k = `${req.objectStoreName}.${req.indexName}`;
|
|
|
|
this.accessStats.readsPerIndex[k] =
|
|
|
|
(this.accessStats.readsPerIndex[k] ?? 0) + 1;
|
|
|
|
this.accessStats.readItemsPerIndex[k] =
|
|
|
|
(this.accessStats.readItemsPerIndex[k] ?? 0) + resp.count;
|
|
|
|
}
|
2019-06-21 19:18:36 +02:00
|
|
|
} else {
|
|
|
|
if (req.advanceIndexKey !== undefined) {
|
|
|
|
throw Error("unsupported request");
|
|
|
|
}
|
2021-12-15 02:37:03 +01:00
|
|
|
resp = getObjectStoreRecords({
|
|
|
|
forward,
|
|
|
|
storeData,
|
|
|
|
limit: req.limit,
|
|
|
|
range,
|
|
|
|
resultLevel: req.resultLevel,
|
|
|
|
advancePrimaryKey: req.advancePrimaryKey,
|
|
|
|
lastIndexPosition: req.lastIndexPosition,
|
|
|
|
lastObjectStorePosition: req.lastObjectStorePosition,
|
|
|
|
});
|
2022-01-11 21:00:12 +01:00
|
|
|
if (this.trackStats) {
|
|
|
|
const k = `${req.objectStoreName}`;
|
|
|
|
this.accessStats.readsPerStore[k] =
|
|
|
|
(this.accessStats.readsPerStore[k] ?? 0) + 1;
|
|
|
|
this.accessStats.readItemsPerStore[k] =
|
|
|
|
(this.accessStats.readItemsPerStore[k] ?? 0) + resp.count;
|
|
|
|
}
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
|
|
|
if (this.enableTracing) {
|
2021-12-15 02:37:03 +01:00
|
|
|
console.log(`TRACING: getRecords got ${resp.count} results`);
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
2021-12-15 02:37:03 +01:00
|
|
|
return resp;
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async storeRecord(
|
|
|
|
btx: DatabaseTransaction,
|
|
|
|
storeReq: RecordStoreRequest,
|
2019-08-01 23:21:05 +02:00
|
|
|
): Promise<RecordStoreResponse> {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(`TRACING: storeRecord`);
|
2022-01-11 21:00:12 +01:00
|
|
|
console.log(
|
|
|
|
`key ${storeReq.key}, record ${JSON.stringify(
|
|
|
|
structuredEncapsulate(storeReq.value),
|
|
|
|
)}`,
|
|
|
|
);
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
2019-06-15 22:44:54 +02:00
|
|
|
const db = this.databases[myConn.dbName];
|
|
|
|
if (!db) {
|
|
|
|
throw Error("db not found");
|
|
|
|
}
|
|
|
|
if (db.txLevel < TransactionLevel.Write) {
|
2021-02-17 17:38:47 +01:00
|
|
|
throw Error("store operation only allowed while running a transaction");
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
2019-11-29 19:25:48 +01:00
|
|
|
if (
|
|
|
|
db.txRestrictObjectStores &&
|
|
|
|
!db.txRestrictObjectStores.includes(storeReq.objectStoreName)
|
|
|
|
) {
|
|
|
|
throw Error(
|
|
|
|
`Not allowed to access store '${
|
|
|
|
storeReq.objectStoreName
|
|
|
|
}', transaction is over ${JSON.stringify(db.txRestrictObjectStores)}`,
|
|
|
|
);
|
|
|
|
}
|
2022-01-11 21:00:12 +01:00
|
|
|
|
|
|
|
if (this.trackStats) {
|
|
|
|
this.accessStats.writesPerStore[storeReq.objectStoreName] =
|
|
|
|
(this.accessStats.writesPerStore[storeReq.objectStoreName] ?? 0) + 1;
|
|
|
|
}
|
|
|
|
|
2019-08-16 23:06:51 +02:00
|
|
|
const schema = myConn.modifiedSchema;
|
2019-08-17 01:03:55 +02:00
|
|
|
const objectStoreMapEntry = myConn.objectStoreMap[storeReq.objectStoreName];
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-08-17 01:03:55 +02:00
|
|
|
if (!objectStoreMapEntry.store.modifiedData) {
|
2019-08-26 02:41:50 +02:00
|
|
|
objectStoreMapEntry.store.modifiedData =
|
|
|
|
objectStoreMapEntry.store.originalData;
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
2019-08-17 01:03:55 +02:00
|
|
|
const modifiedData = objectStoreMapEntry.store.modifiedData;
|
2019-07-31 01:33:23 +02:00
|
|
|
|
|
|
|
let key;
|
|
|
|
let value;
|
|
|
|
|
|
|
|
if (storeReq.storeLevel === StoreLevel.UpdateExisting) {
|
|
|
|
if (storeReq.key === null || storeReq.key === undefined) {
|
|
|
|
throw Error("invalid update request (key not given)");
|
|
|
|
}
|
|
|
|
|
2019-08-17 01:03:55 +02:00
|
|
|
if (!objectStoreMapEntry.store.modifiedData.has(storeReq.key)) {
|
2019-07-31 01:33:23 +02:00
|
|
|
throw Error("invalid update request (record does not exist)");
|
|
|
|
}
|
|
|
|
key = storeReq.key;
|
|
|
|
value = storeReq.value;
|
|
|
|
} else {
|
2019-11-21 11:15:42 +01:00
|
|
|
const keygen =
|
2019-08-26 02:41:50 +02:00
|
|
|
objectStoreMapEntry.store.modifiedKeyGenerator ||
|
2019-11-21 11:15:42 +01:00
|
|
|
objectStoreMapEntry.store.originalKeyGenerator;
|
|
|
|
const autoIncrement =
|
|
|
|
schema.objectStores[storeReq.objectStoreName].autoIncrement;
|
|
|
|
const keyPath = schema.objectStores[storeReq.objectStoreName].keyPath;
|
2021-02-16 13:46:51 +01:00
|
|
|
|
|
|
|
if (
|
|
|
|
keyPath !== null &&
|
|
|
|
keyPath !== undefined &&
|
|
|
|
storeReq.key !== undefined
|
|
|
|
) {
|
|
|
|
// If in-line keys are used, a key can't be explicitly specified.
|
|
|
|
throw new DataError();
|
|
|
|
}
|
|
|
|
|
2019-11-21 11:15:42 +01:00
|
|
|
let storeKeyResult: StoreKeyResult;
|
|
|
|
try {
|
|
|
|
storeKeyResult = makeStoreKeyValue(
|
2021-02-24 17:33:07 +01:00
|
|
|
storeReq.value,
|
2019-11-21 11:15:42 +01:00
|
|
|
storeReq.key,
|
|
|
|
keygen,
|
|
|
|
autoIncrement,
|
|
|
|
keyPath,
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
if (e instanceof DataError) {
|
|
|
|
const kp = JSON.stringify(keyPath);
|
|
|
|
const n = storeReq.objectStoreName;
|
2021-02-08 19:59:19 +01:00
|
|
|
const m = `Could not extract key from value, objectStore=${n}, keyPath=${kp}, value=${JSON.stringify(
|
|
|
|
storeReq.value,
|
|
|
|
)}`;
|
2019-11-21 11:15:42 +01:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.error(e);
|
|
|
|
console.error("value was:", storeReq.value);
|
|
|
|
console.error("key was:", storeReq.key);
|
|
|
|
}
|
|
|
|
throw new DataError(m);
|
2019-11-29 19:25:48 +01:00
|
|
|
} else {
|
2019-11-21 11:15:42 +01:00
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
2019-07-31 01:33:23 +02:00
|
|
|
key = storeKeyResult.key;
|
|
|
|
value = storeKeyResult.value;
|
2019-08-26 02:41:50 +02:00
|
|
|
objectStoreMapEntry.store.modifiedKeyGenerator =
|
|
|
|
storeKeyResult.updatedKeyGenerator;
|
2019-07-31 01:33:23 +02:00
|
|
|
const hasKey = modifiedData.has(key);
|
|
|
|
|
|
|
|
if (hasKey && storeReq.storeLevel !== StoreLevel.AllowOverwrite) {
|
2021-02-16 11:34:50 +01:00
|
|
|
throw new ConstraintError("refusing to overwrite");
|
2019-07-31 01:33:23 +02:00
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
2022-01-11 21:00:12 +01:00
|
|
|
const oldStoreRecord = modifiedData.get(key);
|
|
|
|
|
|
|
|
const newObjectStoreRecord: ObjectStoreRecord = {
|
2021-02-24 17:33:07 +01:00
|
|
|
// FIXME: We should serialize the key here, not just clone it.
|
2019-08-15 23:27:17 +02:00
|
|
|
primaryKey: structuredClone(key),
|
|
|
|
value: structuredClone(value),
|
2019-07-31 01:33:23 +02:00
|
|
|
};
|
|
|
|
|
2019-08-26 02:41:50 +02:00
|
|
|
objectStoreMapEntry.store.modifiedData = modifiedData.with(
|
|
|
|
key,
|
2022-01-11 21:00:12 +01:00
|
|
|
newObjectStoreRecord,
|
2019-08-26 02:41:50 +02:00
|
|
|
true,
|
|
|
|
);
|
2019-06-15 22:44:54 +02:00
|
|
|
|
2019-08-26 02:41:50 +02:00
|
|
|
for (const indexName of Object.keys(
|
|
|
|
schema.objectStores[storeReq.objectStoreName].indexes,
|
|
|
|
)) {
|
|
|
|
const index =
|
|
|
|
myConn.objectStoreMap[storeReq.objectStoreName].indexMap[indexName];
|
2019-06-15 22:44:54 +02:00
|
|
|
if (!index) {
|
|
|
|
throw Error("index referenced by object store does not exist");
|
|
|
|
}
|
2019-08-26 02:41:50 +02:00
|
|
|
const indexProperties =
|
|
|
|
schema.objectStores[storeReq.objectStoreName].indexes[indexName];
|
2022-01-11 21:00:12 +01:00
|
|
|
|
|
|
|
// Remove old index entry first!
|
|
|
|
if (oldStoreRecord) {
|
|
|
|
this.deleteFromIndex(index, key, oldStoreRecord.value, indexProperties);
|
|
|
|
}
|
2021-02-16 13:46:51 +01:00
|
|
|
try {
|
|
|
|
this.insertIntoIndex(index, key, value, indexProperties);
|
|
|
|
} catch (e) {
|
|
|
|
if (e instanceof DataError) {
|
|
|
|
// https://www.w3.org/TR/IndexedDB-2/#object-store-storage-operation
|
|
|
|
// Do nothing
|
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
2019-08-01 23:21:05 +02:00
|
|
|
|
|
|
|
return { key };
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
|
|
|
|
2019-06-23 22:16:03 +02:00
|
|
|
private insertIntoIndex(
|
2019-06-21 19:18:36 +02:00
|
|
|
index: Index,
|
|
|
|
primaryKey: Key,
|
|
|
|
value: Value,
|
|
|
|
indexProperties: IndexProperties,
|
|
|
|
): void {
|
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(
|
|
|
|
`insertIntoIndex(${index.modifiedName || index.originalName})`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
let indexData = index.modifiedData || index.originalData;
|
2019-11-21 10:43:40 +01:00
|
|
|
let indexKeys;
|
|
|
|
try {
|
|
|
|
indexKeys = getIndexKeys(
|
|
|
|
value,
|
|
|
|
indexProperties.keyPath,
|
|
|
|
indexProperties.multiEntry,
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
if (e instanceof DataError) {
|
|
|
|
const n = index.modifiedName || index.originalName;
|
|
|
|
const p = JSON.stringify(indexProperties.keyPath);
|
|
|
|
const m = `Failed to extract index keys from index ${n} for keyPath ${p}.`;
|
|
|
|
if (this.enableTracing) {
|
|
|
|
console.error(m);
|
|
|
|
console.error("value was", value);
|
|
|
|
}
|
|
|
|
throw new DataError(m);
|
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
2019-06-21 19:18:36 +02:00
|
|
|
for (const indexKey of indexKeys) {
|
|
|
|
const existingRecord = indexData.get(indexKey);
|
|
|
|
if (existingRecord) {
|
|
|
|
if (indexProperties.unique) {
|
|
|
|
throw new ConstraintError();
|
|
|
|
} else {
|
2021-12-15 02:37:03 +01:00
|
|
|
const newIndexRecord: IndexRecord = {
|
|
|
|
indexKey: indexKey,
|
|
|
|
primaryKeys: existingRecord.primaryKeys.with(primaryKey),
|
|
|
|
};
|
|
|
|
index.modifiedData = indexData.with(indexKey, newIndexRecord, true);
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
|
|
|
} else {
|
2021-12-15 02:37:03 +01:00
|
|
|
const primaryKeys: ISortedSetF<IDBValidKey> = new BTree(
|
|
|
|
[[primaryKey, undefined]],
|
|
|
|
compareKeys,
|
|
|
|
);
|
2019-06-21 19:18:36 +02:00
|
|
|
const newIndexRecord: IndexRecord = {
|
|
|
|
indexKey: indexKey,
|
2021-12-15 02:37:03 +01:00
|
|
|
primaryKeys,
|
2019-06-21 19:18:36 +02:00
|
|
|
};
|
|
|
|
index.modifiedData = indexData.with(indexKey, newIndexRecord, true);
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async rollback(btx: DatabaseTransaction): Promise<void> {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(`TRACING: rollback`);
|
|
|
|
}
|
|
|
|
const myConn = this.connectionsByTransaction[btx.transactionCookie];
|
2019-06-15 22:44:54 +02:00
|
|
|
if (!myConn) {
|
2021-02-16 11:34:50 +01:00
|
|
|
throw Error("unknown transaction");
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
const db = this.databases[myConn.dbName];
|
|
|
|
if (!db) {
|
|
|
|
throw Error("db not found");
|
|
|
|
}
|
|
|
|
if (db.txLevel < TransactionLevel.Read) {
|
2021-02-17 17:38:47 +01:00
|
|
|
throw Error("rollback is only allowed while running a transaction");
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
2021-02-17 17:38:47 +01:00
|
|
|
db.txLevel = TransactionLevel.None;
|
2019-11-29 19:25:48 +01:00
|
|
|
db.txRestrictObjectStores = undefined;
|
2019-06-15 22:44:54 +02:00
|
|
|
myConn.modifiedSchema = structuredClone(db.committedSchema);
|
2019-08-17 01:03:55 +02:00
|
|
|
myConn.objectStoreMap = this.makeObjectStoreMap(db);
|
2019-06-15 22:44:54 +02:00
|
|
|
for (const objectStoreName in db.committedObjectStores) {
|
|
|
|
const objectStore = db.committedObjectStores[objectStoreName];
|
|
|
|
objectStore.deleted = false;
|
|
|
|
objectStore.modifiedData = undefined;
|
|
|
|
objectStore.modifiedName = undefined;
|
|
|
|
objectStore.modifiedKeyGenerator = undefined;
|
2019-08-26 02:41:50 +02:00
|
|
|
objectStore.modifiedIndexes = {};
|
2019-08-17 01:03:55 +02:00
|
|
|
|
2019-11-19 19:27:26 +01:00
|
|
|
for (const indexName of Object.keys(
|
2019-08-26 02:41:50 +02:00
|
|
|
db.committedSchema.objectStores[objectStoreName].indexes,
|
|
|
|
)) {
|
2019-08-17 01:03:55 +02:00
|
|
|
const index = objectStore.committedIndexes[indexName];
|
|
|
|
index.deleted = false;
|
|
|
|
index.modifiedData = undefined;
|
|
|
|
index.modifiedName = undefined;
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
2019-06-21 19:18:36 +02:00
|
|
|
delete this.connectionsByTransaction[btx.transactionCookie];
|
|
|
|
this.transactionDoneCond.trigger();
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async commit(btx: DatabaseTransaction): Promise<void> {
|
2019-06-21 19:18:36 +02:00
|
|
|
if (this.enableTracing) {
|
|
|
|
console.log(`TRACING: commit`);
|
|
|
|
}
|
2021-02-19 21:27:49 +01:00
|
|
|
const myConn = this.requireConnectionFromTransaction(btx);
|
2019-06-15 22:44:54 +02:00
|
|
|
const db = this.databases[myConn.dbName];
|
|
|
|
if (!db) {
|
|
|
|
throw Error("db not found");
|
|
|
|
}
|
2019-08-16 22:35:44 +02:00
|
|
|
const txLevel = db.txLevel;
|
|
|
|
if (txLevel < TransactionLevel.Read) {
|
2019-06-15 22:44:54 +02:00
|
|
|
throw Error("only allowed while running a transaction");
|
|
|
|
}
|
2019-06-21 19:18:36 +02:00
|
|
|
|
2019-08-16 23:06:51 +02:00
|
|
|
db.committedSchema = structuredClone(myConn.modifiedSchema);
|
2021-02-17 17:38:47 +01:00
|
|
|
db.txLevel = TransactionLevel.None;
|
2019-11-29 19:25:48 +01:00
|
|
|
db.txRestrictObjectStores = undefined;
|
2019-06-21 19:18:36 +02:00
|
|
|
|
|
|
|
db.committedObjectStores = {};
|
|
|
|
db.committedObjectStores = {};
|
|
|
|
|
|
|
|
for (const objectStoreName in myConn.objectStoreMap) {
|
2019-08-17 01:03:55 +02:00
|
|
|
const objectStoreMapEntry = myConn.objectStoreMap[objectStoreName];
|
|
|
|
const store = objectStoreMapEntry.store;
|
|
|
|
store.deleted = false;
|
|
|
|
store.originalData = store.modifiedData || store.originalData;
|
|
|
|
store.originalName = store.modifiedName || store.originalName;
|
|
|
|
store.modifiedIndexes = {};
|
|
|
|
if (store.modifiedKeyGenerator !== undefined) {
|
|
|
|
store.originalKeyGenerator = store.modifiedKeyGenerator;
|
|
|
|
}
|
|
|
|
db.committedObjectStores[objectStoreName] = store;
|
|
|
|
|
|
|
|
for (const indexName in objectStoreMapEntry.indexMap) {
|
|
|
|
const index = objectStoreMapEntry.indexMap[indexName];
|
|
|
|
index.deleted = false;
|
|
|
|
index.originalData = index.modifiedData || index.originalData;
|
|
|
|
index.originalName = index.modifiedName || index.originalName;
|
|
|
|
store.committedIndexes[indexName] = index;
|
2019-06-21 19:18:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-17 01:03:55 +02:00
|
|
|
myConn.objectStoreMap = this.makeObjectStoreMap(db);
|
2019-06-21 19:18:36 +02:00
|
|
|
|
|
|
|
delete this.connectionsByTransaction[btx.transactionCookie];
|
|
|
|
this.transactionDoneCond.trigger();
|
2019-08-16 19:05:48 +02:00
|
|
|
|
2019-08-16 22:35:44 +02:00
|
|
|
if (this.afterCommitCallback && txLevel >= TransactionLevel.Write) {
|
2019-08-16 19:05:48 +02:00
|
|
|
await this.afterCommitCallback();
|
|
|
|
}
|
2019-06-15 22:44:54 +02:00
|
|
|
}
|
|
|
|
}
|
2021-12-15 02:37:03 +01:00
|
|
|
|
|
|
|
function getIndexRecords(req: {
|
|
|
|
indexData: ISortedMapF<IDBValidKey, IndexRecord>;
|
|
|
|
storeData: ISortedMapF<IDBValidKey, ObjectStoreRecord>;
|
|
|
|
lastIndexPosition?: IDBValidKey;
|
|
|
|
forward: boolean;
|
|
|
|
unique: boolean;
|
|
|
|
range: IDBKeyRange;
|
|
|
|
lastObjectStorePosition?: IDBValidKey;
|
|
|
|
advancePrimaryKey?: IDBValidKey;
|
|
|
|
advanceIndexKey?: IDBValidKey;
|
|
|
|
limit: number;
|
|
|
|
resultLevel: ResultLevel;
|
|
|
|
}): RecordGetResponse {
|
|
|
|
let numResults = 0;
|
|
|
|
const indexKeys: Key[] = [];
|
|
|
|
const primaryKeys: Key[] = [];
|
|
|
|
const values: Value[] = [];
|
|
|
|
const { unique, range, forward, indexData } = req;
|
|
|
|
|
2022-01-11 21:00:12 +01:00
|
|
|
function nextIndexEntry(prevPos: IDBValidKey): IndexRecord | undefined {
|
2021-12-15 02:37:03 +01:00
|
|
|
const res: [IDBValidKey, IndexRecord] | undefined = forward
|
2022-01-11 21:00:12 +01:00
|
|
|
? indexData.nextHigherPair(prevPos)
|
|
|
|
: indexData.nextLowerPair(prevPos);
|
|
|
|
return res ? res[1] : undefined;
|
2021-12-15 02:37:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function packResult(): RecordGetResponse {
|
2022-01-11 21:00:12 +01:00
|
|
|
// Collect the values based on the primary keys,
|
|
|
|
// if requested.
|
|
|
|
if (req.resultLevel === ResultLevel.Full) {
|
|
|
|
for (let i = 0; i < numResults; i++) {
|
|
|
|
const result = req.storeData.get(primaryKeys[i]);
|
|
|
|
if (!result) {
|
|
|
|
console.error("invariant violated during read");
|
|
|
|
console.error("request was", req);
|
|
|
|
throw Error("invariant violated during read");
|
|
|
|
}
|
|
|
|
values.push(structuredClone(result.value));
|
|
|
|
}
|
|
|
|
}
|
2021-12-15 02:37:03 +01:00
|
|
|
return {
|
|
|
|
count: numResults,
|
|
|
|
indexKeys:
|
|
|
|
req.resultLevel >= ResultLevel.OnlyKeys ? indexKeys : undefined,
|
|
|
|
primaryKeys:
|
|
|
|
req.resultLevel >= ResultLevel.OnlyKeys ? primaryKeys : undefined,
|
|
|
|
values: req.resultLevel >= ResultLevel.Full ? values : undefined,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-01-11 21:00:12 +01:00
|
|
|
let firstIndexPos = req.lastIndexPosition;
|
|
|
|
{
|
|
|
|
const rangeStart = forward ? range.lower : range.upper;
|
|
|
|
const dataStart = forward ? indexData.minKey() : indexData.maxKey();
|
|
|
|
firstIndexPos = furthestKey(forward, firstIndexPos, rangeStart);
|
|
|
|
firstIndexPos = furthestKey(forward, firstIndexPos, dataStart);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (firstIndexPos == null) {
|
2021-12-15 02:37:03 +01:00
|
|
|
return packResult();
|
|
|
|
}
|
|
|
|
|
2022-01-11 21:00:12 +01:00
|
|
|
let objectStorePos: IDBValidKey | undefined = undefined;
|
|
|
|
let indexEntry: IndexRecord | undefined = undefined;
|
|
|
|
|
2021-12-15 02:37:03 +01:00
|
|
|
// Now we align at indexPos and after objectStorePos
|
|
|
|
|
2022-01-11 21:00:12 +01:00
|
|
|
indexEntry = indexData.get(firstIndexPos);
|
2021-12-15 02:37:03 +01:00
|
|
|
if (!indexEntry) {
|
|
|
|
// We're not aligned to an index key, go to next index entry
|
2022-01-11 21:00:12 +01:00
|
|
|
indexEntry = nextIndexEntry(firstIndexPos);
|
|
|
|
if (!indexEntry) {
|
|
|
|
return packResult();
|
|
|
|
}
|
|
|
|
objectStorePos = nextKey(true, indexEntry.primaryKeys, undefined);
|
|
|
|
} else if (
|
|
|
|
req.lastIndexPosition != null &&
|
|
|
|
compareKeys(req.lastIndexPosition, indexEntry.indexKey) !== 0
|
|
|
|
) {
|
|
|
|
// We're already past the desired lastIndexPosition, don't use
|
|
|
|
// lastObjectStorePosition.
|
|
|
|
objectStorePos = nextKey(true, indexEntry.primaryKeys, undefined);
|
|
|
|
} else {
|
2021-12-15 02:37:03 +01:00
|
|
|
objectStorePos = nextKey(
|
|
|
|
true,
|
|
|
|
indexEntry.primaryKeys,
|
|
|
|
req.lastObjectStorePosition,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-01-11 21:00:12 +01:00
|
|
|
// Now skip lower/upper bound of open ranges
|
|
|
|
|
2021-12-15 02:37:03 +01:00
|
|
|
if (
|
|
|
|
forward &&
|
|
|
|
range.lowerOpen &&
|
|
|
|
range.lower != null &&
|
2022-01-11 21:00:12 +01:00
|
|
|
compareKeys(range.lower, indexEntry.indexKey) === 0
|
2021-12-15 02:37:03 +01:00
|
|
|
) {
|
2022-01-11 21:00:12 +01:00
|
|
|
indexEntry = nextIndexEntry(indexEntry.indexKey);
|
|
|
|
if (!indexEntry) {
|
|
|
|
return packResult();
|
|
|
|
}
|
|
|
|
objectStorePos = indexEntry.primaryKeys.minKey();
|
2021-12-15 02:37:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
!forward &&
|
|
|
|
range.upperOpen &&
|
|
|
|
range.upper != null &&
|
2022-01-11 21:00:12 +01:00
|
|
|
compareKeys(range.upper, indexEntry.indexKey) === 0
|
2021-12-15 02:37:03 +01:00
|
|
|
) {
|
2022-01-11 21:00:12 +01:00
|
|
|
indexEntry = nextIndexEntry(indexEntry.indexKey);
|
|
|
|
if (!indexEntry) {
|
|
|
|
return packResult();
|
|
|
|
}
|
|
|
|
objectStorePos = indexEntry.primaryKeys.minKey();
|
2021-12-15 02:37:03 +01:00
|
|
|
}
|
|
|
|
|
2022-01-11 21:00:12 +01:00
|
|
|
// If requested, return only unique results
|
|
|
|
|
2021-12-15 02:37:03 +01:00
|
|
|
if (
|
|
|
|
unique &&
|
|
|
|
req.lastIndexPosition != null &&
|
2022-01-11 21:00:12 +01:00
|
|
|
compareKeys(indexEntry.indexKey, req.lastIndexPosition) === 0
|
2021-12-15 02:37:03 +01:00
|
|
|
) {
|
2022-01-11 21:00:12 +01:00
|
|
|
indexEntry = nextIndexEntry(indexEntry.indexKey);
|
|
|
|
if (!indexEntry) {
|
|
|
|
return packResult();
|
|
|
|
}
|
|
|
|
objectStorePos = indexEntry.primaryKeys.minKey();
|
2021-12-15 02:37:03 +01:00
|
|
|
}
|
|
|
|
|
2022-01-11 21:00:12 +01:00
|
|
|
if (req.advanceIndexKey != null) {
|
|
|
|
const ik = furthestKey(forward, indexEntry.indexKey, req.advanceIndexKey)!;
|
|
|
|
indexEntry = indexData.get(ik);
|
|
|
|
if (!indexEntry) {
|
|
|
|
indexEntry = nextIndexEntry(ik);
|
|
|
|
}
|
|
|
|
if (!indexEntry) {
|
|
|
|
return packResult();
|
2021-12-15 02:37:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use advancePrimaryKey if necessary
|
|
|
|
if (
|
|
|
|
req.advanceIndexKey != null &&
|
|
|
|
req.advancePrimaryKey &&
|
2022-01-11 21:00:12 +01:00
|
|
|
compareKeys(indexEntry.indexKey, req.advanceIndexKey) == 0
|
2021-12-15 02:37:03 +01:00
|
|
|
) {
|
|
|
|
if (
|
|
|
|
objectStorePos == null ||
|
|
|
|
compareKeys(req.advancePrimaryKey, objectStorePos) > 0
|
|
|
|
) {
|
|
|
|
objectStorePos = nextKey(
|
|
|
|
true,
|
|
|
|
indexEntry.primaryKeys,
|
|
|
|
req.advancePrimaryKey,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
while (1) {
|
|
|
|
if (req.limit != 0 && numResults == req.limit) {
|
|
|
|
break;
|
|
|
|
}
|
2022-01-11 21:00:12 +01:00
|
|
|
if (!range.includes(indexEntry.indexKey)) {
|
2021-12-15 02:37:03 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (indexEntry === undefined) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (objectStorePos == null) {
|
|
|
|
// We don't have any more records with the current index key.
|
2022-01-11 21:00:12 +01:00
|
|
|
indexEntry = nextIndexEntry(indexEntry.indexKey);
|
|
|
|
if (!indexEntry) {
|
|
|
|
return packResult();
|
2021-12-15 02:37:03 +01:00
|
|
|
}
|
2022-01-11 21:00:12 +01:00
|
|
|
objectStorePos = indexEntry.primaryKeys.minKey();
|
2021-12-15 02:37:03 +01:00
|
|
|
continue;
|
|
|
|
}
|
2022-01-11 21:00:12 +01:00
|
|
|
|
|
|
|
indexKeys.push(structuredClone(indexEntry.indexKey));
|
|
|
|
primaryKeys.push(structuredClone(objectStorePos));
|
2021-12-15 02:37:03 +01:00
|
|
|
numResults++;
|
|
|
|
if (unique) {
|
|
|
|
objectStorePos = undefined;
|
|
|
|
} else {
|
|
|
|
objectStorePos = indexEntry.primaryKeys.nextHigherKey(objectStorePos);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return packResult();
|
|
|
|
}
|
|
|
|
|
|
|
|
function getObjectStoreRecords(req: {
|
|
|
|
storeData: ISortedMapF<IDBValidKey, ObjectStoreRecord>;
|
|
|
|
lastIndexPosition?: IDBValidKey;
|
|
|
|
forward: boolean;
|
|
|
|
range: IDBKeyRange;
|
|
|
|
lastObjectStorePosition?: IDBValidKey;
|
|
|
|
advancePrimaryKey?: IDBValidKey;
|
|
|
|
limit: number;
|
|
|
|
resultLevel: ResultLevel;
|
|
|
|
}): RecordGetResponse {
|
|
|
|
let numResults = 0;
|
|
|
|
const indexKeys: Key[] = [];
|
|
|
|
const primaryKeys: Key[] = [];
|
|
|
|
const values: Value[] = [];
|
|
|
|
const { storeData, range, forward } = req;
|
|
|
|
|
|
|
|
function packResult(): RecordGetResponse {
|
|
|
|
return {
|
|
|
|
count: numResults,
|
|
|
|
indexKeys:
|
|
|
|
req.resultLevel >= ResultLevel.OnlyKeys ? indexKeys : undefined,
|
|
|
|
primaryKeys:
|
|
|
|
req.resultLevel >= ResultLevel.OnlyKeys ? primaryKeys : undefined,
|
|
|
|
values: req.resultLevel >= ResultLevel.Full ? values : undefined,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const rangeStart = forward ? range.lower : range.upper;
|
|
|
|
const dataStart = forward ? storeData.minKey() : storeData.maxKey();
|
|
|
|
let storePos = req.lastObjectStorePosition;
|
|
|
|
storePos = furthestKey(forward, storePos, rangeStart);
|
|
|
|
storePos = furthestKey(forward, storePos, dataStart);
|
|
|
|
storePos = furthestKey(forward, storePos, req.advancePrimaryKey);
|
|
|
|
|
|
|
|
if (storePos != null) {
|
|
|
|
// Advance store position if we are either still at the last returned
|
|
|
|
// store key, or if we are currently not on a key.
|
|
|
|
const storeEntry = storeData.get(storePos);
|
|
|
|
if (
|
|
|
|
!storeEntry ||
|
|
|
|
(req.lastObjectStorePosition != null &&
|
|
|
|
compareKeys(req.lastObjectStorePosition, storePos) === 0)
|
|
|
|
) {
|
|
|
|
storePos = forward
|
|
|
|
? storeData.nextHigherKey(storePos)
|
|
|
|
: storeData.nextLowerKey(storePos);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
storePos = forward ? storeData.minKey() : storeData.maxKey();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
storePos != null &&
|
|
|
|
forward &&
|
|
|
|
range.lowerOpen &&
|
|
|
|
range.lower != null &&
|
|
|
|
compareKeys(range.lower, storePos) === 0
|
|
|
|
) {
|
|
|
|
storePos = storeData.nextHigherKey(storePos);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
storePos != null &&
|
|
|
|
!forward &&
|
|
|
|
range.upperOpen &&
|
|
|
|
range.upper != null &&
|
|
|
|
compareKeys(range.upper, storePos) === 0
|
|
|
|
) {
|
|
|
|
storePos = storeData.nextLowerKey(storePos);
|
|
|
|
}
|
|
|
|
|
|
|
|
while (1) {
|
|
|
|
if (req.limit != 0 && numResults == req.limit) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (storePos === null || storePos === undefined) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (!range.includes(storePos)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
const res = storeData.get(storePos);
|
|
|
|
|
|
|
|
if (res === undefined) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (req.resultLevel >= ResultLevel.OnlyKeys) {
|
|
|
|
primaryKeys.push(structuredClone(storePos));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (req.resultLevel >= ResultLevel.Full) {
|
|
|
|
values.push(structuredClone(res.value));
|
|
|
|
}
|
|
|
|
|
|
|
|
numResults++;
|
|
|
|
storePos = nextStoreKey(forward, storeData, storePos);
|
|
|
|
}
|
|
|
|
|
|
|
|
return packResult();
|
|
|
|
}
|