diff options
Diffstat (limited to 'packages/idb-bridge')
-rw-r--r-- | packages/idb-bridge/src/MemoryBackend.ts | 216 | ||||
-rw-r--r-- | packages/idb-bridge/src/bridge-idb.ts | 8 | ||||
-rw-r--r-- | packages/idb-bridge/src/idb-wpt-ported/idbcursor-update-index.test.ts | 1 | ||||
-rw-r--r-- | packages/idb-bridge/src/index.ts | 1 | ||||
-rw-r--r-- | packages/idb-bridge/src/util/structuredClone.ts | 76 |
5 files changed, 229 insertions, 73 deletions
diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts index c1a1c12ea..b37dd376d 100644 --- a/packages/idb-bridge/src/MemoryBackend.ts +++ b/packages/idb-bridge/src/MemoryBackend.ts @@ -229,6 +229,16 @@ function furthestKey( } } +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>; +} + /** * Primitive in-memory backend. * @@ -266,6 +276,18 @@ export class MemoryBackend implements Backend { enableTracing: boolean = false; + trackStats: boolean = true; + + accessStats: AccessStats = { + readTransactions: 0, + writeTransactions: 0, + readsPerStore: {}, + readsPerIndex: {}, + readItemsPerIndex: {}, + readItemsPerStore: {}, + writesPerStore: {}, + }; + /** * Load the data in this IndexedDB backend from a dump in JSON format. * @@ -512,6 +534,14 @@ export class MemoryBackend implements Backend { throw Error("unsupported transaction mode"); } + if (this.trackStats) { + if (mode === "readonly") { + this.accessStats.readTransactions++; + } else if (mode === "readwrite") { + this.accessStats.writeTransactions++; + } + } + myDb.txRestrictObjectStores = [...objectStores]; this.connectionsByTransaction[transactionCookie] = myConn; @@ -1153,6 +1183,13 @@ export class MemoryBackend implements Backend { 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"); @@ -1167,6 +1204,13 @@ export class MemoryBackend implements Backend { 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; + } } if (this.enableTracing) { console.log(`TRACING: getRecords got ${resp.count} results`); @@ -1180,6 +1224,11 @@ export class MemoryBackend implements Backend { ): Promise<RecordStoreResponse> { if (this.enableTracing) { console.log(`TRACING: storeRecord`); + console.log( + `key ${storeReq.key}, record ${JSON.stringify( + structuredEncapsulate(storeReq.value), + )}`, + ); } const myConn = this.requireConnectionFromTransaction(btx); const db = this.databases[myConn.dbName]; @@ -1199,6 +1248,12 @@ export class MemoryBackend implements Backend { }', transaction is over ${JSON.stringify(db.txRestrictObjectStores)}`, ); } + + if (this.trackStats) { + this.accessStats.writesPerStore[storeReq.objectStoreName] = + (this.accessStats.writesPerStore[storeReq.objectStoreName] ?? 0) + 1; + } + const schema = myConn.modifiedSchema; const objectStoreMapEntry = myConn.objectStoreMap[storeReq.objectStoreName]; @@ -1275,7 +1330,9 @@ export class MemoryBackend implements Backend { } } - const objectStoreRecord: ObjectStoreRecord = { + const oldStoreRecord = modifiedData.get(key); + + const newObjectStoreRecord: ObjectStoreRecord = { // FIXME: We should serialize the key here, not just clone it. primaryKey: structuredClone(key), value: structuredClone(value), @@ -1283,7 +1340,7 @@ export class MemoryBackend implements Backend { objectStoreMapEntry.store.modifiedData = modifiedData.with( key, - objectStoreRecord, + newObjectStoreRecord, true, ); @@ -1297,6 +1354,11 @@ export class MemoryBackend implements Backend { } const indexProperties = schema.objectStores[storeReq.objectStoreName].indexes[indexName]; + + // Remove old index entry first! + if (oldStoreRecord) { + this.deleteFromIndex(index, key, oldStoreRecord.value, indexProperties); + } try { this.insertIntoIndex(index, key, value, indexProperties); } catch (e) { @@ -1482,31 +1544,28 @@ function getIndexRecords(req: { const primaryKeys: Key[] = []; const values: Value[] = []; const { unique, range, forward, indexData } = req; - let indexPos = req.lastIndexPosition; - let objectStorePos: IDBValidKey | undefined = undefined; - let indexEntry: IndexRecord | undefined = undefined; - const rangeStart = forward ? range.lower : range.upper; - const dataStart = forward ? indexData.minKey() : indexData.maxKey(); - indexPos = furthestKey(forward, indexPos, rangeStart); - indexPos = furthestKey(forward, indexPos, dataStart); - function nextIndexEntry(): IndexRecord | undefined { - assertInvariant(indexPos != null); + function nextIndexEntry(prevPos: IDBValidKey): IndexRecord | undefined { const res: [IDBValidKey, IndexRecord] | undefined = forward - ? indexData.nextHigherPair(indexPos) - : indexData.nextLowerPair(indexPos); - if (res) { - indexEntry = res[1]; - indexPos = indexEntry.indexKey; - return indexEntry; - } else { - indexEntry = undefined; - indexPos = undefined; - return undefined; - } + ? indexData.nextHigherPair(prevPos) + : indexData.nextLowerPair(prevPos); + return res ? res[1] : undefined; } function packResult(): RecordGetResponse { + // 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)); + } + } return { count: numResults, indexKeys: @@ -1517,18 +1576,39 @@ function getIndexRecords(req: { }; } - if (indexPos == null) { + 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) { return packResult(); } + let objectStorePos: IDBValidKey | undefined = undefined; + let indexEntry: IndexRecord | undefined = undefined; + // Now we align at indexPos and after objectStorePos - indexEntry = indexData.get(indexPos); + indexEntry = indexData.get(firstIndexPos); if (!indexEntry) { // We're not aligned to an index key, go to next index entry - nextIndexEntry(); - } - if (indexEntry) { + 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 { objectStorePos = nextKey( true, indexEntry.primaryKeys, @@ -1536,43 +1616,56 @@ function getIndexRecords(req: { ); } + // Now skip lower/upper bound of open ranges + if ( forward && range.lowerOpen && range.lower != null && - compareKeys(range.lower, indexPos) === 0 + compareKeys(range.lower, indexEntry.indexKey) === 0 ) { - const e = nextIndexEntry(); - objectStorePos = e?.primaryKeys.minKey(); + indexEntry = nextIndexEntry(indexEntry.indexKey); + if (!indexEntry) { + return packResult(); + } + objectStorePos = indexEntry.primaryKeys.minKey(); } if ( !forward && range.upperOpen && range.upper != null && - compareKeys(range.upper, indexPos) === 0 + compareKeys(range.upper, indexEntry.indexKey) === 0 ) { - const e = nextIndexEntry(); - objectStorePos = e?.primaryKeys.minKey(); + indexEntry = nextIndexEntry(indexEntry.indexKey); + if (!indexEntry) { + return packResult(); + } + objectStorePos = indexEntry.primaryKeys.minKey(); } + // If requested, return only unique results + if ( unique && - indexPos != null && req.lastIndexPosition != null && - compareKeys(indexPos, req.lastIndexPosition) === 0 + compareKeys(indexEntry.indexKey, req.lastIndexPosition) === 0 ) { - const e = nextIndexEntry(); - objectStorePos = e?.primaryKeys.minKey(); + indexEntry = nextIndexEntry(indexEntry.indexKey); + if (!indexEntry) { + return packResult(); + } + objectStorePos = indexEntry.primaryKeys.minKey(); } - if (req.advancePrimaryKey) { - indexPos = furthestKey(forward, indexPos, req.advanceIndexKey); - if (indexPos) { - indexEntry = indexData.get(indexPos); - if (!indexEntry) { - nextIndexEntry(); - } + 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(); } } @@ -1580,9 +1673,7 @@ function getIndexRecords(req: { if ( req.advanceIndexKey != null && req.advancePrimaryKey && - indexPos != null && - indexEntry && - compareKeys(indexPos, req.advanceIndexKey) == 0 + compareKeys(indexEntry.indexKey, req.advanceIndexKey) == 0 ) { if ( objectStorePos == null || @@ -1597,13 +1688,10 @@ function getIndexRecords(req: { } while (1) { - if (indexPos === undefined) { - break; - } if (req.limit != 0 && numResults == req.limit) { break; } - if (!range.includes(indexPos)) { + if (!range.includes(indexEntry.indexKey)) { break; } if (indexEntry === undefined) { @@ -1611,14 +1699,16 @@ function getIndexRecords(req: { } if (objectStorePos == null) { // We don't have any more records with the current index key. - nextIndexEntry(); - if (indexEntry) { - objectStorePos = indexEntry.primaryKeys.minKey(); + indexEntry = nextIndexEntry(indexEntry.indexKey); + if (!indexEntry) { + return packResult(); } + objectStorePos = indexEntry.primaryKeys.minKey(); continue; } - indexKeys.push(indexEntry.indexKey); - primaryKeys.push(objectStorePos); + + indexKeys.push(structuredClone(indexEntry.indexKey)); + primaryKeys.push(structuredClone(objectStorePos)); numResults++; if (unique) { objectStorePos = undefined; @@ -1627,20 +1717,6 @@ function getIndexRecords(req: { } } - // Now we can 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(result.value); - } - } - return packResult(); } diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts index 5d5f531b0..8264b43ec 100644 --- a/packages/idb-bridge/src/bridge-idb.ts +++ b/packages/idb-bridge/src/bridge-idb.ts @@ -64,7 +64,10 @@ import { makeStoreKeyValue } from "./util/makeStoreKeyValue"; import { normalizeKeyPath } from "./util/normalizeKeyPath"; import { openPromise } from "./util/openPromise"; import queueTask from "./util/queueTask"; -import { structuredClone } from "./util/structuredClone"; +import { + checkStructuredCloneOrThrow, + structuredClone, +} from "./util/structuredClone"; import { validateKeyPath } from "./util/validateKeyPath"; import { valueToKey } from "./util/valueToKey"; @@ -303,7 +306,7 @@ export class BridgeIDBCursor implements IDBCursor { try { // Only called for the side effect of throwing an exception - structuredClone(value); + checkStructuredCloneOrThrow(value); } catch (e) { throw new DataCloneError(); } @@ -327,6 +330,7 @@ export class BridgeIDBCursor implements IDBCursor { } const { btx } = this.source._confirmStartedBackendTransaction(); await this._backend.storeRecord(btx, storeReq); + // FIXME: update the index position here! }; return transaction._execRequestAsync({ operation, diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-update-index.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-update-index.test.ts index 538665457..dcbee2b16 100644 --- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-update-index.test.ts +++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-update-index.test.ts @@ -10,7 +10,6 @@ import { // IDBCursor.update() - index - modify a record in the object store test.cb("WPT test idbcursor_update_index.htm", (t) => { var db: any, - count = 0, records = [ { pKey: "primaryKey_0", iKey: "indexKey_0" }, { pKey: "primaryKey_1", iKey: "indexKey_1" }, diff --git a/packages/idb-bridge/src/index.ts b/packages/idb-bridge/src/index.ts index 0abbf1056..c4dbb8281 100644 --- a/packages/idb-bridge/src/index.ts +++ b/packages/idb-bridge/src/index.ts @@ -72,6 +72,7 @@ export type { }; export { MemoryBackend } from "./MemoryBackend"; +export type { AccessStats } from "./MemoryBackend"; // globalThis polyfill, see https://mathiasbynens.be/notes/globalthis (function () { diff --git a/packages/idb-bridge/src/util/structuredClone.ts b/packages/idb-bridge/src/util/structuredClone.ts index 51e4483e1..c33dc5e36 100644 --- a/packages/idb-bridge/src/util/structuredClone.ts +++ b/packages/idb-bridge/src/util/structuredClone.ts @@ -171,6 +171,75 @@ export function mkDeepClone() { } } +/** + * Check if an object is deeply cloneable. + * Only called for the side-effect of throwing an exception. + */ +export function mkDeepCloneCheckOnly() { + const refs = [] as any; + + return clone; + + function cloneArray(a: any) { + var keys = Object.keys(a); + refs.push(a); + for (var i = 0; i < keys.length; i++) { + var k = keys[i] as any; + var cur = a[k]; + checkCloneableOrThrow(cur); + if (typeof cur !== "object" || cur === null) { + // do nothing + } else if (cur instanceof Date) { + // do nothing + } else if (ArrayBuffer.isView(cur)) { + // do nothing + } else { + var index = refs.indexOf(cur); + if (index !== -1) { + // do nothing + } else { + clone(cur); + } + } + } + refs.pop(); + } + + function clone(o: any) { + checkCloneableOrThrow(o); + if (typeof o !== "object" || o === null) return o; + if (o instanceof Date) return; + if (Array.isArray(o)) return cloneArray(o); + if (o instanceof Map) return cloneArray(Array.from(o)); + if (o instanceof Set) return cloneArray(Array.from(o)); + refs.push(o); + for (var k in o) { + if (Object.hasOwnProperty.call(o, k) === false) continue; + var cur = o[k] as any; + checkCloneableOrThrow(cur); + if (typeof cur !== "object" || cur === null) { + // do nothing + } else if (cur instanceof Date) { + // do nothing + } else if (cur instanceof Map) { + cloneArray(Array.from(cur)); + } else if (cur instanceof Set) { + cloneArray(Array.from(cur)); + } else if (ArrayBuffer.isView(cur)) { + // do nothing + } else { + var i = refs.indexOf(cur); + if (i !== -1) { + // do nothing + } else { + clone(cur); + } + } + } + refs.pop(); + } +} + function internalEncapsulate( val: any, outRoot: any, @@ -358,3 +427,10 @@ export function structuredRevive(val: any): any { export function structuredClone(val: any): any { return mkDeepClone()(val); } + +/** + * Structured clone for IndexedDB. + */ +export function checkStructuredCloneOrThrow(val: any): void { + return mkDeepCloneCheckOnly()(val); +} |