UNPKG

@magnetarjs/core

Version:
200 lines (199 loc) 11.5 kB
import { isBoolean, isPromise } from 'is-what'; import { getEventNameFnsMap } from '../helpers/eventHelpers.js'; import { executeOnFns } from '../helpers/executeOnFns.js'; import { getModifyPayloadFnsMap } from '../helpers/modifyPayload.js'; import { getModifyReadResponseFnsMap } from '../helpers/modifyReadResponse.js'; import { getPluginModuleConfig } from '../helpers/moduleHelpers.js'; import { getCollectionWriteLocks } from '../helpers/pathHelpers.js'; import { isDoOnFetch, isDoOnFetchAggregate, isFetchAggregateResponse, isFetchResponse, } from '../helpers/pluginHelpers.js'; import { throwIfNoFnsToExecute } from '../helpers/throwFns.js'; import { handleAction } from './handleAction.js'; export function handleFetchPerStore(sharedParams, actionName) { const { collectionPath, _docId, moduleConfig, globalConfig, fetchPromises, writeLockMap, docFn, collectionFn, setLastFetched } = sharedParams; // prettier-ignore // returns the action the dev can call with myModule.insert() etc. return function (payload, actionConfig = {}) { // first of all, check if the same fetch call was just made or not, if so return the same fetch promise early const fetchPromiseKey = JSON.stringify(payload); const foundFetchPromise = fetchPromises[actionName]?.get(fetchPromiseKey); // return the same fetch promise early if it's not yet resolved if (actionName === 'fetch' && isPromise(foundFetchPromise)) return foundFetchPromise; // set up and/or reset te writeLock for write actions const writeLockId = _docId ? `${collectionPath}/${_docId}` : collectionPath; const writeLock = writeLockMap.get(writeLockId); // eslint-disable-next-line no-async-promise-executor const actionPromise = new Promise(async (resolve, reject) => { const docId = _docId; const modulePath = [collectionPath, docId].filter(Boolean).join('/'); /** * Are we forcing to check in with the DB or can we be satisfied with only optimistic fetch? */ const force = payload?.force === true; const willForceFetch = force || (docId ? docFn(modulePath, moduleConfig).exists !== true : false); // we need to await any writeLock _before_ fetching, to prevent grabbing outdated data if (actionName === 'fetch' && willForceFetch) { await writeLock?.promise; if (!_docId) { // we need to await all promises of all docs in this collection... const collectionWriteMaps = getCollectionWriteLocks(collectionPath, writeLockMap); await Promise.allSettled(collectionWriteMaps.map((w) => w.promise)); } } try { // get all the config needed to perform this action const onError = actionConfig.onError || moduleConfig.onError || globalConfig.onError; const modifyPayloadFnsMap = getModifyPayloadFnsMap(globalConfig.modifyPayloadOn, moduleConfig.modifyPayloadOn, actionConfig.modifyPayloadOn); const modifyReadResponseMap = getModifyReadResponseFnsMap(globalConfig.modifyReadResponseOn, moduleConfig.modifyReadResponseOn, actionConfig.modifyReadResponseOn); const eventNameFnsMap = getEventNameFnsMap(globalConfig.on, moduleConfig.on, actionConfig.on); const storesToExecute = actionConfig.executionOrder || (actionName === 'fetch' ? (moduleConfig.executionOrder || {})[actionName] : false) || (moduleConfig.executionOrder || {})['read'] || (actionName === 'fetch' ? (globalConfig.executionOrder || {})[actionName] : false) || (globalConfig.executionOrder || {})['read'] || []; throwIfNoFnsToExecute(storesToExecute); // update the payload if (actionName === 'fetch') { for (const modifyFn of modifyPayloadFnsMap[actionName]) { payload = modifyFn(payload, docId); } } let stopExecution = false; /** * The abort mechanism for the entire store chain. When executed in handleAction() it won't go to the next store in executionOrder. */ function stopExecutionAfterAction(trueOrRevert = true) { stopExecution = trueOrRevert; } /** * each each time a store returns a `FetchResponse` then it must first go through all on added fns to potentially modify the retuned payload before writing to cache */ const doOnAddedFns = modifyReadResponseMap.added; /** * each each time a store returns a `FetchResponse` then all `doOnFetchFns` need to be executed */ const doOnFetchFns = []; /** * each each time a store returns a `FetchAggregateResponse` then all `DoOnFetchAggregate` need to be executed */ const doOnFetchAggregateFns = []; /** * Fetching on a collection should return a map with just the fetched records for that API call */ const collectionFetchResult = new Map(); /** * the result of fetchCount / fetchSum / fetchAverage */ let fetchAggregate = NaN; /** * All possible results from the plugins. * `unknown` in case an error was thrown */ let resultFromPlugin; // handle and await each action in sequence for (const storeName of storesToExecute.values()) { // a previous iteration stopped the execution: if (stopExecution === true) break; // find the action on the plugin const pluginAction = globalConfig.stores[storeName]?.actions[actionName]; const pluginModuleConfig = getPluginModuleConfig(moduleConfig, storeName); // the plugin action resultFromPlugin = !pluginAction ? resultFromPlugin : await handleAction({ collectionPath, docId, modulePath, pluginModuleConfig, pluginAction, payload, // should always use the payload as passed originally for clarity actionConfig, eventNameFnsMap, onError, actionName, stopExecutionAfterAction, storeName, }); // handle reverting. stopExecution might have been modified by `handleAction` if (stopExecution === 'revert') { // we must update the `exists` prop for fetch calls if (actionName === 'fetch' && docId) { doOnFetchFns.forEach((fn) => fn(undefined, 'error')); } // now we must throw the error throw resultFromPlugin; } // special handling for 'fetch' (resultFromPlugin will always be `FetchResponse | OnAddedFn`) if (actionName === 'fetch') { if (isDoOnFetch(resultFromPlugin)) { doOnFetchFns.push(resultFromPlugin); } if (isFetchResponse(resultFromPlugin)) { const { docs, reachedEnd, cursor } = resultFromPlugin; if (isBoolean(reachedEnd)) setLastFetched?.({ reachedEnd, cursor }); for (const docMetaData of docs) { const docResult = executeOnFns({ modifyReadResultFns: doOnAddedFns, cacheStoreFns: doOnFetchFns, payload: docMetaData.data, docMetaData, }); // after doing all `doOnAddedFns` (modifying the read result) // and all `doOnFetchFns` (adding it to the local cache store) // we still have a record, so must return it when resolving the fetch action if (docResult) collectionFetchResult.set(docMetaData.id, docResult); // optimistic fetching can stop the action chain after getting a fetch response for the first time const optimisticFetch = !force; if (optimisticFetch) { stopExecutionAfterAction(true); } } } } // special handling for 'fetch' (resultFromPlugin will always be `FetchResponse | OnAddedFn`) if (actionName === 'fetchCount' || actionName === 'fetchSum' || actionName === 'fetchAverage') { if (isDoOnFetchAggregate(resultFromPlugin)) { doOnFetchAggregateFns.push(resultFromPlugin); } if (isFetchAggregateResponse(resultFromPlugin)) { for (const doOnFetchAggregateFn of doOnFetchAggregateFns) { doOnFetchAggregateFn(resultFromPlugin); } if (isNaN(fetchAggregate) || resultFromPlugin > fetchAggregate) { fetchAggregate = resultFromPlugin; } } } } // all the stores resolved their actions fetchPromises[actionName].delete(fetchPromiseKey); if (actionName === 'fetchCount' || actionName === 'fetchSum' || actionName === 'fetchAverage') { // return the fetchCount resolve(fetchAggregate); return; } // anything that's executed from a "doc" module: if (docId || !collectionFn) { // return the module's up-to-date data resolve(docFn(modulePath, moduleConfig).data); return; } // return the collectionFetchResult resolve(collectionFetchResult); } catch (error) { reject(error); fetchPromises[actionName].delete(fetchPromiseKey); } }); fetchPromises[actionName]?.set(fetchPromiseKey, actionPromise); return actionPromise; }; }