UNPKG

kinto

Version:

An Offline-First JavaScript client for Kinto.

216 lines (215 loc) 7.46 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../utils"); const errors_1 = require("./errors"); /** * Enhanced HTTP client for the Kinto protocol. * @private */ class HTTP { /** * Default HTTP request headers applied to each outgoing request. * * @type {Object} */ static get DEFAULT_REQUEST_HEADERS() { return { Accept: "application/json", "Content-Type": "application/json", }; } /** * Default options. * * @type {Object} */ static get defaultOptions() { return { timeout: null, requestMode: "cors" }; } /** * Constructor. * * @param {EventEmitter} events The event handler. * @param {Object} [options={}} The options object. * @param {Number} [options.timeout=null] The request timeout in ms, if any (default: `null`). * @param {String} [options.requestMode="cors"] The HTTP request mode (default: `"cors"`). */ constructor(events, options = {}) { // public properties /** * The event emitter instance. * @type {EventEmitter} */ this.events = events; /** * The request mode. * @see https://fetch.spec.whatwg.org/#requestmode * @type {String} */ this.requestMode = options.requestMode || HTTP.defaultOptions.requestMode; /** * The request timeout. * @type {Number} */ this.timeout = options.timeout || HTTP.defaultOptions.timeout; /** * The fetch() function. * @type {Function} */ this.fetchFunc = options.fetchFunc || globalThis.fetch.bind(globalThis); } /** * @private */ timedFetch(url, options) { let hasTimedout = false; return new Promise((resolve, reject) => { // Detect if a request has timed out. let _timeoutId; if (this.timeout) { _timeoutId = setTimeout(() => { hasTimedout = true; if (options && options.headers) { options = { ...options, headers: (0, utils_1.obscureAuthorizationHeader)(options.headers), }; } reject(new errors_1.NetworkTimeoutError(url, options)); }, this.timeout); } function proceedWithHandler(fn) { return (arg) => { if (!hasTimedout) { if (_timeoutId) { clearTimeout(_timeoutId); } fn(arg); } }; } this.fetchFunc(url, options) .then(proceedWithHandler(resolve)) .catch(proceedWithHandler(reject)); }); } /** * @private */ async processResponse(response) { const { status, headers } = response; const text = await response.text(); // Check if we have a body; if so parse it as JSON. let json; if (text.length !== 0) { try { json = JSON.parse(text); } catch (err) { throw new errors_1.UnparseableResponseError(response, text, err); } } if (status >= 400) { throw new errors_1.ServerResponse(response, json); } return { status, json: json, headers }; } /** * @private */ async retry(url, retryAfter, request, options) { await (0, utils_1.delay)(retryAfter); return this.request(url, request, { ...options, retry: options.retry - 1, }); } /** * Performs an HTTP request to the Kinto server. * * Resolves with an objet containing the following HTTP response properties: * - `{Number} status` The HTTP status code. * - `{Object} json` The JSON response body. * - `{Headers} headers` The response headers object; see the ES6 fetch() spec. * * @param {String} url The URL. * @param {Object} [request={}] The request object, passed to * fetch() as its options object. * @param {Object} [request.headers] The request headers object (default: {}) * @param {Object} [options={}] Options for making the * request * @param {Number} [options.retry] Number of retries (default: 0) * @return {Promise} */ async request(url, request = { headers: {} }, options = { retry: 0 }) { // Ensure default request headers are always set request.headers = { ...HTTP.DEFAULT_REQUEST_HEADERS, ...request.headers }; // If a multipart body is provided, remove any custom Content-Type header as // the fetch() implementation will add the correct one for us. if (request.body && request.body instanceof FormData) { if (request.headers instanceof Headers) { request.headers.delete("Content-Type"); } else if (!Array.isArray(request.headers)) { delete request.headers["Content-Type"]; } } request.mode = this.requestMode; const response = await this.timedFetch(url, request); const { headers } = response; this._checkForDeprecationHeader(headers); this._checkForBackoffHeader(headers); // Check if the server summons the client to retry after a while. const retryAfter = this._checkForRetryAfterHeader(headers); // If number of allowed of retries is not exhausted, retry the same request. if (retryAfter && options.retry > 0) { return this.retry(url, retryAfter, request, options); } return this.processResponse(response); } _checkForDeprecationHeader(headers) { const alertHeader = headers.get("Alert"); if (!alertHeader) { return; } let alert; try { alert = JSON.parse(alertHeader); } catch (err) { console.warn("Unable to parse Alert header message", alertHeader); return; } console.warn(alert.message, alert.url); if (this.events) { this.events.emit("deprecated", alert); } } _checkForBackoffHeader(headers) { let backoffMs; const backoffHeader = headers.get("Backoff"); const backoffSeconds = backoffHeader ? parseInt(backoffHeader, 10) : 0; if (backoffSeconds > 0) { backoffMs = new Date().getTime() + backoffSeconds * 1000; } else { backoffMs = 0; } if (this.events) { this.events.emit("backoff", backoffMs); } } _checkForRetryAfterHeader(headers) { const retryAfter = headers.get("Retry-After"); if (!retryAfter) { return null; } const delay = parseInt(retryAfter, 10) * 1000; const tryAgainAfter = new Date().getTime() + delay; if (this.events) { this.events.emit("retry-after", tryAgainAfter); } return delay; } } exports.default = HTTP;