UNPKG

http-svc

Version:

A HTTP request service for browser and node.js

805 lines (804 loc) 24.1 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; import { HttpSvcMiddleware } from "@http-svc/middleware"; const getBuiltInMiddlewareName = (name) => `BUILT_IN_${name}`; const createError = (ctx, message) => { var _a; const error = new Error(message || `Request Error: ${((_a = ctx.response) == null ? void 0 : _a.status) || "unknow status"}`); error.config = ctx.request; if (ctx.response) { error.response = ctx.response; if (!ctx.response.ok) { error.code = ctx.response.status; } } return error; }; const isMiddleware = (middleware) => { if (middleware.name && middleware.handler) return true; return false; }; const isNode = typeof window === "undefined"; const isArray = (val) => toString.call(val) === "[object Array]"; const isObject = (val) => val !== null && typeof val === "object"; const isRecordObj = (obj) => Object.prototype.toString.call(obj) === "[object Object]"; const isFormData = (val) => typeof FormData !== "undefined" && val instanceof FormData; const isDate = (val) => toString.call(val) === "[object Date]"; const encodeParams = (val) => { return encodeURIComponent(val).replace(/%3A/gi, ":").replace(/%24/g, "$").replace(/%2C/gi, ",").replace(/%20/g, "+").replace(/%5B/gi, "[").replace(/%5D/gi, "]"); }; const buildURL = (url, params) => { const parts = []; Object.keys(params).forEach((key) => { let val = params[key]; if (val === null || typeof val === "undefined") { return; } if (isArray(val)) { key = key + "[]"; } else { val = [val]; } val.forEach((v) => { if (isDate(v)) { v = v.toString(); } else if (isObject(v)) { v = JSON.stringify(v); } parts.push(encodeParams(key) + "=" + encodeParams(v)); }); }); const serializedParams = parts.join("&"); if (serializedParams) { const hashMarkIndex = url.indexOf("#"); if (hashMarkIndex !== -1) { url = url.slice(0, hashMarkIndex); } url += (url.indexOf("?") === -1 ? "?" : "&") + serializedParams; } return url; }; const parseUrl = (url) => { const [u, s] = url.split("?"); if (s) { const searchParams = s.split("&"); if (searchParams.length) { const params = {}; for (const searchParam of searchParams) { const [key, value] = searchParam.split("="); if (key && typeof value !== "undefined") { params[key] = value; } } return { url: u, params }; } } return { url }; }; const CONTENT_TYPE_KEY = "Content-Type"; const getContentType = (headers) => (headers == null ? void 0 : headers[CONTENT_TYPE_KEY]) || (headers == null ? void 0 : headers["content-type"]); const setContentType = (headers, contentType) => { if (!contentType) return; headers[CONTENT_TYPE_KEY] = contentType; }; const getCookie = (key, options) => { if (!options) options = {}; const isNoDecode = options.decode === false; const template = options.template || document.cookie; const decodedCookie = isNoDecode ? template : decodeURIComponent(template); const cookies = decodedCookie.split(";"); for (let i = 0; i < cookies.length; i++) { let cookie = cookies[i]; while (cookie.charAt(0) === " ") { cookie = cookie.substring(1); } const keyVal = cookie.split("="); const name = keyVal[0]; const value = keyVal[1]; if (key === name) return value; } return ""; }; const setObjectValue = (obj, keyOrKeys, value, spread) => { if (Array.isArray(keyOrKeys)) { const keys = [...keyOrKeys]; while (keys.length) { if (keys.length === 1) { setObjectValue(obj, keys[0], value); break; } const key = keys.shift(); if (!isRecordObj(obj)) { if (typeof obj[key] === "undefined" && spread) { obj[key] = {}; } else { console.warn(`The value of "${key}" is not a record object!`); break; } } obj = obj[key]; } } else { const key = keyOrKeys; obj[key] = value; } }; const initCtx = async function(ctx, next) { if (!ctx.request) return next(); const { baseURL, headers, params, data, credentials = "include" } = ctx.config; let url = ctx.config.url; const { url: originUrl, params: originParams } = parseUrl(url); if (originParams) { url = ctx.request.url = originUrl; ctx.request.params = { ...originParams, ...params || {} }; } else { ctx.request.url = url; ctx.request.params = { ...params || {} }; } if (baseURL && /^(https?:)?\/\//.test(url) === false) { ctx.request.url = `${baseURL}${url}`; } ctx.request.headers = { ...headers || {} }; if (data) { if (typeof data === "object") { if (typeof FormData !== "undefined" && data instanceof FormData) { const form = new FormData(); const entries = Array.from(data.entries()); entries.forEach(([key, value]) => { form.append(key, value); }); ctx.request.data = form; } else if (Object.keys(data)) { ctx.request.data = JSON.parse(JSON.stringify(data)); } } else { ctx.request.data = data; } } ctx.request.credentials = credentials; return await next(); }; class HttpSvcInitCtx extends HttpSvcMiddleware { constructor() { super(...arguments); __publicField(this, "handler", initCtx); __publicField(this, "name", getBuiltInMiddlewareName("INIT_CTX")); } } __publicField(HttpSvcInitCtx, "handler", initCtx); const timeout = async function(ctx, next) { let ms = ctx.config.timeout; if (typeof ctx.config.timeout !== "number") { ms = isNode ? 350 : 1e4; } if (!ctx.abortController && ms) { const abortController = new AbortController(); ctx.abortController = abortController; ctx.timeoutId = setTimeout(() => { abortController.abort(); }, ms); await next(); clearTimeout(ctx.timeoutId); } else { await next(); } }; class HttpSvcTimeout extends HttpSvcMiddleware { constructor() { super(timeout); __publicField(this, "name", getBuiltInMiddlewareName("TIMEOUT")); } } __publicField(HttpSvcTimeout, "handler", timeout); const xsrf = async function(ctx, next, config) { var _a, _b; if (!ctx.request) return next(); const payload = (config == null ? void 0 : config.payload) || { token: "xsrf_token" }; const { token, params, data, headers } = payload; let xsrfValue; if (params || data || headers) { try { if (!isNode) { xsrfValue = getCookie(token, { decode: false }); } else { xsrfValue = getCookie(token, { template: ((_a = ctx.request.headers) == null ? void 0 : _a["cookie"]) || ((_b = ctx.request.headers) == null ? void 0 : _b["Cookie"]) || "_", decode: false }); } } catch (error) { if (!isNode) { console.warn(error); } } } if (xsrfValue) { if (params) { setObjectValue(ctx.request, ["params", params], xsrfValue, true); } if (data) { if (typeof ctx.request.data === "undefined") { setObjectValue(ctx.request, ["data", data], xsrfValue, true); } else if (isObject(ctx.request.data)) { if (isRecordObj(ctx.request.data)) { setObjectValue(ctx.request, ["data", data], xsrfValue, true); } else if (isFormData(ctx.request.data)) { ctx.request.data.append(data, xsrfValue); } } } if (headers) { setObjectValue(ctx.request, ["headers", headers], xsrfValue, true); } } return await next(); }; class HttpSvcXsrf extends HttpSvcMiddleware { constructor(payload) { super({ handler: xsrf, payload }); __publicField(this, "name", getBuiltInMiddlewareName("XSRF")); } } __publicField(HttpSvcXsrf, "handler", xsrf); const isEqualContentType = (target, current) => { if (!current) return false; return current.indexOf(target) > -1; }; const jsonBody = (ctx, data, contentType) => { if (!ctx.request) return; ctx.request.data = JSON.stringify(data); if (!ctx.request.headers) { const headers = {}; setContentType(headers, "application/json"); ctx.request.headers = headers; } else if (!contentType) { setContentType(ctx.request.headers, "application/json"); } }; const body = async function(ctx, next, config) { var _a; if (!ctx.request) return next(); const { params, headers } = ctx.request; ctx.request.url = buildURL(ctx.request.url, params || {}); if (ctx.request.data) { const isContinue = Object.prototype.toString.call(ctx.request.data) !== "[object Object]"; if (!isContinue) { const contentType = getContentType(headers); const data = ctx.request.data; if ((_a = config == null ? void 0 : config.payload) == null ? void 0 : _a.stringify) { jsonBody(ctx, data, contentType); return await next(); } if (isEqualContentType("application/x-www-form-urlencoded", contentType)) { ctx.request.data = buildURL("", data).slice(1); } if (isEqualContentType("application/json", contentType || "application/json")) { jsonBody(ctx, data, contentType); } if (isEqualContentType("multipart/form-data", contentType)) { const keys = Object.keys(data); if (keys.length) { ctx.request.data = keys.reduce((form, key) => { form.append(key, data[key]); return form; }, new FormData()); } } } } return await next(); }; class HttpSvcBody extends HttpSvcMiddleware { constructor() { super(body); __publicField(this, "name", getBuiltInMiddlewareName("BODY")); } } __publicField(HttpSvcBody, "handler", body); const fetch = async (ctx, next) => { var _a; if (isNode) { throw new Error("When you are in a Node environment, please use server-side fetch middleware."); } if (!window.fetch) { throw new Error("When you are in a non-modern browser, please manually polyfill fetch."); } if (!ctx.request) return await next(); if (ctx.response) return await next(); const { url, method, headers, credentials } = ctx.request; const body2 = ctx.request.data; ctx.response = await window.fetch(url, { method, body: body2, headers, credentials, signal: (_a = ctx.abortController) == null ? void 0 : _a.signal }); return await next(); }; class HttpSvcFetch extends HttpSvcMiddleware { constructor() { super(...arguments); __publicField(this, "handler", fetch); __publicField(this, "name", getBuiltInMiddlewareName("FETCH")); } } __publicField(HttpSvcFetch, "handler", fetch); const resData = async function(ctx, next, config) { var _a, _b; await next(); if ((_a = ctx.response) == null ? void 0 : _a.ok) { if (typeof ctx.response.data === "undefined") { try { const responseType = ((_b = config == null ? void 0 : config.payload) == null ? void 0 : _b.type) || "json"; ctx.response.data = await ctx.response[responseType](); } catch (error) { console.warn(error); } } } }; class HttpSvcResData extends HttpSvcMiddleware { constructor() { super(resData); __publicField(this, "name", getBuiltInMiddlewareName("RES_DATA")); } } __publicField(HttpSvcResData, "handler", resData); const spray = async (ctx, next) => { await next(); }; class HttpSvcSprayMiddleware extends HttpSvcMiddleware { constructor(handler, payload) { super(); __publicField(this, "name", getBuiltInMiddlewareName("SPRAY")); this.handler = async (ctx, next) => { return await handler(ctx, next, { payload }); }; } } __publicField(HttpSvcSprayMiddleware, "handler", spray); const retry = async (ctx, next, config) => { var _a; const requestFn = (_a = ctx.request) == null ? void 0 : _a.function; const { times, condition, onRetry } = (config == null ? void 0 : config.payload) || { times: 0 }; const isDone = () => { return !times || ctx.retry === times; }; const doRetry = async (extra) => { var _a2; if (!requestFn) { throw createError(ctx, "Missing request function"); } if (isDone() && !((_a2 = config == null ? void 0 : config.payload) == null ? void 0 : _a2.runtimeShortCircuit)) { throw createError(ctx); } ctx.retry = ctx.retry ? ctx.retry + 1 : 1; try { if (onRetry) { const newConfig = await onRetry(ctx.config, extra); if (newConfig) { ctx.config = newConfig; } } if (ctx.response) delete ctx.response; await requestFn(ctx); } catch (error) { throw createError(ctx, error.message); } }; try { await next(); } catch (error) { if (isDone()) { throw createError(ctx, error == null ? void 0 : error.message); } await doRetry(); return; } if (condition) { const isMeetTheCondition = await condition(ctx.response); if (isMeetTheCondition) { await doRetry(isMeetTheCondition); return; } } if (!ctx.response || !ctx.response.ok) { await doRetry(); } }; class HttpSvcRetry extends HttpSvcMiddleware { constructor() { super(...arguments); __publicField(this, "name", getBuiltInMiddlewareName("RETRY")); __publicField(this, "handler", retry); } } __publicField(HttpSvcRetry, "handler", retry); const getKeyByKeyDict = (url, dict) => { return Object.keys(dict).sort().reduce((k, c) => { k += `_${c}:${dict[c]}`; return k; }, `api:${url}`); }; const cache = async (ctx, next, config) => { var _a; if (!ctx.request || ctx.request.method !== "GET") return await next(); if (!(config == null ? void 0 : config.payload)) return await next(); const { store, extra } = config.payload; let key = config.payload.key; if (!key || !store) return await next(); if (typeof key !== "string") key = getKeyByKeyDict(ctx.request.url, key); const data = store.get(key, extra); if (data) { ctx.response = new Response(JSON.stringify(data), { status: 200, statusText: `Cache:${key}` }); await next(); } else { await next(); if ((_a = ctx.response) == null ? void 0 : _a.data) { store.set(key, ctx.response.data, extra); } } }; class HttpSvcCache extends HttpSvcMiddleware { constructor(store) { super({ handler: cache, payload: { store } }); __publicField(this, "name", getBuiltInMiddlewareName("CACHE")); } } __publicField(HttpSvcCache, "handler", cache); class HttpSvcControl { constructor(httpSvc) { this.httpSvc = httpSvc; } } class ConfigControl extends HttpSvcControl { provide(ctx = {}) { const provider = { inject: (key, value) => { this.inject(ctx, key, value); return provider; }, disable: (key) => { this.disable(ctx, key); return provider; }, get: () => { return ctx; } }; return provider; } inject(ctx, name, config) { ctx[name] = config; } disable(ctx, name) { ctx[name] = { ...ctx[name] || {}, disabled: true }; } get(ctx, name) { return ctx[name] || {}; } } const BUILT_IN_MIDDLEWARE = { INIT_CTX: getBuiltInMiddlewareName("INIT_CTX"), TIMEOUT: getBuiltInMiddlewareName("TIMEOUT"), BODY: getBuiltInMiddlewareName("BODY"), XSRF: getBuiltInMiddlewareName("XSRF"), FETCH: getBuiltInMiddlewareName("FETCH"), RETRY: getBuiltInMiddlewareName("RETRY"), SPRAY: getBuiltInMiddlewareName("SPRAY"), RES_DATA: getBuiltInMiddlewareName("RES_DATA"), RES_EXTRACT: getBuiltInMiddlewareName("RES_EXTRACT"), LOG: getBuiltInMiddlewareName("LOG"), CACHE: getBuiltInMiddlewareName("CACHE") }; const ORDER_PRIFIX = "$ORDER_"; class AssembleDispatcher { constructor(control) { __publicField(this, "middlewares", []); __publicField(this, "middlewareConfigCtxProvider"); this.control = control; this.middlewareConfigCtxProvider = this.control.httpSvc.configCtrl.provide(); } with(middleware, payload) { if (!middleware) return this; if (typeof middleware === "string") { this.middlewares.push({ name: `${ORDER_PRIFIX}${middleware}`, handler: HttpSvcSprayMiddleware.handler }); if (payload) { this.inject(middleware, payload); } } else if (typeof middleware === "function") { this.middlewares.push(new HttpSvcSprayMiddleware(middleware, payload)); } else if (isMiddleware(middleware)) { this.middlewares.push(middleware); if (payload) { this.inject(middleware.name, payload); } } return this; } inject(name, payload) { this.middlewareConfigCtxProvider.inject(name, { payload }); return this; } disable(name) { this.middlewareConfigCtxProvider.disable(name); return this; } request(config) { const fn = this.control.compose(this.middlewares); return this.control.httpSvc.requestCtrl.request(config, this.middlewareConfigCtxProvider.get(), fn); } } class AssembleControl extends HttpSvcControl { constructor() { super(...arguments); __publicField(this, "middlewares", []); } alreadyExistsIdx(middlewareName) { return this.middlewares.findIndex((m) => m.name === middlewareName); } register(middlewares) { if (Array.isArray(middlewares)) { let reset = false; middlewares.forEach((m) => { if (isMiddleware(m)) { reset = true; const idx = this.alreadyExistsIdx(m.name); if (idx > -1) { this.middlewares[idx] = m; } else { this.middlewares.push(m); } } }); if (reset) { this.httpSvc.requestCtrl.reset(); } } } setup() { return new AssembleDispatcher(this); } disable(middlewareName) { return this.setup().disable(middlewareName); } with(middleware, payload) { return this.setup().with(middleware, payload); } compose(middlewares) { const list1 = [...this.middlewares]; const list2 = []; const builtInOverride = { [BUILT_IN_MIDDLEWARE.RETRY]: null, [BUILT_IN_MIDDLEWARE.BODY]: null, [BUILT_IN_MIDDLEWARE.RES_DATA]: null, [BUILT_IN_MIDDLEWARE.TIMEOUT]: null }; const builtInNames = Object.keys(builtInOverride); const unique = {}; (middlewares || []).forEach((m) => { if (isMiddleware(m)) { let middlewareName = m.name; if (builtInNames.includes(middlewareName)) { if (!builtInOverride[middlewareName]) { builtInOverride[middlewareName] = m; } } else { if (middlewareName.startsWith(ORDER_PRIFIX)) { middlewareName = middlewareName.substring(ORDER_PRIFIX.length); if (!unique[middlewareName] && !(middlewareName in builtInOverride)) { const globalMiddlewareIndex = list1.findIndex((m2) => m2.name === middlewareName); if (globalMiddlewareIndex > -1) { const realM = list1[globalMiddlewareIndex]; list1.splice(globalMiddlewareIndex, 1); unique[middlewareName] = realM; list2.push(realM); } } } else { if (BUILT_IN_MIDDLEWARE.SPRAY === middlewareName) { list2.push(m); } else if (!unique[middlewareName]) { unique[middlewareName] = m; list2.push(m); } } } } }); const list = []; list1.forEach((m) => { if (builtInNames.includes(m.name)) { if (!builtInOverride[m.name]) { builtInOverride[m.name] = m; } } else if (!unique[m.name]) { list.push(m); } }); list.push(...list2); const middlewareList = [ builtInOverride[BUILT_IN_MIDDLEWARE.RETRY] || new HttpSvcRetry(), new HttpSvcInitCtx(), ...list, builtInOverride[BUILT_IN_MIDDLEWARE.RES_DATA] || new HttpSvcResData(), builtInOverride[BUILT_IN_MIDDLEWARE.BODY] || new HttpSvcBody(), builtInOverride[BUILT_IN_MIDDLEWARE.TIMEOUT] || new HttpSvcTimeout(), this.httpSvc.fetch ]; return async (context) => { var _a, _b; let index = -1; const configCtrl = this.httpSvc.configCtrl; const debug = configCtrl.get(context.middleware, "DEBUG").payload; await dispatch(0); if (debug) { return context; } const disableResExtract = configCtrl.get(context.middleware, BUILT_IN_MIDDLEWARE.RES_EXTRACT).disabled; if (!disableResExtract && ((_a = context.response) == null ? void 0 : _a.ok)) { return ((_b = context.response) == null ? void 0 : _b.data) || null; } return context.response; async function dispatch(i) { if (i === middlewareList.length) return; if (i <= index) throw createError(context, "The next() called multiple times"); index = i; const middleware = middlewareList[index]; if (!middleware) { throw createError(context, "Middleware is not exist"); } await middleware.handler(context, dispatch.bind(null, i + 1), configCtrl.get(context.middleware, middleware.name)); } }; } } class RequestControl extends HttpSvcControl { constructor() { super(...arguments); __publicField(this, "fn", null); } async request(config, middlewareCtx = {}, fn) { if (!fn) { if (!this.fn) { this.generateRequestFunction(); } fn = this.fn; } return await fn(this.createContext(config, middlewareCtx, fn)); } generateRequestFunction() { this.fn = this.httpSvc.assembleCtrl.compose(); } reset() { this.fn = null; } get useAsyncRequest() { return (asyncRequest, config) => { const inst = this.httpSvc.create(config); asyncRequest(inst.request.bind(inst)); }; } createContext(config, middleware = {}, requestFn) { const { url, method } = config; const ctx = { config: { baseURL: this.httpSvc.baseURL, ...config }, useAsyncRequest: this.useAsyncRequest, middleware, request: { url, method: (method || "get").toUpperCase(), function: requestFn } }; return ctx; } } class HttpService { constructor(initConfig) { __publicField(this, "fetch"); __publicField(this, "assembleCtrl", new AssembleControl(this)); __publicField(this, "configCtrl", new ConfigControl(this)); __publicField(this, "requestCtrl", new RequestControl(this)); __publicField(this, "baseURL", "//api.domain.com"); if (initConfig) { if (Array.isArray(initConfig)) { this.fetch = new HttpSvcFetch(); this.register(initConfig); } else { const { middlewares, fetch: fetch2, baseURL } = initConfig; this.fetch = fetch2 || new HttpSvcFetch(); this.register(middlewares || []); if (baseURL) this.baseURL = baseURL; } } else { this.fetch = new HttpSvcFetch(); this.register([]); } } register(middlewares) { this.assembleCtrl.register(middlewares); } request(config) { return this.requestCtrl.request(config); } disable(middlewareName) { return this.assembleCtrl.disable(middlewareName); } with(middleware, payload) { return this.assembleCtrl.with(middleware, payload); } setFetch(fetch2) { this.fetch = fetch2; this.requestCtrl.reset(); } create(config) { return new HttpService({ fetch: this.fetch, ...config || {} }); } } export { BUILT_IN_MIDDLEWARE, HttpService, HttpSvcCache, HttpSvcRetry, HttpSvcSprayMiddleware, HttpSvcXsrf }; //# sourceMappingURL=index.mjs.map