UNPKG

@magnetarjs/core

Version:
206 lines (205 loc) 10.4 kB
import { 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 { isDoOnStream } from '../helpers/pluginHelpers.js'; import { throwIfNoFnsToExecute, throwOnIncompleteStreamResponses } from '../helpers/throwFns.js'; import { getDocAfterWritelock } from '../helpers/writeLockHelpers.js'; import { handleStream } from './handleStream.js'; export function handleStreamPerStore([collectionPath, docId], moduleConfig, globalConfig, actionType, streaming, cacheStream, writeLockMap) { // returns the action the dev can call with myModule.insert() etc. return async function (payload, actionConfig = {}) { // return the same stream promise if it's already open const foundStream = streaming(); if (isPromise(foundStream)) { // If onFirstData is provided and stream is already open, call it with existingStream flag if (payload?.onFirstData) { setTimeout(() => payload.onFirstData({ empty: undefined, existingStream: true }), 0); } return foundStream; } // get all the config needed to perform this action const eventNameFnsMap = getEventNameFnsMap(globalConfig.on, moduleConfig.on, actionConfig.on); const modifyPayloadFnsMap = getModifyPayloadFnsMap(globalConfig.modifyPayloadOn, moduleConfig.modifyPayloadOn, actionConfig.modifyPayloadOn); const modifyReadResponseMap = getModifyReadResponseFnsMap(globalConfig.modifyReadResponseOn, moduleConfig.modifyReadResponseOn, actionConfig.modifyReadResponseOn); const storesToExecute = actionConfig.executionOrder || (moduleConfig.executionOrder || {})['stream'] || (moduleConfig.executionOrder || {})[actionType] || (globalConfig.executionOrder || {})['stream'] || (globalConfig.executionOrder || {})[actionType] || []; throwIfNoFnsToExecute(storesToExecute); // update the payload for (const modifyFn of modifyPayloadFnsMap['stream']) { payload = modifyFn(payload, docId); } const streamInfoPerStore = {}; const modifyReadResponseFns = { added: modifyReadResponseMap.added, modified: modifyReadResponseMap.modified, removed: modifyReadResponseMap.removed, }; const doOnStreamFns = { added: [], modified: [], removed: [], }; /** * Last incoming added/modified docs are cached here temporarily to prevent UI flashing because of the writeLock */ const lastIncomingDocs = new Map(); /** * this is what must be executed by a plugin store that implemented "stream" functionality */ const mustExecuteOnRead = { /** musn't be async to avoid an issue where it can't group UI updates in 1 tick, when many docs come in */ added: (_payload, _meta) => { // check if there's a WriteLock for the document: const docIdentifier = `${collectionPath}/${_meta.id}`; // check if there's a WriteLock for the document const writeLock = writeLockMap.get(docIdentifier); if (writeLock && isPromise(writeLock.promise)) { // add to lastIncoming map lastIncomingDocs.set(docIdentifier, { payload: _payload, meta: _meta }); writeLock.promise.then(() => { // ... const result = getDocAfterWritelock({ docIdentifier, lastIncomingDocs }); // if `undefined` nothing further must be done if (!result) return; return executeOnFns({ modifyReadResultFns: modifyReadResponseFns.added, cacheStoreFns: doOnStreamFns.added, payload: result.payload, docMetaData: result.meta, }); }); // prevent immediate execution while there's a write lock return; } return executeOnFns({ modifyReadResultFns: modifyReadResponseFns.added, cacheStoreFns: doOnStreamFns.added, payload: _payload, docMetaData: _meta, }); }, /** musn't be async to avoid an issue where it can't group UI updates in 1 tick, when many docs come in */ modified: (_payload, _meta) => { // check if there's a WriteLock for the document: const docIdentifier = `${collectionPath}/${_meta.id}`; // check if there's a WriteLock for the document const writeLock = writeLockMap.get(docIdentifier); if (writeLock && isPromise(writeLock.promise)) { // add to lastIncoming map lastIncomingDocs.set(docIdentifier, { payload: _payload, meta: _meta }); writeLock.promise.then(() => { // ... const result = getDocAfterWritelock({ docIdentifier, lastIncomingDocs }); // if `undefined` nothing further must be done if (!result) return; return executeOnFns({ modifyReadResultFns: modifyReadResponseFns.modified, cacheStoreFns: doOnStreamFns.modified, payload: result.payload, docMetaData: result.meta, }); }); // prevent immediate execution while there's a write lock return; } return executeOnFns({ modifyReadResultFns: modifyReadResponseFns.modified, cacheStoreFns: doOnStreamFns.modified, payload: _payload, docMetaData: _meta, }); }, /** musn't be async to avoid an issue where it can't group UI updates in 1 tick, when many docs come in */ removed: (_payload, _meta) => { // check if there's a WriteLock for the document const docIdentifier = `${collectionPath}/${_meta.id}`; // must delete any piled up writeLock docs if by now it's deleted lastIncomingDocs.delete(docIdentifier); const writeLock = writeLockMap.get(docIdentifier); if (writeLock && isPromise(writeLock.promise)) { writeLock.promise.then(() => { return executeOnFns({ modifyReadResultFns: modifyReadResponseFns.removed, cacheStoreFns: doOnStreamFns.removed, payload: _payload, docMetaData: _meta, }); }); // prevent immediate execution while there's a write lock return; } return executeOnFns({ modifyReadResultFns: modifyReadResponseFns.removed, cacheStoreFns: doOnStreamFns.removed, payload: _payload, docMetaData: _meta, }); }, }; // handle and await each action in sequence for (const storeName of storesToExecute) { // find the action on the plugin const pluginAction = globalConfig.stores[storeName]?.actions['stream']; const pluginModuleConfig = getPluginModuleConfig(moduleConfig, storeName); // the plugin action if (pluginAction) { const result = await handleStream({ collectionPath, docId, pluginModuleConfig, pluginAction, actionConfig, payload, // should always use the payload as passed originally for clarity eventNameFnsMap, actionName: 'stream', storeName, mustExecuteOnRead, }); // if the plugin action for stream returns a "do on read" result if (isDoOnStream(result)) { // register the functions we received: result for (const [doOn, doFn] of Object.entries(result)) { if (doFn) doOnStreamFns[doOn].push(doFn); } } // if the plugin action for stream returns a "stream response" result if (!isDoOnStream(result)) { streamInfoPerStore[storeName] = result; } } } throwOnIncompleteStreamResponses(streamInfoPerStore, doOnStreamFns); // create a function to closeStream from the stream of each store const closeStream = () => { Object.values(streamInfoPerStore).forEach(({ stop }) => stop()); cacheStream(() => undefined, null); }; // handle caching the returned promises const streamPromises = Object.values(streamInfoPerStore).map((res) => res.streaming); // create a single stream promise from multiple stream promises the store plugins return const streamPromise = new Promise((resolve, reject) => { Promise.all(streamPromises) .then(() => { resolve(); closeStream(); }) .catch((e) => { reject(e); closeStream(); }); }); cacheStream(closeStream, streamPromise); // return the stream promise return streamPromise; }; }