type MutableRequest = { result: Value; error: DOMException | null; onsuccess: ((event: Event) => unknown) | null; onerror: ((event: Event) => unknown) | null; }; type MutableOpenRequest = MutableRequest & { transaction: IDBTransaction | null; onblocked: ((event: IDBVersionChangeEvent) => unknown) | null; onupgradeneeded: ((event: IDBVersionChangeEvent) => unknown) | null; }; type IndexState = { keyPath: string | string[]; unique: boolean; multiEntry: boolean; }; type StoreState = { keyPath: string; autoIncrement: boolean; data: Map; indexes: Map; }; type FakeKeyRange = | Readonly<{ fakeKind: "ONLY"; boundary: IDBValidKey; }> | Readonly<{ fakeKind: "LOWER"; boundary: IDBValidKey; open: boolean; }> | Readonly<{ fakeKind: "UPPER"; boundary: IDBValidKey; open: boolean; }> | Readonly<{ fakeKind: "BOUND"; lower: IDBValidKey; upper: IDBValidKey; lowerOpen: boolean; upperOpen: boolean; }>; function createRequest(): MutableRequest { return { result: undefined as Value, error: null, onsuccess: null, onerror: null, }; } function cloneStore(state: StoreState): StoreState { return { keyPath: state.keyPath, autoIncrement: state.autoIncrement, data: new Map( Array.from(state.data, ([key, value]) => [ key, structuredClone(value), ]), ), indexes: new Map( Array.from(state.indexes, ([name, index]) => [ name, { ...index, keyPath: Array.isArray(index.keyPath) ? [...index.keyPath] : index.keyPath, }, ]), ), }; } function cloneStores( stores: ReadonlyMap, ): Map { return new Map( Array.from(stores, ([name, state]) => [name, cloneStore(state)]), ); } function asException(error: unknown): DOMException { return error instanceof DOMException ? error : new DOMException("Fake IndexedDB failure.", "UnknownError"); } function stringList(values: Iterable): DOMStringList { const items = [...values]; return { contains: (value: string) => items.includes(value), item: (index: number) => items[index] ?? null, get length() { return items.length; }, [Symbol.iterator]: () => items[Symbol.iterator](), } as DOMStringList; } function readPath(value: unknown, keyPath: string): unknown { let current = value; for (const segment of keyPath.split(".")) { if (!current || typeof current !== "object") return undefined; current = (current as Record)[segment]; } return current; } function primaryKey(state: StoreState, value: unknown): string { const key = readPath(value, state.keyPath); if ( (typeof key !== "string" && typeof key !== "number") || String(key).length === 0 ) { throw new DOMException("Invalid fake key.", "DataError"); } return String(key); } function compareKeys(first: IDBValidKey, second: IDBValidKey): number { const left = first instanceof Date ? first.getTime() : typeof first === "number" || typeof first === "string" ? first : JSON.stringify(first); const right = second instanceof Date ? second.getTime() : typeof second === "number" || typeof second === "string" ? second : JSON.stringify(second); if (left === right) return 0; if (typeof left === "number" && typeof right === "number") { return left < right ? -1 : 1; } return String(left) < String(right) ? -1 : 1; } class FakeUpgradeTransaction { aborted = false; constructor(private readonly database: FakeDatabase) {} abort(): void { this.aborted = true; } objectStore(name: string): IDBObjectStore { return this.database.upgradeObjectStore(name, this); } request(operation: () => Value): IDBRequest { const request = createRequest(); try { request.result = structuredClone(operation()); queueMicrotask(() => request.onsuccess?.(new Event("success"))); } catch (error) { request.error = asException(error); this.aborted = true; queueMicrotask(() => request.onerror?.(new Event("error"))); } return request as unknown as IDBRequest; } } class FakeDatabase { version = 0; closed = false; onversionchange: ((event: IDBVersionChangeEvent) => unknown) | null = null; onclose: ((event: Event) => unknown) | null = null; constructor(private readonly owner: MemoryIndexedDbFactory) {} get objectStoreNames(): DOMStringList { return stringList(this.owner.storeNames()); } createObjectStore( name: string, options?: IDBObjectStoreParameters, ): IDBObjectStore { if (this.owner.hasStore(name)) { throw new DOMException("Store already exists.", "ConstraintError"); } if (typeof options?.keyPath !== "string") { throw new DOMException("The fake requires a string key path.", "DataError"); } this.owner.createStore(name, { keyPath: options.keyPath, autoIncrement: options.autoIncrement ?? false, data: new Map(), indexes: new Map(), }); return this.schemaObjectStore(name); } deleteObjectStore(name: string): void { if (!this.owner.deleteStore(name)) { throw new DOMException("Store does not exist.", "NotFoundError"); } } schemaObjectStore(name: string): IDBObjectStore { const state = this.owner.store(name); return new FakeObjectStore(null, state) as unknown as IDBObjectStore; } upgradeObjectStore( name: string, transaction: FakeUpgradeTransaction, ): IDBObjectStore { const state = this.owner.store(name); return new FakeObjectStore( null, state, transaction, ) as unknown as IDBObjectStore; } transaction( storeNames: string | Iterable, mode: IDBTransactionMode = "readonly", ): IDBTransaction { if (this.closed) { throw new DOMException("Connection is closed.", "InvalidStateError"); } const names = typeof storeNames === "string" ? [storeNames] : [...storeNames]; const transaction = new FakeTransaction(this.owner, names, mode); this.owner.noteTransaction(transaction); return transaction as unknown as IDBTransaction; } close(): void { this.closed = true; } fireVersionChange(newVersion: number): void { this.onversionchange?.({ oldVersion: this.version, newVersion, } as IDBVersionChangeEvent); } fireForcedClose(): void { this.closed = true; this.onclose?.(new Event("close")); } } class FakeTransaction { oncomplete: ((event: Event) => unknown) | null = null; onerror: ((event: Event) => unknown) | null = null; onabort: ((event: Event) => unknown) | null = null; error: DOMException | null = null; readonly mode: IDBTransactionMode; private active = true; private completionQueued = false; private pending = 0; private readonly workingStores: Map; constructor( private readonly owner: MemoryIndexedDbFactory, private readonly names: readonly string[], mode: IDBTransactionMode, ) { this.mode = mode; this.workingStores = new Map( names.map((name) => [name, cloneStore(owner.store(name))]), ); } objectStore(name: string): IDBObjectStore { if (!this.names.includes(name)) { throw new DOMException("Store is outside transaction.", "NotFoundError"); } const state = this.workingStores.get(name); if (!state) { throw new DOMException("Store does not exist.", "NotFoundError"); } return new FakeObjectStore(this, state) as unknown as IDBObjectStore; } abort(): void { if (!this.active) { throw new DOMException("Transaction is inactive.", "InvalidStateError"); } this.abortWith(new DOMException("Transaction aborted.", "AbortError")); } request(operation: () => Value): IDBRequest { const request = createRequest(); this.pending += 1; this.owner.scheduleTransaction(() => { if (!this.active) return; try { request.result = structuredClone(operation()); request.onsuccess?.(new Event("success")); } catch (error) { request.error = asException(error); request.onerror?.(new Event("error")); this.abortWith(request.error); } if (!this.active) return; this.pending -= 1; this.queueCompletion(); }, this.mode); return request as unknown as IDBRequest; } cursor( rows: readonly Readonly<{ key: IDBValidKey; primaryKey: IDBValidKey; value: unknown; }>[], direction: IDBCursorDirection, ): IDBRequest { const request = createRequest(); let position = 0; this.pending += 1; const emit = () => { this.owner.scheduleTransaction(() => { if (!this.active) return; let continued = false; let eventReturned = false; const row = rows[position]; if (!row) { request.result = null; } else { request.result = { key: structuredClone(row.key), primaryKey: structuredClone(row.primaryKey), value: structuredClone(row.value), continue: (key?: IDBValidKey) => { if (continued) { throw new DOMException( "Cursor already continued.", "InvalidStateError", ); } continued = true; if (eventReturned) this.pending += 1; if (key === undefined) { position += 1; } else { const nextPosition = rows.findIndex( (candidate, candidateIndex) => candidateIndex > position && (direction === "prev" || direction === "prevunique" ? compareKeys(candidate.key, key) <= 0 : compareKeys(candidate.key, key) >= 0), ); position = nextPosition < 0 ? rows.length : nextPosition; } emit(); }, continuePrimaryKey: ( key: IDBValidKey, primaryKey: IDBValidKey, ) => { if (continued) { throw new DOMException( "Cursor already continued.", "InvalidStateError", ); } continued = true; if (eventReturned) this.pending += 1; const nextPosition = rows.findIndex( (candidate, candidateIndex) => { if (candidateIndex <= position) return false; const keyOrder = compareKeys(candidate.key, key); const tupleOrder = keyOrder === 0 ? compareKeys( candidate.primaryKey, primaryKey, ) : keyOrder; return direction === "prev" || direction === "prevunique" ? tupleOrder <= 0 : tupleOrder >= 0; }, ); position = nextPosition < 0 ? rows.length : nextPosition; emit(); }, } as unknown as IDBCursorWithValue; } request.onsuccess?.(new Event("success")); eventReturned = true; if (continued || !this.active) return; this.pending -= 1; this.queueCompletion(); }, this.mode); }; emit(); return request as unknown as IDBRequest; } assertWritable(): void { if (this.mode !== "readwrite") { throw new DOMException("Transaction is read-only.", "ReadOnlyError"); } } private queueCompletion(): void { if ( !this.active || this.pending !== 0 || this.completionQueued ) { return; } this.completionQueued = true; this.owner.scheduleTransaction(() => { this.completionQueued = false; if (!this.active || this.pending !== 0) return; const commitFailure = this.mode === "readwrite" ? this.owner.consumeCommitFailure() : null; if (commitFailure) { this.abortWith(commitFailure); return; } if (this.mode === "readwrite") { for (const [name, state] of this.workingStores) { this.owner.replaceStore(name, cloneStore(state)); } } this.active = false; this.oncomplete?.(new Event("complete")); }, this.mode); } private abortWith(error: DOMException): void { if (!this.active) return; this.active = false; this.error = error; this.owner.scheduleLifecycle(() => { this.onerror?.(new Event("error")); this.onabort?.(new Event("abort")); }); } } class FakeObjectStore { constructor( private readonly transaction: FakeTransaction | null, private readonly state: StoreState, private readonly upgradeTransaction: FakeUpgradeTransaction | null = null, ) {} get indexNames(): DOMStringList { return stringList(this.state.indexes.keys()); } get(query: IDBValidKey | IDBKeyRange): IDBRequest { return this.request(() => this.state.data.get(String(query)), ); } put(value: unknown): IDBRequest { this.assertWritable(); return this.request(() => { const key = primaryKey(this.state, value); this.state.data.set(key, structuredClone(value)); return key as IDBValidKey; }); } add(value: unknown): IDBRequest { this.assertWritable(); return this.request(() => { const key = primaryKey(this.state, value); if (this.state.data.has(key)) { throw new DOMException("Key already exists.", "ConstraintError"); } this.state.data.set(key, structuredClone(value)); return key as IDBValidKey; }); } delete(query: IDBValidKey | IDBKeyRange): IDBRequest { this.assertWritable(); return this.request(() => { this.state.data.delete(String(query)); return undefined; }); } openCursor( query?: IDBValidKey | IDBKeyRange | null, direction: IDBCursorDirection = "next", ): IDBRequest { return this.requireTransaction().cursor( this.rows(undefined, direction, query), direction, ); } index(name: string): IDBIndex { if (!this.state.indexes.has(name)) { throw new DOMException("Index does not exist.", "NotFoundError"); } return new FakeIndex( this.requireTransaction(), this.state, name, ) as unknown as IDBIndex; } createIndex( name: string, keyPath: string | string[], options?: IDBIndexParameters, ): IDBIndex { if (this.state.indexes.has(name)) { throw new DOMException("Index already exists.", "ConstraintError"); } this.state.indexes.set(name, { keyPath: Array.isArray(keyPath) ? [...keyPath] : keyPath, unique: options?.unique ?? false, multiEntry: options?.multiEntry ?? false, }); return {} as IDBIndex; } deleteIndex(name: string): void { if (!this.state.indexes.delete(name)) { throw new DOMException("Index does not exist.", "NotFoundError"); } } rows( indexName: string | undefined, direction: IDBCursorDirection, query?: IDBValidKey | IDBKeyRange | null, ): readonly Readonly<{ key: IDBValidKey; primaryKey: IDBValidKey; value: unknown; }>[] { const index = indexName ? this.state.indexes.get(indexName) : undefined; let rows = Array.from(this.state.data, ([key, value]) => { const indexKey = index && typeof index.keyPath === "string" ? readPath(value, index.keyPath) : key; if (index && indexKey === undefined) return null; return { key: indexKey as IDBValidKey, primaryKey: key, value, }; }).filter( ( row, ): row is { key: IDBValidKey; primaryKey: string; value: unknown; } => row !== null, ); if ( query && typeof query === "object" && "fakeKind" in query ) { const range = query as unknown as FakeKeyRange; rows = rows.filter((row) => { if (range.fakeKind === "BOUND") { const lower = compareKeys(row.key, range.lower); const upper = compareKeys(row.key, range.upper); return ( (range.lowerOpen ? lower > 0 : lower >= 0) && (range.upperOpen ? upper < 0 : upper <= 0) ); } const comparison = compareKeys(row.key, range.boundary); if (range.fakeKind === "ONLY") return comparison === 0; if (range.fakeKind === "LOWER") { return range.open ? comparison > 0 : comparison >= 0; } return range.open ? comparison < 0 : comparison <= 0; }); } rows.sort((left, right) => { const indexOrder = compareKeys(left.key, right.key); return indexOrder === 0 ? compareKeys(left.primaryKey, right.primaryKey) : indexOrder; }); if (direction === "prev" || direction === "prevunique") { rows.reverse(); } return rows; } private requireTransaction(): FakeTransaction { if (!this.transaction) { throw new DOMException( "Schema object store has no data transaction.", "TransactionInactiveError", ); } return this.transaction; } private request(operation: () => Value): IDBRequest { if (this.transaction) return this.transaction.request(operation); if (this.upgradeTransaction) { return this.upgradeTransaction.request(operation); } throw new DOMException( "Schema object store has no data transaction.", "TransactionInactiveError", ); } private assertWritable(): void { if (this.transaction) { this.transaction.assertWritable(); return; } if (!this.upgradeTransaction) { throw new DOMException( "Schema object store has no data transaction.", "TransactionInactiveError", ); } } } class FakeIndex { constructor( private readonly transaction: FakeTransaction, private readonly state: StoreState, private readonly name: string, ) {} openCursor( query?: IDBValidKey | IDBKeyRange | null, direction: IDBCursorDirection = "next", ): IDBRequest { const store = new FakeObjectStore(this.transaction, this.state); return this.transaction.cursor( store.rows(this.name, direction, query), direction, ); } } /** * Minimal deterministic IndexedDB model for adapter contract tests. It models * request ordering, transaction-level commit/rollback and upgrade blocking; * it is not intended to polyfill IndexedDB for application code. */ export class MemoryIndexedDbFactory { private stores = new Map(); private databaseVersion = 0; private latestDatabase = new FakeDatabase(this); private blockedRequest: Readonly<{ request: MutableOpenRequest; targetVersion: number; database: FakeDatabase; }> | null = null; private transactionQueue: Array<() => void> = []; private transactionsPaused = false; private nextCommitFailure: DOMException | null = null; private shouldBlockNextOpen = false; lastTransaction: FakeTransaction | null = null; readonly factory: IDBFactory = { open: (_name: string, version?: number) => this.open(version ?? 1) as unknown as IDBOpenDBRequest, cmp: compareKeys, } as IDBFactory; readonly keyRange: Pick< typeof IDBKeyRange, "only" | "lowerBound" | "upperBound" | "bound" > = { only: (value: IDBValidKey) => ({ fakeKind: "ONLY", boundary: structuredClone(value), }) as unknown as IDBKeyRange, lowerBound: (bound: IDBValidKey, open = false) => ({ fakeKind: "LOWER", boundary: structuredClone(bound), open, }) as unknown as IDBKeyRange, upperBound: (bound: IDBValidKey, open = false) => ({ fakeKind: "UPPER", boundary: structuredClone(bound), open, }) as unknown as IDBKeyRange, bound: ( lower: IDBValidKey, upper: IDBValidKey, lowerOpen = false, upperOpen = false, ) => ({ fakeKind: "BOUND", lower: structuredClone(lower), upper: structuredClone(upper), lowerOpen, upperOpen, }) as unknown as IDBKeyRange, }; open(version: number): MutableOpenRequest { const database = new FakeDatabase(this); database.version = this.databaseVersion; this.latestDatabase = database; const request: MutableOpenRequest = { ...createRequest(), result: database as unknown as IDBDatabase, transaction: null, onblocked: null, onupgradeneeded: null, }; queueMicrotask(() => { if (this.shouldBlockNextOpen) { this.shouldBlockNextOpen = false; this.blockedRequest = { request, targetVersion: version, database, }; request.onblocked?.({ oldVersion: this.databaseVersion, newVersion: version, } as IDBVersionChangeEvent); return; } this.finishOpen(request, version, database); }); return request; } blockNextOpen(): void { this.shouldBlockNextOpen = true; } releaseBlockedOpen(): void { const blocked = this.blockedRequest; this.blockedRequest = null; if (blocked) { this.finishOpen( blocked.request, blocked.targetVersion, blocked.database, ); } } pauseTransactions(): void { this.transactionsPaused = true; } clearLastTransaction(): void { this.lastTransaction = null; } resumeTransactions(): void { this.transactionsPaused = false; const queued = this.transactionQueue; this.transactionQueue = []; for (const callback of queued) queueMicrotask(callback); } failNextWriteCommit(error: DOMException): void { this.nextCommitFailure = error; } triggerVersionChange(newVersion: number): void { this.latestDatabase.fireVersionChange(newVersion); } triggerForcedClose(): void { this.latestDatabase.fireForcedClose(); } isConnectionClosed(): boolean { return this.latestDatabase.closed; } seed(storeName: string, value: unknown): void { const state = this.store(storeName); const key = primaryKey(state, value); state.data.set(key, structuredClone(value)); } readRaw(storeName: string, key: string): unknown { const value = this.store(storeName).data.get(key); return value === undefined ? undefined : structuredClone(value); } scheduleTransaction( callback: () => void, mode?: IDBTransactionMode, ): void { if (this.transactionsPaused && mode === "readwrite") { this.transactionQueue.push(callback); return; } queueMicrotask(callback); } scheduleLifecycle(callback: () => void): void { queueMicrotask(callback); } consumeCommitFailure(): DOMException | null { const failure = this.nextCommitFailure; this.nextCommitFailure = null; return failure; } noteTransaction(transaction: FakeTransaction): void { this.lastTransaction = transaction; } storeNames(): Iterable { return this.stores.keys(); } hasStore(name: string): boolean { return this.stores.has(name); } store(name: string): StoreState { const state = this.stores.get(name); if (!state) { throw new DOMException("Store does not exist.", "NotFoundError"); } return state; } createStore(name: string, state: StoreState): void { this.stores.set(name, state); } replaceStore(name: string, state: StoreState): void { this.stores.set(name, state); } deleteStore(name: string): boolean { return this.stores.delete(name); } private finishOpen( request: MutableOpenRequest, targetVersion: number, database: FakeDatabase, ): void { if (targetVersion < this.databaseVersion) { request.error = new DOMException( "Requested version is older.", "VersionError", ); request.onerror?.(new Event("error")); return; } if (targetVersion > this.databaseVersion) { const before = cloneStores(this.stores); const oldVersion = this.databaseVersion; const upgrade = new FakeUpgradeTransaction(database); request.transaction = upgrade as unknown as IDBTransaction; database.version = targetVersion; request.onupgradeneeded?.({ oldVersion, newVersion: targetVersion, } as IDBVersionChangeEvent); request.transaction = null; if (upgrade.aborted) { this.stores = before; database.version = oldVersion; request.error = new DOMException( "Upgrade transaction aborted.", "AbortError", ); request.onerror?.(new Event("error")); return; } this.databaseVersion = targetVersion; } database.version = this.databaseVersion; request.result = database as unknown as IDBDatabase; request.onsuccess?.(new Event("success")); } }