feat: 기능 추가 과정중
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
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<void>;
|
||||
resolve(): void;
|
||||
}>;
|
||||
|
||||
export type PausedPushControlWrite = Readonly<{
|
||||
reached: Promise<void>;
|
||||
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<BrowserDataResult<void>> {
|
||||
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<PushControlV1>,
|
||||
): Promise<BrowserDataResult<IndexedDbWriteReceipt>> {
|
||||
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<BrowserDataResult<IndexedDbWriteReceipt>> {
|
||||
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<Value>(
|
||||
signal: AbortSignal | undefined,
|
||||
value: Value,
|
||||
): BrowserDataResult<Value> {
|
||||
return (
|
||||
this.#unavailable(signal) ?? browserDataSuccess(value)
|
||||
);
|
||||
}
|
||||
|
||||
#unavailable(
|
||||
signal: AbortSignal | undefined,
|
||||
): BrowserDataResult<never> | 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<void>((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
return Object.freeze({
|
||||
promise,
|
||||
resolve() {
|
||||
resolvePromise?.();
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user