UNPKG

react-native-mosquito-transport

Version:

React native javascript sdk for mosquito-transport (https://github.com/brainbehindx/mosquito-transport)

232 lines (200 loc) 8.95 kB
import { Buffer } from "buffer"; import { deserializeE2E, niceHash, normalizeRoute, serializeE2E } from "../../helpers/peripherals"; import { awaitReachableServer, awaitStore } from "../../helpers/utils"; import { RETRIEVAL } from "../../helpers/values"; import { Scoped } from "../../helpers/variables"; import { ensureActiveToken, parseToken } from "../auth/accessor"; import { simplifyCaughtError } from "simplify-error"; import { guardObject, Validator } from "guard-object"; import { serialize } from "entity-serializer"; import { getFetchResources, insertFetchResources } from "./accessor"; import { basicClone } from "../../helpers/basic_clone"; const buildFetchData = (data, extras) => { const { ok, type, status, statusText, redirected, url, headers, size, buffer } = data; const response = new Response(Buffer.from(buffer), { headers: new Headers(headers), status, statusText, url, size }); Object.entries({ ok, type, url, redirected, size, ...extras }) .forEach(([k, v]) => { if (response[k] !== v) Object.defineProperty(response, k, { value: v, writable: false }); }); return response; } export const mfetch = async (input = '', init, config) => { const { projectUrl, serverE2E_PublicKey, method, maxRetries = 1, disableCache = false, uglify, extraHeaders } = config; const { headers, body } = init || {}; if (method !== undefined) guardObject({ enableMinimizer: t => t === undefined || Validator.BOOLEAN(t), rawApproach: t => t === undefined || Validator.BOOLEAN(t), disableAuth: t => t === undefined || Validator.BOOLEAN(t), retrieval: t => t === undefined || Object.values(RETRIEVAL).includes(t), enforce200: t => t === undefined || Validator.BOOLEAN(t) }).validate(method); const { retrieval = RETRIEVAL.DEFAULT, enableMinimizer, rawApproach, enforce200 } = method || {}; const isLink = Validator.LINK(input); const isBaseUrl = isLink || rawApproach; const disableAuth = method?.disableAuth === undefined ? isBaseUrl : method?.disableAuth; const hasBody = body !== undefined; const shouldCache = (retrieval !== RETRIEVAL.DEFAULT || (config.disableCache === undefined ? !hasBody : !disableCache)) && ![RETRIEVAL.NO_CACHE_NO_AWAIT, RETRIEVAL.NO_CACHE_AWAIT].includes(retrieval); const uglified = !!(!isBaseUrl && uglify); const rawHeader = Object.fromEntries( [...new Headers(headers).entries()] ); ['mtoken', 'uglified'].forEach(e => { if ([e] in rawHeader) throw `"${e}" in header is a reserved prop`; }); if (body !== undefined) { if ( typeof body !== 'string' && !Buffer.isBuffer(body) && !Validator.JSON(body) ) throw `"body" must be any of string, buffer, object`; } await awaitStore(); const thisToken = disableAuth ? '' : (Scoped.AuthJWTToken[projectUrl] || ''); const reqId = await niceHash( serialize([ rawHeader, body, // !!disableAuth, input, thisToken && await parseToken(thisToken).uid ]).toString('base64') ); const processReqId = `${reqId}_${thisToken}_${disableCache}_${retrieval}`; let retries = 0, hasFinalize; const callFetch = () => new Promise(async (resolve, reject) => { const retryProcess = ++retries; const finalize = (a, b, extras) => { if (a) resolve(buildFetchData(a, extras)); else reject(basicClone(b)); if (hasFinalize || retryProcess !== 1) return; hasFinalize = true; if (enableMinimizer) { const resolutionList = (Scoped.PendingFetchCollective[processReqId] || []).slice(0); if (Scoped.PendingFetchCollective[processReqId]) delete Scoped.PendingFetchCollective[processReqId]; resolutionList.forEach(e => { e(a && buildFetchData(a, extras), b && basicClone(b)); }); } }; const resolveCache = (reqData) => { finalize(reqData, undefined, { fromCache: true }); }; try { if (retryProcess === 1) { if (enableMinimizer) { if (Scoped.PendingFetchCollective[processReqId]) { Scoped.PendingFetchCollective[processReqId].push((a, b) => { if (a) resolve(a); else reject(b); }); return; } Scoped.PendingFetchCollective[processReqId] = []; } let reqData; if ( retrieval.startsWith('sticky') && (reqData = await getFetchResources(projectUrl, reqId)) ) { resolveCache(reqData); if (retrieval !== RETRIEVAL.STICKY_RELOAD) return; } } if (!disableAuth) await ensureActiveToken(projectUrl); const mtoken = disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl]; const initType = rawHeader['content-type']; const encodeBody = initType === undefined && hasBody && !isBaseUrl; const [reqBuilder, [privateKey]] = uglified ? await serializeE2E(body, mtoken, serverE2E_PublicKey) : [null, []]; const f = await fetch(isLink ? input : `${projectUrl}/${normalizeRoute(input)}`, { ...(hasBody || uglified) ? { method: 'POST' } : {}, credentials: 'omit', ...init, ...uglified ? { body: reqBuilder } : encodeBody ? { body: serialize(body) } : {}, headers: { ...extraHeaders, ...rawHeader, ...uglified ? { uglified, 'content-type': 'request/buffer', ...initType ? { 'init-content-type': initType } : {} } : encodeBody ? { 'content-type': 'request/buffer', 'entity-encoded': '1' } : {}, ...(!mtoken || uglified) ? {} : { mtoken } } }); const { ok, type, status, statusText, redirected, url, headers, size } = f; const simple = headers.get('simple_error'); if (enforce200 && status !== 200) throw `expected response status to be 200 but got ${status}`; if (!isLink && simple) throw { simpleError: JSON.parse(simple) }; const buffer = uglified ? Buffer.from(await deserializeE2E(await f.arrayBuffer(), serverE2E_PublicKey, privateKey)) : Buffer.from(await f.arrayBuffer()); const resObj = { buffer, type, status, statusText, redirected, url, ok, size, headers: Object.fromEntries( [...headers.entries()] ) }; if (shouldCache) { if (status === 200) insertFetchResources(projectUrl, reqId, resObj); } finalize(resObj); } catch (e) { let thisRecord; const getThisRecord = async () => thisRecord ? thisRecord[0] : (thisRecord = [await getFetchResources(projectUrl, reqId)])[0]; if (e?.simpleError) { finalize(undefined, e.simpleError); } else if ( (retrieval === RETRIEVAL.CACHE_NO_AWAIT && !(await getThisRecord())) || retrieval === RETRIEVAL.STICKY_NO_AWAIT || retrieval === RETRIEVAL.NO_CACHE_NO_AWAIT ) { finalize(undefined, simplifyCaughtError(e).simpleError); } else if ( shouldCache && [ RETRIEVAL.DEFAULT, RETRIEVAL.CACHE_NO_AWAIT, RETRIEVAL.CACHE_AWAIT ].includes(retrieval) && await getThisRecord() ) { resolveCache(await getThisRecord()); } else if (retries > maxRetries) { finalize(undefined, simplifyCaughtError(e).simpleError); } else { awaitReachableServer(projectUrl, true).then(() => { callFetch().then( e => finalize(e), e => finalize(undefined, e) ); }); } } }); return (await callFetch()); };