@magnetarjs/core
Version:
Magnetar core library.
206 lines (205 loc) • 10.4 kB
JavaScript
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;
};
}