UNPKG

@nymphjs/client

Version:

Nymph.js - Client

365 lines 13.1 kB
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