import { HttpSchema, HttpSchemaMethod, HttpSchemaPath, HttpStatusCode } from '@zimic/http'; import { Default, PossiblePromise } from '@zimic/utils/types'; import HttpInterceptorImplementation from '../interceptor/HttpInterceptorImplementation'; import HttpRequestHandlerImplementation from './HttpRequestHandlerImplementation'; import { InternalHttpRequestHandler, SyncedRemoteHttpRequestHandler as PublicSyncedRemoteHttpRequestHandler, } from './types/public'; import { HttpInterceptorRequest, HttpInterceptorResponse, HttpRequestHandlerResponseDeclaration, HttpRequestHandlerResponseDeclarationFactory, HttpRequestHandlerResponseDelayFactory, InterceptedHttpInterceptorRequest, } from './types/requests'; import { HttpRequestHandlerRestriction } from './types/restrictions'; const UNSYNCED_PROPERTIES = new Set(['then'] satisfies (keyof Promise)[]); class RemoteHttpRequestHandler< Schema extends HttpSchema, Method extends HttpSchemaMethod, Path extends HttpSchemaPath, StatusCode extends HttpStatusCode = never, > implements InternalHttpRequestHandler { readonly type = 'remote'; implementation: HttpRequestHandlerImplementation; private syncPromises: Promise[] = []; private unsynced: this; private synced: this; constructor( interceptor: HttpInterceptorImplementation, method: Method, path: Path, ) { this.implementation = new HttpRequestHandlerImplementation(interceptor, method, path, this); this.unsynced = this; this.synced = this.createSyncedProxy(); } private createSyncedProxy() { return new Proxy(this, { has: (target, property) => { if (this.shouldBeHiddenPropertyWhenSynced(property)) { return false; } return Reflect.has(target, property); }, get: (target, property) => { if (this.shouldBeHiddenPropertyWhenSynced(property)) { return undefined; } return Reflect.get(target, property); }, }); } private shouldBeHiddenPropertyWhenSynced(property: string | symbol) { return UNSYNCED_PROPERTIES.has(property); } get method() { return this.implementation.method; } get path() { return this.implementation.path; } with(restriction: HttpRequestHandlerRestriction): this { this.implementation.with(restriction); return this.unsynced; } delay( minMilliseconds: number | HttpRequestHandlerResponseDelayFactory>, maxMilliseconds?: number, ): this { this.implementation.delay(minMilliseconds, maxMilliseconds); return this.unsynced; } respond( declaration: | HttpRequestHandlerResponseDeclaration, NewStatusCode> | HttpRequestHandlerResponseDeclarationFactory, NewStatusCode>, ): RemoteHttpRequestHandler { const newUnsyncedThis = this.unsynced as unknown as RemoteHttpRequestHandler; newUnsyncedThis.implementation.respond(declaration); return newUnsyncedThis; } times(minNumberOfRequests: number, maxNumberOfRequests?: number): this { this.implementation.times(minNumberOfRequests, maxNumberOfRequests); return this; } async checkTimes() { return new Promise((resolve, reject) => { try { this.implementation.checkTimes(); resolve(); } catch (error) { reject(error); } }); } clear(): this { this.implementation.clear(); return this.unsynced; } get requests(): readonly InterceptedHttpInterceptorRequest, StatusCode>[] { return this.implementation.requests; } async matchesRequest(request: HttpInterceptorRequest>) { const requestMatch = await this.implementation.matchesRequest(request); if (requestMatch.success) { this.implementation.markRequestAsMatched(request); } else if (requestMatch.cause === 'unmatchedRestrictions') { this.implementation.markRequestAsUnmatched(request, { diff: requestMatch.diff }); } else { this.implementation.markRequestAsMatched(request); } return requestMatch; } async applyResponseDeclaration(request: HttpInterceptorRequest>) { return this.implementation.applyResponseDeclaration(request); } saveInterceptedRequest( request: HttpInterceptorRequest>, response: HttpInterceptorResponse, StatusCode>, ) { this.implementation.saveInterceptedRequest(request, response); } registerSyncPromise(promise: Promise) { this.syncPromises.push(promise); } get isSynced() { return this.syncPromises.length === 0; } then< FulfilledResult = PublicSyncedRemoteHttpRequestHandler, RejectedResult = never, >( onFulfilled?: | (( handler: PublicSyncedRemoteHttpRequestHandler, ) => PossiblePromise) | null, onRejected?: ((reason: unknown) => PossiblePromise) | null, ): Promise { const promisesToWait = new Set(this.syncPromises); return Promise.all(promisesToWait) .then(() => { this.syncPromises = this.syncPromises.filter((promise) => !promisesToWait.has(promise)); return this.isSynced ? this.synced : this.unsynced; }) .then(onFulfilled, onRejected); } catch( onRejected?: ((reason: unknown) => PossiblePromise) | null, ): Promise | RejectedResult> { return this.then().catch(onRejected); } finally( onFinally?: (() => void) | null, ): Promise> { return this.then().finally(onFinally); } } export type AnyRemoteHttpRequestHandler = // eslint-disable-next-line @typescript-eslint/no-explicit-any | RemoteHttpRequestHandler // eslint-disable-next-line @typescript-eslint/no-explicit-any | RemoteHttpRequestHandler; export default RemoteHttpRequestHandler;