UNPKG

mutoid

Version:

Reactive library for data fetching, caching, state management

118 lines (117 loc) 5.1 kB
import * as E from 'fp-ts/Either'; import * as O from 'fp-ts/Option'; import * as T from 'fp-ts/Task'; import { pipe } from 'fp-ts/function'; import * as t from 'io-ts'; import { PathReporter } from 'io-ts/PathReporter'; import * as OR from './ObservableResource'; import * as ROR from './ReaderObservableResource'; const getEndpointAppCacheTtl = (endpoint) => endpoint.method === 'GET' && typeof endpoint.appCacheTtl !== 'undefined' && endpoint.appCacheTtl > 0 ? endpoint.appCacheTtl : undefined; export const resourceBadRejected = (d) => ({ type: 'rejected', error: d.error, detail: d.detail, statusCode: d.statusCode ?? 0, errorMessage: d.errorMessage ?? d.error, }); export const resourceBadFail = (d) => ({ type: 'fail', error: d.error, detail: d.detail, statusCode: d.statusCode ?? 0, errorMessage: d.errorMessage ?? d.error, }); // ------------------------------------------------------------------------------------- // Factory // ------------------------------------------------------------------------------------- const errorMessagePrefix = '[fetchFactory]'; export const fetchFactory = (env) => (request, decoderL, successCodes) => { return pipe((deps) => runRequest(deps, request, decoderL), ROR_filterResponse(successCodes, request), logFail(env.loggerFail)); }; export const fetchCacheableFactory = (env) => (request, decoderL, successCodes) => { const appCacheTtl = getEndpointAppCacheTtl(request); if (typeof appCacheTtl === 'undefined') { return fetchFactory(env)(request, decoderL, successCodes); } return pipe(env.createCacheKey(request), ROR.fromReader, ROR.chainW(cacheKey => (deps) => { return pipe(deps.cachePool._tag === 'sync' ? T.fromIO(deps.cachePool.findItem(cacheKey)) : deps.cachePool.findItem(cacheKey), OR.fromTask, OR.chain(item => { return pipe(item, O.bindTo('item'), O.bind('decoder', x => O.fromNullable(decoderL()[x.item.status])), O.bind('payload', x => pipe(x.decoder(x.item.payload), // when decode fail don't delete item, but run new request and overwrite it E.fold(() => O.none, O.some))), O.map(x => pipe(x.payload, (payload) => ({ status: x.item.status, payload, }), OR.done, OR_filterResponse(successCodes, request))), O.getOrElse(() => pipe(runRequest(deps, request, decoderL), OR.chainFirstW(r => pipe(deps.cachePool._tag === 'sync' ? T.fromIO(deps.cachePool.addItem(cacheKey, r, appCacheTtl)) : deps.cachePool.addItem(cacheKey, r, appCacheTtl), OR.doneTask)), OR_filterResponse(successCodes, request)))); })); }), logFail(env.loggerFail)); }; const runRequest = (deps, request, decoder) => pipe(OR.fromAjax(deps.ajax(request), decoder()), OR.mapLeft((e) => { switch (e.type) { case 'networkError': case 'unexpectedResponse': return { type: 'fail', statusCode: e.detail.status, error: e.type, errorMessage: errorMessage(request, e.type), detail: e.detail, }; case 'unknownError': return { type: 'fail', statusCode: 0, error: e.type, errorMessage: errorMessage(request, e.type), detail: e.detail, }; case 'decodeError': return { type: 'fail', error: e.type, errorMessage: errorMessage(request, e.type), statusCode: e.statusCode, detail: PathReporter.report(t.failures(e.detail)).join('\n>> '), }; } })); const filterPredicate = (successCodes) => (r) => successCodes.includes(r.status); const filterOnFalse = (request) => (r) => { if (r.status === 404) { return { type: 'rejected', error: 'notFound', errorMessage: errorMessage(request, 'notFound'), statusCode: r.status, detail: r.payload, }; } if (r.status >= 400 && r.status <= 499) { return { type: 'rejected', error: 'clientError', errorMessage: errorMessage(request, 'clientError'), statusCode: r.status, detail: r.payload, }; } return { type: 'fail', error: 'fail', errorMessage: errorMessage(request, 'fail'), statusCode: r.status, detail: r.payload, }; }; const ROR_filterResponse = (successCodes, request) => ROR.filterOrElseW(filterPredicate(successCodes), filterOnFalse(request)); const OR_filterResponse = (successCodes, request) => OR.filterOrElseW(filterPredicate(successCodes), filterOnFalse(request)); export const errorMessage = (endpoint, error) => `${errorMessagePrefix} ${error} ${endpoint.url}`; export const logFail = (loggerFail) => (ma) => { return pipe(ma, ROR.orElseW((e) => { return pipe(loggerFail(e), ROR.fromReader, ROR.map(() => e), ROR.swap); })); };