diff options
Diffstat (limited to 'packages/idb-bridge/src/MemoryBackend.ts')
-rw-r--r-- | packages/idb-bridge/src/MemoryBackend.ts | 367 |
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) { |