@opra/client
Version:
Opra Client package
231 lines (230 loc) • 9.8 kB
JavaScript
// /// <reference lib="dom" />
import typeIs from '@browsery/type-is';
import { omit } from '@jsopen/objects';
import { isBlob, isFormData } from '@opra/common';
import { Observable } from 'rxjs';
import { isReadableStreamLike } from 'rxjs/internal/util/isReadableStreamLike';
import { HttpBackend } from './http-backend.js';
import { HttpResponse } from './http-response.js';
import { HttpEventType, } from './interfaces/http-event.js';
/**
*
* @class FetchBackend
*/
export class FetchBackend extends HttpBackend {
interceptors;
defaults;
constructor(serviceUrl, options) {
super(serviceUrl, options);
// Create deduped interceptor array
this.interceptors = Array.from(new Set([...(options?.interceptors || [])]));
this.defaults = {
...options?.defaults,
headers: options?.defaults?.headers instanceof Headers
? options?.defaults?.headers
: new Headers(options?.defaults?.headers),
params: options?.defaults?.params instanceof URLSearchParams
? options?.defaults?.params
: new URLSearchParams(options?.defaults?.params),
};
}
handle(init) {
return new Observable(subscriber => {
(async () => {
let request = this.prepareRequest(init);
if (request.body && init.reportProgress) {
const stream = request.body;
const contentLength = request.headers.get('content-length') || '0';
const total = parseInt(contentLength, 10) || 0;
let loaded = 0;
const progressTrackingStream = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk);
loaded += chunk.byteLength;
// Emit 'UploadProgress' event
subscriber.next({
type: HttpEventType.UploadProgress,
request,
total,
loaded,
});
},
});
request = new Request(request.url, {
cache: request.cache,
credentials: request.credentials,
headers: request.headers,
integrity: request.integrity,
keepalive: request.keepalive,
method: request.method,
mode: request.mode,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
signal: request.signal,
body: stream.pipeThrough(progressTrackingStream),
window: init.window,
...{
// undici library requires
duplex: 'half',
},
});
}
// Send request
const fetchPromise = this.send(request);
// Emit 'Sent' event
subscriber.next({
request,
type: HttpEventType.Sent,
});
const fetchResponse = await fetchPromise;
// Emit 'ResponseHeader' event
const headersResponse = this.createResponse({
url: fetchResponse.url,
headers: fetchResponse.headers,
status: fetchResponse.status,
statusText: fetchResponse.statusText,
hasBody: !!fetchResponse.body,
});
subscriber.next({
request,
type: HttpEventType.ResponseHeader,
response: headersResponse,
});
// Parse body
let body;
if (fetchResponse.body) {
if (init.reportProgress) {
const fetchBody = fetchResponse.body;
const contentLength = fetchResponse.headers.get('content-length') || '0';
const total = parseInt(contentLength, 10) || 0;
let loaded = 0;
const res = new Response(new ReadableStream({
async start(controller) {
const reader = fetchBody.getReader();
for (;;) {
const { done, value } = await reader.read();
if (done)
break;
loaded += value.byteLength;
controller.enqueue(value);
// Emit 'DownloadProgress' event
subscriber.next({
type: HttpEventType.DownloadProgress,
request,
total,
loaded,
});
}
controller.close();
},
}));
body = await this.parseBody(res);
}
else {
body = await this.parseBody(fetchResponse);
}
}
const response = this.createResponse({
url: fetchResponse.url,
headers: fetchResponse.headers,
status: fetchResponse.status,
statusText: fetchResponse.statusText,
body,
});
// Emit 'Response' event
subscriber.next({
request,
type: HttpEventType.Response,
response,
});
subscriber.complete();
})().catch(error => subscriber.error(error));
});
}
send(request) {
return fetch(request);
}
prepareRequest(init) {
const headers = init.headers || new Headers();
const requestInit = {
...init,
...omit(this.defaults, ['headers', 'params']),
headers,
};
this.defaults.headers.forEach((val, key) => {
if (!headers.has(key))
headers.set(key, val);
});
const url = new URL(requestInit.url, this.serviceUrl);
if (this.defaults.params.size) {
this.defaults.params.forEach((val, key) => {
if (!url.searchParams.has(key))
url.searchParams.set(key, val);
});
requestInit.url = url.toString();
}
const body = requestInit.body;
if (body) {
let contentType = '';
if (typeof body === 'string' ||
typeof body === 'number' ||
typeof body === 'boolean') {
contentType = 'text/plain; charset="UTF-8"';
requestInit.body = new Blob([String(body)], { type: contentType });
headers.set('Content-Length', String(requestInit.body.size));
delete requestInit.duplex;
}
else if (isReadableStreamLike(body)) {
contentType = 'application/octet-stream';
requestInit.duplex = 'half'; // undici library requires "duplex" option to be set for streams
}
else if (Buffer.isBuffer(body)) {
contentType = 'application/octet-stream';
headers.set('Content-Length', String(body.length));
delete requestInit.duplex;
}
else if (isBlob(body)) {
contentType = body.type || 'application/octet-stream';
headers.set('Content-Length', String(body.size));
delete requestInit.duplex;
}
else if (isFormData(body)) {
delete requestInit.duplex;
}
else {
contentType = 'application/json;charset="UTF-8"';
requestInit.body = new Blob([JSON.stringify(body)], {
type: contentType,
});
headers.set('Content-Length', String(requestInit.body.size));
delete requestInit.duplex;
}
if (contentType && !headers.has('Content-Type'))
headers.set('Content-Type', contentType);
}
return new Request(url.toString(), requestInit);
}
createResponse(init) {
return new HttpResponse(init);
}
async parseBody(fetchResponse) {
let body;
const contentType = fetchResponse.headers.get('Content-Type') || '';
if (typeIs.is(contentType, ['json', 'application/*+json'])) {
body = await fetchResponse.json();
if (typeof body === 'string')
body = JSON.parse(body);
}
else if (typeIs.is(contentType, ['text']))
body = await fetchResponse.text();
else if (typeIs.is(contentType, ['multipart/form-data']))
body = await fetchResponse.formData();
else {
const buf = await fetchResponse.arrayBuffer();
if (buf.byteLength)
body = buf;
}
return body;
}
}