UNPKG

@magnetarjs/core

Version:
177 lines (176 loc) 10.1 kB
import { isStoreSplit } from '@magnetarjs/utils'; import { mapGetOrSet } from 'getorset-anything'; import { isFullArray, isFullString, isPlainObject } from 'is-what'; import { mapObject } from 'map-anything'; import { getEventNameFnsMap } from '../helpers/eventHelpers.js'; import { getModifyPayloadFnsMap } from '../helpers/modifyPayload.js'; import { getPluginModuleConfig } from '../helpers/moduleHelpers.js'; import { throwIfNoFnsToExecute } from '../helpers/throwFns.js'; import { handleAction } from './handleAction.js'; export function handleWritePerStore(sharedParams, actionName) { const { collectionPath, _docId, moduleConfig, globalConfig, writeLockMap, docFn, collectionFn } = sharedParams; // prettier-ignore // returns the action the dev can call with myModule.insert() etc. return function (payload, actionConfig = {}) { // set up and/or reset te writeLock for write actions const writeLockId = _docId ? `${collectionPath}/${_docId}` : collectionPath; const writeLock = mapGetOrSet(writeLockMap, writeLockId, () => { return { promise: null, resolve: () => undefined, countdown: null }; }); // we need to create a promise we'll resolve later to prevent any incoming docs from being written to the cache store during this time if (writeLock.promise === null) { writeLock.promise = new Promise((resolve) => { writeLock.resolve = () => { resolve(); writeLock.resolve = () => undefined; writeLock.promise = null; if (writeLock.countdown !== null) { clearTimeout(writeLock.countdown); writeLock.countdown = null; } }; }); } if (writeLock.promise !== null && writeLock.countdown !== null) { // there already is a promise, let's just stop the countdown, we'll start it again at the end of all the store actions clearTimeout(writeLock.countdown); writeLock.countdown = null; } // eslint-disable-next-line no-async-promise-executor const actionPromise = new Promise(async (resolve, reject) => { let docId = _docId; let modulePath = [collectionPath, docId].filter(Boolean).join('/'); 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 eventNameFnsMap = getEventNameFnsMap(globalConfig.on, moduleConfig.on, actionConfig.on); const storesToExecute = actionConfig.executionOrder || (moduleConfig.executionOrder || {})[actionName] || (moduleConfig.executionOrder || {})['write'] || (globalConfig.executionOrder || {})[actionName] || (globalConfig.executionOrder || {})['write'] || []; throwIfNoFnsToExecute(storesToExecute); const unwrapStoreSplits = (payloadChunk, storeName) => { return isStoreSplit(payloadChunk) ? payloadChunk.storePayloadDic[storeName] : isPlainObject(payloadChunk) ? mapObject(payloadChunk, (value) => unwrapStoreSplits(value, storeName)) : payloadChunk; }; let payloadModified = payload; for (const modifyFn of modifyPayloadFnsMap[actionName]) { /** it's important to only execute the hooks once! */ payloadModified = modifyFn(payloadModified); } /** Now let's create a separate payload per store */ const storePayloadDic = storesToExecute.reduce((dic, storeName) => ({ ...dic, [storeName]: unwrapStoreSplits(payloadModified, storeName) }), // prettier-ignore {}); 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; } /** * Fetching on a collection should return a map with just the fetched records for that API call */ const collectionFetchResult = new Map(); /** * All possible results from the plugins. * `unknown` in case an error was thrown */ let resultFromPlugin; // handle and await each action in sequence for (const [i, storeName] of storesToExecute.entries()) { // 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: storePayloadDic[storeName], // 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') { const storesToRevert = storesToExecute.slice(0, i); storesToRevert.reverse(); for (const storeToRevert of storesToRevert) { const pluginRevertAction = globalConfig.stores[storeToRevert]?.revert; const pluginModuleConfig = getPluginModuleConfig(moduleConfig, storeToRevert); if (pluginRevertAction) { await pluginRevertAction({ payload: storePayloadDic[storeName], actionConfig, collectionPath, docId, pluginModuleConfig, actionName, error: resultFromPlugin, // in this case the result is the error }); } // revert eventFns, handle and await each eventFn in sequence for (const fn of eventNameFnsMap.revert) { await fn({ payload: storePayloadDic[storeName], result: resultFromPlugin, actionName, storeName, collectionPath, docId, path: modulePath, pluginModuleConfig }); // prettier-ignore } } writeLock.resolve(); // now we must throw the error throw resultFromPlugin; } // special handling for 'insert' (resultFromPlugin will always be `string | [string, SyncBatch]`) if (actionName === 'insert') { // update the modulePath if a doc with random ID was inserted in a collection // if this is the case the result will be a string - the randomly genererated ID if (!docId) { if (isFullString(resultFromPlugin)) { docId = resultFromPlugin; } if (isFullArray(resultFromPlugin) && isFullString(resultFromPlugin[0])) { docId = resultFromPlugin[0]; } modulePath = [collectionPath, docId].filter(Boolean).join('/'); } } } // all the stores resolved their actions // start the writeLock countdown if (!writeLock.countdown) { writeLock.countdown = setTimeout(writeLock.resolve, 5000); } // 'insert' always returns a DocInstance, unless the "abort" action was called, then the modulePath might still be a collection: if (actionName === 'insert' && docId) { resolve(docFn(modulePath, moduleConfig)); return; } // anything that's executed from a "doc" module: if (docId || !collectionFn) { resolve(docFn(modulePath, moduleConfig).data); return; } // all other actions triggered on collections ('fetch' is the only possibility left) resolve(collectionFetchResult); } catch (error) { reject(error); } }); return actionPromise; }; }