@nymphjs/client
Version:
Nymph.js - Client
365 lines • 13.1 kB
JavaScript
import { fetchEventSource, EventStreamContentType, } from 'fetch-event-source-hperrin';
export default class HttpRequester {
fetch;
requestCallbacks = [];
responseCallbacks = [];
iteratorCallbacks = [];
static makeUrl(url, data) {
if (!data) {
return url;
}
for (let [key, value] of Object.entries(data)) {
url =
url +
(url.indexOf('?') !== -1 ? '&' : '?') +
encodeURIComponent(key) +
'=' +
encodeURIComponent(JSON.stringify(value));
}
return url;
}
constructor(ponyFetch) {
this.fetch = ponyFetch ? ponyFetch : (...args) => fetch(...args);
}
on(event, callback) {
const prop = (event + 'Callbacks');
if (!(prop in this)) {
throw new Error('Invalid event type.');
}
// @ts-ignore: The callback should always be the right type here.
this[prop].push(callback);
return () => this.off(event, callback);
}
off(event, callback) {
const prop = (event + 'Callbacks');
if (!(prop in this)) {
return false;
}
// @ts-ignore: The callback should always be the right type here.
const i = this[prop].indexOf(callback);
if (i > -1) {
// @ts-ignore: The callback should always be the right type here.
this[prop].splice(i, 1);
}
return true;
}
async GET(opt) {
return await this._httpRequest('GET', opt);
}
async POST(opt) {
return await this._httpRequest('POST', opt);
}
async POST_ITERATOR(opt) {
return await this._iteratorRequest('POST', opt);
}
async PUT(opt) {
return await this._httpRequest('PUT', opt);
}
async PATCH(opt) {
return await this._httpRequest('PATCH', opt);
}
async DELETE(opt) {
return await this._httpRequest('DELETE', opt);
}
async _httpRequest(method, opt) {
const dataString = JSON.stringify(opt.data);
let url = opt.url;
if (method === 'GET') {
// TODO: what should this size be?
// && dataString.length < 1) {
url = HttpRequester.makeUrl(opt.url, opt.data);
}
const options = {
method,
headers: opt.headers ?? {},
credentials: 'include',
};
if (method !== 'GET' && opt.data) {
options.headers['Content-Type'] =
'application/json';
options.body = dataString;
}
for (let i = 0; i < this.requestCallbacks.length; i++) {
this.requestCallbacks[i] && this.requestCallbacks[i](this, url, options);
}
const response = await this.fetch(url, options);
let text;
try {
text = await response.text();
}
catch (e) {
throw new InvalidResponseError('Server response did not contain valid text body.');
}
if (!response.ok) {
let errObj;
try {
errObj = JSON.parse(text);
}
catch (e) {
if (!(e instanceof SyntaxError)) {
throw e;
}
}
if (typeof errObj !== 'object') {
errObj = {
textStatus: response.statusText,
};
}
errObj.status = response.status;
throw response.status < 200
? new InformationalError(response, errObj)
: response.status < 300
? new SuccessError(response, errObj)
: response.status < 400
? new RedirectError(response, errObj)
: response.status < 500
? new ClientError(response, errObj)
: new ServerError(response, errObj);
}
for (let i = 0; i < this.responseCallbacks.length; i++) {
this.responseCallbacks[i] &&
this.responseCallbacks[i](this, response, text);
}
if (opt.dataType === 'json') {
if (!text.length) {
throw new InvalidResponseError('Server response was empty.');
}
try {
return JSON.parse(text);
}
catch (e) {
if (!(e instanceof SyntaxError)) {
throw e;
}
throw new InvalidResponseError('Server response was invalid: ' + JSON.stringify(text));
}
}
else {
return text;
}
}
async _iteratorRequest(method, opt) {
const dataString = JSON.stringify(opt.data);
let url = opt.url;
if (method === 'GET') {
// TODO: what should this size be?
// && dataString.length < 1) {
url = HttpRequester.makeUrl(opt.url, opt.data);
}
const hasBody = method !== 'GET' && opt.data;
const headers = opt.headers ?? {};
if (hasBody) {
headers['Content-Type'] = 'application/json';
}
for (let i = 0; i < this.iteratorCallbacks.length; i++) {
this.iteratorCallbacks[i] &&
this.iteratorCallbacks[i](this, url, headers);
}
const responses = [];
let nextResponseResolve;
let nextResponseReadyPromise = new Promise((res) => {
nextResponseResolve = res;
});
let responsesDone = false;
let serverResponse;
const ctrl = new AbortController();
fetchEventSource(url, {
openWhenHidden: true,
fetch: this.fetch,
method,
headers,
credentials: 'include',
body: hasBody ? dataString : undefined,
signal: ctrl.signal,
async onopen(response) {
serverResponse = response;
if (response.ok) {
if (response.headers.get('content-type') === EventStreamContentType) {
throw new InvalidResponseError('Server response is not an event stream.');
}
// Response is ok, wait for messages.
return;
}
let text = '';
try {
text = await response.text();
}
catch (e) {
// Ignore error here.
}
let errObj;
try {
errObj = JSON.parse(text);
}
catch (e) {
if (!(e instanceof SyntaxError)) {
throw e;
}
}
if (typeof errObj !== 'object') {
errObj = {
textStatus: response.statusText,
};
}
errObj.status = response.status;
throw response.status < 200
? new InformationalError(response, errObj)
: response.status < 300
? new SuccessError(response, errObj)
: response.status < 400
? new RedirectError(response, errObj)
: response.status < 500
? new ClientError(response, errObj)
: new ServerError(response, errObj);
},
onmessage(event) {
if (event.event === 'next') {
let text = event.data;
if (opt.dataType === 'json') {
if (!text.length) {
responses.push(new InvalidResponseError('Server response was empty.'));
}
else {
try {
responses.push(JSON.parse(text));
}
catch (e) {
if (!(e instanceof SyntaxError)) {
responses.push(e);
}
else {
responses.push(new InvalidResponseError('Server response was invalid: ' + JSON.stringify(text)));
}
}
}
}
else {
responses.push(text);
}
}
else if (event.event === 'error') {
let text = event.data;
let errObj;
try {
errObj = JSON.parse(text);
}
catch (e) {
if (!(e instanceof SyntaxError)) {
throw e;
}
}
if (typeof errObj !== 'object') {
errObj = {
status: 500,
textStatus: 'Iterator Error',
};
}
responses.push(errObj.status < 200
? new InformationalError(serverResponse, errObj)
: errObj.status < 300
? new SuccessError(serverResponse, errObj)
: errObj.status < 400
? new RedirectError(serverResponse, errObj)
: errObj.status < 500
? new ClientError(serverResponse, errObj)
: new ServerError(serverResponse, errObj));
}
else if (event.event === 'finished') {
responsesDone = true;
}
else if (event.event === 'ping') {
// Ignore keep-alive pings.
return;
}
const resolve = nextResponseResolve;
if (!responsesDone) {
nextResponseReadyPromise = new Promise((res) => {
nextResponseResolve = res;
});
}
// Resolve the promise to continue any waiting iterator.
resolve();
},
onclose() {
responses.push(new ConnectionClosedUnexpectedlyError('The connection to the server was closed unexpectedly.'));
responsesDone = true;
nextResponseResolve();
},
onerror(err) {
// Rethrow to stop the operation.
throw err;
},
}).catch((err) => {
responses.push(new ConnectionError('The connection could not be established: ' + err));
responsesDone = true;
nextResponseResolve();
});
const iterator = {
abortController: ctrl,
async *[Symbol.asyncIterator]() {
do {
await nextResponseReadyPromise;
while (responses.length) {
yield responses.shift();
}
} while (!responsesDone);
},
};
return iterator;
}
}
export class InvalidResponseError extends Error {
constructor(message) {
super(message);
this.name = 'InvalidResponseError';
}
}
export class ConnectionClosedUnexpectedlyError extends Error {
constructor(message) {
super(message);
this.name = 'ConnectionClosedUnexpectedlyError';
}
}
export class ConnectionError extends Error {
constructor(message) {
super(message);
this.name = 'ConnectionError';
}
}
export class HttpError extends Error {
status;
statusText;
constructor(name, response, errObj) {
super(errObj.textStatus);
this.name = name;
this.status = response.status;
this.statusText = response.statusText;
Object.assign(this, errObj);
}
}
export class InformationalError extends HttpError {
constructor(response, errObj) {
super('InformationalError', response, errObj);
}
}
export class SuccessError extends HttpError {
constructor(response, errObj) {
super('SuccessError', response, errObj);
}
}
export class RedirectError extends HttpError {
constructor(response, errObj) {
super('RedirectError', response, errObj);
}
}
export class ClientError extends HttpError {
constructor(response, errObj) {
super('ClientError', response, errObj);
}
}
export class ServerError extends HttpError {
constructor(response, errObj) {
super('ServerError', response, errObj);
}
}
//# sourceMappingURL=HttpRequester.js.map