@opra/client
Version:
Opra Client package
191 lines (190 loc) • 6.88 kB
JavaScript
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));
}
}