import type { IndexedDbCompareAndSwapInput, IndexedDbDeleteInput, IndexedDbWriteReceipt, } from "../../src/application/ports/browser-file-storage/indexeddb-port.ts"; import type { BrowserDataResult, } from "../../src/application/ports/browser-file-storage/shared.ts"; import type { PushControlV1, } from "../../src/contracts/web-push.ts"; import type { PushControlRepository, } from "../../src/adapters/web-push/push-association-fence-store.ts"; import { browserDataFailure, browserDataSuccess, } from "../../src/adapters/browser-file-storage/result.ts"; type StoredControl = Readonly<{ value: unknown; revision: number; }>; type Deferred = Readonly<{ promise: Promise; resolve(): void; }>; export type PausedPushControlWrite = Readonly<{ reached: Promise; release(): void; }>; export class FakePushControlRepository implements PushControlRepository { #stored: StoredControl | null = null; #closed = false; #pause: | Readonly<{ predicate(value: PushControlV1): boolean; reached: Deferred; released: Deferred; }> | null = null; #pausedRemove: | Readonly<{ reached: Deferred; released: Deferred; }> | null = null; open( signal?: AbortSignal, ): Promise> { return Promise.resolve(this.#available(signal, undefined)); } read( _key: string, signal?: AbortSignal, ): Promise< BrowserDataResult< Readonly<{ value: PushControlV1; revision: number }> | null > > { const unavailable = this.#unavailable(signal); if (unavailable) return Promise.resolve(unavailable); if (!this.#stored) return Promise.resolve(browserDataSuccess(null)); return Promise.resolve( browserDataSuccess( Object.freeze({ value: this.#stored.value as PushControlV1, revision: this.#stored.revision, }), ), ); } async compareAndSwap( input: IndexedDbCompareAndSwapInput, ): Promise> { const unavailable = this.#unavailable(input.signal); if (unavailable) return unavailable; const pause = this.#pause; if (pause?.predicate(input.value)) { this.#pause = null; pause.reached.resolve(); await pause.released.promise; } const lateUnavailable = this.#unavailable(input.signal); if (lateUnavailable) return lateUnavailable; const currentRevision = this.#stored?.revision ?? null; if (currentRevision !== input.expectedRevision) { return browserDataFailure("CONFLICT", "INDEXEDDB_WRITE"); } const revision = (currentRevision ?? 0) + 1; this.#stored = Object.freeze({ value: structuredClone(input.value), revision, }); return browserDataSuccess( Object.freeze({ key: input.key, revision, replayed: false, }), ); } async remove( input: IndexedDbDeleteInput, ): Promise> { const unavailable = this.#unavailable(input.signal); if (unavailable) return unavailable; const pause = this.#pausedRemove; if (pause) { this.#pausedRemove = null; pause.reached.resolve(); await pause.released.promise; } const lateUnavailable = this.#unavailable(input.signal); if (lateUnavailable) return lateUnavailable; if ( !this.#stored || this.#stored.revision !== input.expectedRevision ) { return browserDataFailure("CONFLICT", "INDEXEDDB_WRITE"); } const revision = input.expectedRevision + 1; this.#stored = null; return browserDataSuccess( Object.freeze({ key: input.key, revision, replayed: false, }), ); } close(): void { this.#closed = true; } seedRaw(value: unknown, revision = 1): void { this.#stored = Object.freeze({ value, revision }); } pauseNextWrite( predicate: (value: PushControlV1) => boolean, ): PausedPushControlWrite { if (this.#pause) { throw new Error("A push-control write is already paused."); } const reached = deferred(); const released = deferred(); this.#pause = Object.freeze({ predicate, reached, released }); return Object.freeze({ reached: reached.promise, release: released.resolve, }); } pauseNextRemove(): PausedPushControlWrite { if (this.#pausedRemove) { throw new Error("A push-control remove is already paused."); } const reached = deferred(); const released = deferred(); this.#pausedRemove = Object.freeze({ reached, released }); return Object.freeze({ reached: reached.promise, release: released.resolve, }); } #available( signal: AbortSignal | undefined, value: Value, ): BrowserDataResult { return ( this.#unavailable(signal) ?? browserDataSuccess(value) ); } #unavailable( signal: AbortSignal | undefined, ): BrowserDataResult | null { if (signal?.aborted) { return browserDataFailure("ABORTED", "INDEXEDDB_READ"); } return this.#closed ? browserDataFailure("UNAVAILABLE", "INDEXEDDB_OPEN", { retryable: true, recovery: "REOPEN", }) : null; } } export function createFakePushControlStoreDependencies(): Readonly<{ repository: FakePushControlRepository; idempotencyKeyFactory(): string; }> { const repository = new FakePushControlRepository(); let sequence = 0; return Object.freeze({ repository, idempotencyKeyFactory() { sequence += 1; return `push-control-test-${sequence}`; }, }); } function deferred(): Deferred { let resolvePromise: (() => void) | undefined; const promise = new Promise((resolve) => { resolvePromise = resolve; }); return Object.freeze({ promise, resolve() { resolvePromise?.(); }, }); }