summaryrefslogtreecommitdiff
path: root/packages/idb-bridge/src/MemoryBackend.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/idb-bridge/src/MemoryBackend.ts')
-rw-r--r--packages/idb-bridge/src/MemoryBackend.ts367
1 files changed, 209 insertions, 158 deletions
diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts
index f40f1c98b..526920a9f 100644
--- a/packages/idb-bridge/src/MemoryBackend.ts
+++ b/packages/idb-bridge/src/MemoryBackend.ts
@@ -14,43 +14,38 @@
permissions and limitations under the License.
*/
+import { AsyncCondition, TransactionLevel } from "./backend-common.js";
import {
Backend,
+ ConnectResult,
DatabaseConnection,
DatabaseTransaction,
- Schema,
- RecordStoreRequest,
- IndexProperties,
- RecordGetRequest,
+ IndexGetQuery,
+ IndexMeta,
+ ObjectStoreGetQuery,
+ ObjectStoreMeta,
RecordGetResponse,
+ RecordStoreRequest,
+ RecordStoreResponse,
ResultLevel,
StoreLevel,
- RecordStoreResponse,
} from "./backend-interface.js";
+import { BridgeIDBKeyRange } from "./bridge-idb.js";
+import { IDBKeyRange, IDBTransactionMode, IDBValidKey } from "./idbtypes.js";
+import BTree, { ISortedMapF, ISortedSetF } from "./tree/b+tree.js";
+import { compareKeys } from "./util/cmp.js";
+import { ConstraintError, DataError } from "./util/errors.js";
+import { getIndexKeys } from "./util/getIndexKeys.js";
+import { StoreKeyResult, makeStoreKeyValue } from "./util/makeStoreKeyValue.js";
import {
structuredClone,
structuredEncapsulate,
structuredRevive,
} 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";
type Key = IDBValidKey;
type Value = unknown;
-enum TransactionLevel {
- None = 0,
- Read = 1,
- Write = 2,
- VersionChange = 3,
-}
-
interface ObjectStore {
originalName: string;
modifiedName: string | undefined;
@@ -95,24 +90,39 @@ interface Database {
connectionCookies: string[];
}
-/** @public */
export interface ObjectStoreDump {
name: string;
keyGenerator: number;
records: ObjectStoreRecord[];
}
-/** @public */
export interface DatabaseDump {
schema: Schema;
objectStores: { [name: string]: ObjectStoreDump };
}
-/** @public */
export interface MemoryBackendDump {
databases: { [name: string]: DatabaseDump };
}
+export interface ObjectStoreProperties {
+ keyPath: string | string[] | null;
+ autoIncrement: boolean;
+ indexes: { [nameame: string]: IndexProperties };
+}
+
+export interface IndexProperties {
+ keyPath: string | string[];
+ multiEntry: boolean;
+ unique: boolean;
+}
+
+export interface Schema {
+ databaseName: string;
+ databaseVersion: number;
+ objectStores: { [name: string]: ObjectStoreProperties };
+}
+
interface ObjectStoreMapEntry {
store: ObjectStore;
indexMap: { [currentName: string]: Index };
@@ -142,27 +152,6 @@ export interface ObjectStoreRecord {
value: Value;
}
-class AsyncCondition {
- _waitPromise: Promise<void>;
- _resolveWaitPromise: () => void;
- constructor() {
- const op = openPromise<void>();
- this._waitPromise = op.promise;
- this._resolveWaitPromise = op.resolve;
- }
-
- wait(): Promise<void> {
- return this._waitPromise;
- }
-
- trigger(): void {
- this._resolveWaitPromise();
- const op = openPromise<void>();
- this._waitPromise = op.promise;
- this._resolveWaitPromise = op.resolve;
- }
-}
-
function nextStoreKey<T>(
forward: boolean,
data: ISortedMapF<Key, ObjectStoreRecord>,
@@ -178,12 +167,6 @@ function nextStoreKey<T>(
return res[1].primaryKey;
}
-function assertInvariant(cond: boolean): asserts cond {
- if (!cond) {
- throw Error("invariant failed");
- }
-}
-
function nextKey(
forward: boolean,
tree: ISortedSetF<IDBValidKey>,
@@ -230,6 +213,7 @@ function furthestKey(
}
export interface AccessStats {
+ primitiveStatements: number;
writeTransactions: number;
readTransactions: number;
writesPerStore: Record<string, number>;
@@ -279,6 +263,7 @@ export class MemoryBackend implements Backend {
trackStats: boolean = true;
accessStats: AccessStats = {
+ primitiveStatements: 0,
readTransactions: 0,
writeTransactions: 0,
readsPerStore: {},
@@ -459,7 +444,7 @@ export class MemoryBackend implements Backend {
delete this.databases[name];
}
- async connectDatabase(name: string): Promise<DatabaseConnection> {
+ async connectDatabase(name: string): Promise<ConnectResult> {
if (this.enableTracing) {
console.log(`TRACING: connectDatabase(${name})`);
}
@@ -498,7 +483,11 @@ export class MemoryBackend implements Backend {
this.connections[connectionCookie] = myConn;
- return { connectionCookie };
+ return {
+ conn: { connectionCookie },
+ version: database.committedSchema.databaseVersion,
+ objectStores: Object.keys(database.committedSchema.objectStores).sort(),
+ };
}
async beginTransaction(
@@ -601,14 +590,6 @@ export class MemoryBackend implements Backend {
this.disconnectCond.trigger();
}
- 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 {
@@ -619,36 +600,6 @@ export class MemoryBackend implements Backend {
return myConn;
}
- getSchema(dbConn: DatabaseConnection): Schema {
- if (this.enableTracing) {
- console.log(`TRACING: getSchema`);
- }
- const myConn = this.requireConnection(dbConn);
- const db = this.databases[myConn.dbName];
- if (!db) {
- throw Error("db not found");
- }
- return db.committedSchema;
- }
-
- getCurrentTransactionSchema(btx: DatabaseTransaction): Schema {
- const myConn = this.requireConnectionFromTransaction(btx);
- const db = this.databases[myConn.dbName];
- if (!db) {
- throw Error("db not found");
- }
- return myConn.modifiedSchema;
- }
-
- getInitialTransactionSchema(btx: DatabaseTransaction): Schema {
- const myConn = this.requireConnectionFromTransaction(btx);
- const db = this.databases[myConn.dbName];
- if (!db) {
- throw Error("db not found");
- }
- return db.committedSchema;
- }
-
renameIndex(
btx: DatabaseTransaction,
objectStoreName: string,
@@ -799,7 +750,7 @@ export class MemoryBackend implements Backend {
createObjectStore(
btx: DatabaseTransaction,
name: string,
- keyPath: string[] | null,
+ keyPath: string | string[] | null,
autoIncrement: boolean,
): void {
if (this.enableTracing) {
@@ -842,7 +793,7 @@ export class MemoryBackend implements Backend {
btx: DatabaseTransaction,
indexName: string,
objectStoreName: string,
- keyPath: string[],
+ keyPath: string | string[],
multiEntry: boolean,
unique: boolean,
): void {
@@ -1102,12 +1053,91 @@ export class MemoryBackend implements Backend {
}
}
- async getRecords(
+ async getObjectStoreRecords(
+ btx: DatabaseTransaction,
+ req: ObjectStoreGetQuery,
+ ): Promise<RecordGetResponse> {
+ if (this.enableTracing) {
+ console.log(`TRACING: getObjectStoreRecords`);
+ console.log("query", req);
+ }
+ const myConn = this.requireConnectionFromTransaction(btx);
+ 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");
+ }
+ if (
+ db.txRestrictObjectStores &&
+ !db.txRestrictObjectStores.includes(req.objectStoreName)
+ ) {
+ throw Error(
+ `Not allowed to access store '${
+ req.objectStoreName
+ }', transaction is over ${JSON.stringify(db.txRestrictObjectStores)}`,
+ );
+ }
+ const objectStoreMapEntry = myConn.objectStoreMap[req.objectStoreName];
+ if (!objectStoreMapEntry) {
+ throw Error("object store not found");
+ }
+
+ let range;
+ if (req.range == null) {
+ range = new BridgeIDBKeyRange(undefined, undefined, true, true);
+ } else {
+ range = req.range;
+ }
+
+ if (typeof range !== "object") {
+ throw Error(
+ "getObjectStoreRecords was given an invalid range (sanity check failed, not an object)",
+ );
+ }
+
+ if (!("lowerOpen" in range)) {
+ throw Error(
+ "getObjectStoreRecords was given an invalid range (sanity check failed, lowerOpen missing)",
+ );
+ }
+
+ const forward: boolean =
+ req.direction === "next" || req.direction === "nextunique";
+
+ const storeData =
+ objectStoreMapEntry.store.modifiedData ||
+ objectStoreMapEntry.store.originalData;
+
+ const resp = getObjectStoreRecords({
+ forward,
+ storeData,
+ limit: req.limit,
+ range,
+ resultLevel: req.resultLevel,
+ advancePrimaryKey: req.advancePrimaryKey,
+ lastObjectStorePosition: req.lastObjectStorePosition,
+ });
+ 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;
+ }
+ if (this.enableTracing) {
+ console.log(`TRACING: getRecords got ${resp.count} results`);
+ }
+ return resp;
+ }
+
+ async getIndexRecords(
btx: DatabaseTransaction,
- req: RecordGetRequest,
+ req: IndexGetQuery,
): Promise<RecordGetResponse> {
if (this.enableTracing) {
- console.log(`TRACING: getRecords`);
+ console.log(`TRACING: getIndexRecords`);
console.log("query", req);
}
const myConn = this.requireConnectionFromTransaction(btx);
@@ -1161,58 +1191,31 @@ export class MemoryBackend implements Backend {
objectStoreMapEntry.store.modifiedData ||
objectStoreMapEntry.store.originalData;
- const haveIndex = req.indexName !== undefined;
-
- let resp: RecordGetResponse;
-
- if (haveIndex) {
- const index =
- myConn.objectStoreMap[req.objectStoreName].indexMap[req.indexName!];
- const indexData = index.modifiedData || index.originalData;
- 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,
- });
- 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;
- }
- } else {
- if (req.advanceIndexKey !== undefined) {
- throw Error("unsupported request");
- }
- resp = getObjectStoreRecords({
- forward,
- storeData,
- limit: req.limit,
- range,
- resultLevel: req.resultLevel,
- advancePrimaryKey: req.advancePrimaryKey,
- lastIndexPosition: req.lastIndexPosition,
- lastObjectStorePosition: req.lastObjectStorePosition,
- });
- 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;
- }
+ const index =
+ myConn.objectStoreMap[req.objectStoreName].indexMap[req.indexName!];
+ const indexData = index.modifiedData || index.originalData;
+ const 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,
+ });
+ 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;
}
if (this.enableTracing) {
- console.log(`TRACING: getRecords got ${resp.count} results`);
+ console.log(`TRACING: getIndexRecords got ${resp.count} results`);
}
return resp;
}
@@ -1294,13 +1297,13 @@ export class MemoryBackend implements Backend {
let storeKeyResult: StoreKeyResult;
try {
- storeKeyResult = makeStoreKeyValue(
- storeReq.value,
- storeReq.key,
- keygen,
- autoIncrement,
- keyPath,
- );
+ storeKeyResult = makeStoreKeyValue({
+ value: storeReq.value,
+ key: storeReq.key,
+ currentKeyGenerator: keygen,
+ autoIncrement: autoIncrement,
+ keyPath: keyPath,
+ });
} catch (e) {
if (e instanceof DataError) {
const kp = JSON.stringify(keyPath);
@@ -1445,7 +1448,7 @@ export class MemoryBackend implements Backend {
}
}
- async rollback(btx: DatabaseTransaction): Promise<void> {
+ rollback(btx: DatabaseTransaction): void {
if (this.enableTracing) {
console.log(`TRACING: rollback`);
}
@@ -1536,6 +1539,57 @@ export class MemoryBackend implements Backend {
await this.afterCommitCallback();
}
}
+
+ getObjectStoreMeta(
+ dbConn: DatabaseConnection,
+ objectStoreName: string,
+ ): ObjectStoreMeta | undefined {
+ const conn = this.connections[dbConn.connectionCookie];
+ if (!conn) {
+ throw Error("db connection not found");
+ }
+ let schema = conn.modifiedSchema;
+ if (!schema) {
+ throw Error();
+ }
+ const storeInfo = schema.objectStores[objectStoreName];
+ if (!storeInfo) {
+ return undefined;
+ }
+ return {
+ autoIncrement: storeInfo.autoIncrement,
+ indexSet: Object.keys(storeInfo.indexes).sort(),
+ keyPath: structuredClone(storeInfo.keyPath),
+ };
+ }
+
+ getIndexMeta(
+ dbConn: DatabaseConnection,
+ objectStoreName: string,
+ indexName: string,
+ ): IndexMeta | undefined {
+ const conn = this.connections[dbConn.connectionCookie];
+ if (!conn) {
+ throw Error("db connection not found");
+ }
+ let schema = conn.modifiedSchema;
+ if (!schema) {
+ throw Error();
+ }
+ const storeInfo = schema.objectStores[objectStoreName];
+ if (!storeInfo) {
+ return undefined;
+ }
+ const indexInfo = storeInfo.indexes[indexName];
+ if (!indexInfo) {
+ return;
+ }
+ return {
+ keyPath: structuredClone(indexInfo.keyPath),
+ multiEntry: indexInfo.multiEntry,
+ unique: indexInfo.unique,
+ };
+ }
}
function getIndexRecords(req: {
@@ -1734,7 +1788,6 @@ function getIndexRecords(req: {
function getObjectStoreRecords(req: {
storeData: ISortedMapF<IDBValidKey, ObjectStoreRecord>;
- lastIndexPosition?: IDBValidKey;
forward: boolean;
range: IDBKeyRange;
lastObjectStorePosition?: IDBValidKey;
@@ -1743,7 +1796,6 @@ function getObjectStoreRecords(req: {
resultLevel: ResultLevel;
}): RecordGetResponse {
let numResults = 0;
- const indexKeys: Key[] = [];
const primaryKeys: Key[] = [];
const values: Value[] = [];
const { storeData, range, forward } = req;
@@ -1751,8 +1803,7 @@ function getObjectStoreRecords(req: {
function packResult(): RecordGetResponse {
return {
count: numResults,
- indexKeys:
- req.resultLevel >= ResultLevel.OnlyKeys ? indexKeys : undefined,
+ indexKeys: undefined,
primaryKeys:
req.resultLevel >= ResultLevel.OnlyKeys ? primaryKeys : undefined,
values: req.resultLevel >= ResultLevel.Full ? values : undefined,
@@ -1762,8 +1813,8 @@ function getObjectStoreRecords(req: {
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, rangeStart);
storePos = furthestKey(forward, storePos, req.advancePrimaryKey);
if (storePos != null) {