UNPKG

@opra/client

Version:

Opra Client package

191 lines (190 loc) 6.88 kB
import typeIs from '@browsery/type-is'; import { omit } from '@jsopen/objects'; import { MimeTypes } from '@opra/common'; import { lastValueFrom, Observable, ReplaySubject } from 'rxjs'; import { uid } from 'uid'; import { ClientError } from './client-error.js'; import { kBackend, kContext } from './constants.js'; import { HttpObserveType } from './enums/http-observable-type.enum.js'; import { HttpBackend } from './http-backend.js'; import { HttpInterceptorHandler } from './http-interceptor-handler.js'; import { HttpResponse } from './http-response.js'; import { HttpEventType } from './interfaces/http-event.js'; /** * * @class HttpRequestObservable */ export class HttpRequestObservable extends Observable { constructor(backend, init) { super(subscriber => { const context = this[kContext]; const observe = context.observe; const handleEvent = (event) => { if (observe === HttpObserveType.Events) { subscriber.next(event); return; } if (observe === HttpObserveType.ResponseHeader && event.type === HttpEventType.ResponseHeader) { subscriber.next(event.response); subscriber.complete(); return; } if (event.type === HttpEventType.Response) { const { response } = event; if (observe === HttpObserveType.Response) { subscriber.next(response); subscriber.complete(); return; } const isOpraResponse = typeIs.is(event.response.contentType || '', [ MimeTypes.opra_response_json, ]); if (response.status >= 400 && response.status < 600) { subscriber.error(new ClientError({ message: response.status + ' ' + response.statusText, status: response.status, issues: isOpraResponse ? response.body.errors : undefined, })); subscriber.complete(); return; } subscriber.next(event.response.body); subscriber.complete(); } }; /* Bundled requests never talk to the network themselves — they wait for whatever HttpBundleObservable pushes into this subject */ const source = context._bundleEvents || new HttpInterceptorHandler(backend.interceptors || [], this[kBackend]).handle(context); source.subscribe({ next: handleEvent, error(error) { subscriber.error(error); }, complete() { subscriber.complete(); }, }); }); Object.defineProperty(this, kBackend, { enumerable: false, value: backend, }); Object.defineProperty(this, kContext, { enumerable: false, value: { ...init, requestId: uid(12), observe: HttpObserveType.Body, headers: new Headers(init?.headers), }, }); } get requestId() { return this[kContext].requestId; } /** * Marks this request as part of a bundle: it will no longer issue its own * network call when subscribed — instead, subscriptions wait on the internal * subject that {@link _finalize} pushes into. Idempotent. * @internal */ _bindBundle() { this[kContext]._bundleEvents ??= new ReplaySubject(); } /** * Resolves this request with a response parsed out-of-band, e.g. a matching * part of a bundled multipart response — pushed to any subscriber that's * already waiting as well as replayed to any future one, exactly as if the * response had come back over the network. * @internal */ _finalize(response) { const subject = this[kContext]._bundleEvents; if (!subject) return; const request = this[kContext]; subject.next({ type: HttpEventType.ResponseHeader, request, response, }); subject.next({ type: HttpEventType.Response, request, response, }); subject.complete(); } /** * Rejects this request, e.g. when the bundle it belongs to failed before * (or without) producing a matching response part. * @internal */ _finalizeError(error) { this[kContext]._bundleEvents?.error(error); } options(options) { Object.assign(this[kContext], omit(options, [ 'requestId', 'method', 'observe', 'url', 'headers', ])); return this; } header(arg0, value) { const target = this[kContext].headers; if (typeof arg0 === 'object') { const h = arg0 instanceof Headers ? arg0 : new Headers(arg0); h.forEach((v, k) => { if (k.toLowerCase() === 'set-cookie') { target.append(k, v); } else target.set(k, v); }); return this; } if (value == null || value === '') target.delete(arg0); else target.append(arg0, String(value)); return this; } param(arg0, value) { if (value && typeof value === 'object') { value = JSON.stringify(value); } const target = this[kContext].url.searchParams; if (typeof arg0 === 'object') { if (typeof arg0.forEach === 'function') { arg0.forEach((v, k) => target.set(String(k), String(v))); } else { Object.entries(arg0).forEach(entry => target.set(String(entry[0]), String(entry[1]))); } return this; } if (value == null) target.delete(arg0); else target.set(arg0, String(value)); return this; } observe(observe) { if (observe === this[kContext].observe) return this; const cloned = new HttpRequestObservable(this[kBackend], this[kContext]); cloned[kContext].observe = observe || HttpObserveType.Body; return cloned; } getBody() { return lastValueFrom(this.observe(HttpObserveType.Body)); } getResponse() { return lastValueFrom(this.observe(HttpObserveType.Response)); } }