mutoid
Version:
Reactive library for data fetching, caching, state management
118 lines (117 loc) • 5.1 kB
JavaScript
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);
}));
};