UNPKG

repond-events

Version:
518 lines (517 loc) 24.7 kB
import { breakableForEach, forEach } from "chootils/dist/loops"; import { addItem, getItemIds, getItemWillExist, getState, onNextTick, removeItem, setState, whenSettingStates, } from "repond"; import { repondEventsMeta as meta, repondEventsMeta } from "./meta"; import { evaluateParams } from "./valueHelpers"; export function getElapsedTime(liveId) { let foundElapsedTimeStatePath = meta.defaultElapsedTimePath; if (liveId) { const liveEventElapsedTimePath = getState("liveEvents", liveId)?.elapsedTimePath; foundElapsedTimeStatePath = liveEventElapsedTimePath ?? foundElapsedTimeStatePath; } if (!foundElapsedTimeStatePath) return console.warn("no elapsedTimePath found"), 0; const [itemType, itemId, itemProp] = foundElapsedTimeStatePath; // Using any since the item type is dynamic const foundElapsedTime = getState(itemType, itemId)?.[itemProp]; if (!foundElapsedTime) return console.warn("no elapsedTime state found for ", itemType, itemId, itemProp), 0; return foundElapsedTime; } export function eventTupleToEventBlock(eventTuple) { return { group: eventTuple[0], name: eventTuple[1], params: eventTuple[2], options: eventTuple[3] ?? {}, }; } export function toEventBlockIfNeeded(event) { return Array.isArray(event) ? eventTupleToEventBlock(event) : event; } export function eventTuplesToEventBlocks(eventTuples) { // return eventTuples.map(eventTupleToEventBlock); return eventTuples.map(toEventBlockIfNeeded); } export function getLiveIdsForGroup(group) { const allLiveEventIds = getItemIds("liveEvents"); return allLiveEventIds.filter((liveId) => getState("liveEvents", liveId)?.event.group === group); } export function getLiveEventsIdsUpToLiveEventId(liveId) { const chainId = getChainIdFromLiveEventId(liveId); // `no chain found for ${liveId}` if (!chainId) return []; const chainState = getState("chains", chainId); const liveIds = chainState?.liveEventIds ?? []; const eventIndex = liveIds.indexOf(liveId); if (eventIndex === -1) return console.warn(`no event found for ${liveId}`), []; return liveIds.slice(0, eventIndex + 1); } export function findFirstNonActiveEventIndex(liveEventIds) { if (liveEventIds.length === 0) { return 0; // If the list is empty, return 0 } const firstEventState = getState("liveEvents", liveEventIds[0]); const firstEventCanStart = firstEventState?.nowRunMode === "add"; if ((!firstEventState || !firstEventState.isParallel) && firstEventCanStart) { return 1; // If the first event is not parallel, immediately return 1 } if (!firstEventCanStart) return 0; // If the first event is not ready to start, return 0 // If the first event is parallel, find the first non-parallel event, or non ready to start parallel event for (let i = 1; i < liveEventIds.length; i++) { const eventState = getState("liveEvents", liveEventIds[i]); const eventCanStart = eventState?.nowRunMode === "add"; if (!eventState || (!eventState.isParallel && eventCanStart) || (eventState?.isParallel && !eventCanStart)) { return i; // Return the 0-based index of the first non-parallel event } } // If all events are parallel, return the length of the list return firstEventCanStart ? liveEventIds.length : 0; } export function getActiveEventIds(liveEventIds) { const index = findFirstNonActiveEventIndex(liveEventIds); // If index is 0, no events are active, otherwise slice up to index return index === 0 ? [] : liveEventIds.slice(0, index); } function getInsertEventIdsAfterActive(liveEventIds, newEventIds) { const index = findFirstNonActiveEventIndex(liveEventIds); return [...liveEventIds.slice(0, index), ...newEventIds, ...liveEventIds.slice(index)]; } export function getChainIdFromLiveEventId(liveId) { // find the chainId from the liveId, then skip the chain let foundChainId; const allChainIds = getItemIds("chains"); breakableForEach(allChainIds, (chainId) => { const chainState = getState("chains", chainId); if (chainState?.liveEventIds.includes(liveId)) { foundChainId = chainId; return true; // break from the loop } }); return foundChainId; } export function getNewChainId() { return `chain_${Date.now()}_${Math.floor(Math.random() * 10000)}`; } // TODO maybe update the name to runLiveEventHandler because of isFast handling running events differently export async function runEventHandler(liveEventId) { const liveEventState = getState("liveEvents", liveEventId); if (!liveEventState) return console.warn(`no liveEvent found for ${liveEventId}`); const { nowRunMode: runMode } = liveEventState; if (!runMode) return console.warn(`no runMode found for ${liveEventId}`); const eventTypeDefinition = meta.allEventTypeGroups[liveEventState.event.group]?.[liveEventState.event.name]; const eventHandler = eventTypeDefinition.run; if (!eventHandler) return console.warn(`no handler found for ${liveEventState.event.group}.${liveEventState.event.name}`); const elapsedTime = getElapsedTime(liveEventState.chainId); let relevantTimeProp = "startTime"; // NOTE "unpause" or "unsuspend" is never seen in the handler, since it will resume to what it was before pause or suspend if (runMode === "start") relevantTimeProp = "startTime"; else if (runMode === "add") relevantTimeProp = "addTime"; else if (runMode === "pause") relevantTimeProp = "pauseTime"; else if (runMode === "suspend") relevantTimeProp = "suspendTime"; const relevantTime = liveEventState[relevantTimeProp]; // inifity if it hasn't been set yet const isUnpausing = relevantTime != null && liveEventState.unpauseTime === relevantTime; const isUnsuspending = relevantTime != null && liveEventState.unsuspendTime === relevantTime; const isUnfreezing = isUnpausing || isUnsuspending; const isFreezing = runMode === "pause" || runMode === "suspend"; const unpauseTime = liveEventState.unpauseTime ?? 0; const unsuspendTime = liveEventState.unsuspendTime ?? 0; const liveInfo = { runBy: "repond-events", addedBy: liveEventState.addedBy ?? "unknown", runMode, isFast: false, // TODO Support isFast, It might not ever be true in this part didStart: (liveEventState.startTime ?? 0) > 0, // a little different to isActive, if it started at all? chainId: liveEventState.chainId, liveId: liveEventState.id, remainingTime: 0, elapsedTime, isUnpausing, isUnsuspending, isUnfreezing, isFreezing, isFirstAdd: runMode === "add" && !isUnfreezing, isFirstStart: runMode === "start" && !isUnfreezing, isFirstPause: runMode === "pause" && !unpauseTime, isFirstSuspend: runMode === "suspend" && !unsuspendTime, addTime: liveEventState.addTime ?? 0, startTime: liveEventState.startTime ?? 0, pauseTime: liveEventState.pauseTime ?? 0, unpauseTime, suspendTime: liveEventState.suspendTime ?? 0, unsuspendTime, goalEndTime: liveEventState.goalEndTime ?? 0, }; const paramsWithDefaults = { ...eventTypeDefinition.params, ...liveEventState.event.params }; // If the event doesn't have evaluatedParams in state, evaluate them // If the run mode is "run", and it's the first time the event is run, re-evaluate the params let evaluatedParams = liveEventState.evaluatedParams; if (!evaluatedParams || (runMode === "start" && liveInfo.isFirstStart)) { evaluatedParams = await evaluateParams(paramsWithDefaults, { addedBy: liveInfo.addedBy, runBy: liveInfo.runBy, parentChainId: liveInfo.chainId, valueIdBase: liveInfo.liveId, }); } // Run the event handler if (evaluatedParams) { eventHandler(evaluatedParams, liveInfo); setState(`liveEvents.evaluatedParams`, evaluatedParams, liveEventId); } else { console.warn(`no evaluatedParams found for ${liveEventId} , ${eventTypeDefinition.group}.${eventTypeDefinition.name}`); } } export function finalizeEvent(liveEventId) { whenSettingStates(() => { const liveEventState = getState("liveEvents", liveEventId); if (!liveEventState) return console.warn(`no liveEvent found for ${liveEventId}`), {}; const chainId = liveEventState.chainId; if (!chainId) return {}; // `no chain found for ${liveEventId}` const chainState = getState("chains", chainId); if (!chainState) return {}; // `no chain found for ${chainId}` // Remove this liveEventId from the chain const newLiveEventIds = chainState.liveEventIds.filter((id) => id !== liveEventId); setState(`chains.liveEventIds`, newLiveEventIds, chainId); }); onNextTick(() => removeItem("liveEvents", liveEventId)); } function _makeLiveEventStateFromEvent(event) { const { chainId, liveId, addedBy, isParallel, timePath, hasPriority, duration, parentChainId } = event.options; const eventType = getEventTypeDefinition(event.group, event.name); let foundElapsedTime = 0; const foundElapsedTimeStatePath = timePath ?? meta.defaultElapsedTimePath; if (foundElapsedTimeStatePath) { const [itemType, itemId, itemProp] = foundElapsedTimeStatePath; // Using any since the item type is dynamic foundElapsedTime = getState(itemType, itemId)?.[itemProp] ?? 0; } // Finish instantly by default, can set to Infinity for no automatic end // If a duration's provided, then it sets the endTime in when the live event starts const goalEndTime = 0; return { id: liveId, // nowRunMode: null, nowRunMode: "add", // isActive: false, elapsedTimePath: timePath ?? meta.defaultElapsedTimePath, isParallel: !!isParallel, duration: duration ?? null, addedBy: addedBy ?? null, chainId, parentChainId, event, addTime: Date.now(), readyTime: null, startTime: null, pauseTime: null, unpauseTime: null, suspendTime: null, unsuspendTime: null, goalEndTime, runBy: null, runModeOptionsWhenReady: null, runModeBeforePause: null, runModeBeforeSuspend: null, }; } export function _makeLiveIdFromEventBlock(event) { const { chainId, liveId, addedBy } = event.options; return (liveId ?? `${event.group}_${event.name}_${chainId}${addedBy ? "_by" + addedBy : ""}${"_event_" + Math.floor(Math.random() * 10000)}`); } export function makeNewChainId() { return `chain_${Date.now()}_${Math.floor(Math.random() * 10000)}`; } export function eventBlockBaseToEventBlock(eventBlockBase, options) { return { ...eventBlockBase, options: options ?? {}, }; } export function getEventTypeDefinition(group, name) { return meta.allEventTypeGroups[group]?.[name]; } // Meant to be used for fast mode // in a loop until the chain is finished async function runEventFastMode(event, options) { // Finds and runs the event handler for the event // it will always run immediately const { chainId } = options; if (!chainId) return console.warn(`no chainId found for fast ${event.group}.${event.name}`), {}; const eventTypeDefinition = meta.allEventTypeGroups[event.group]?.[event.name]; const eventHandler = eventTypeDefinition.run; // let evaluatedParams: ParamMap | undefined | null = liveEventState.evaluatedParams; const paramsWithDefaults = { ...eventTypeDefinition.params, ...event.params }; const evaluatedParams = await evaluateParams(paramsWithDefaults, { addedBy: options.addedBy, runBy: "repond-events", parentChainId: options.parentChainId, valueIdBase: chainId, }); const liveInfo = { runBy: "repond-events", addedBy: "unknown", // TODO maybe add addedBy for fast mode runMode: "start", isFast: true, didStart: true, chainId: chainId, liveId: "unknown", // NOTE no live id in fast mode remainingTime: 0, elapsedTime: 0, isUnpausing: false, isUnsuspending: false, isUnfreezing: false, isFreezing: false, isFirstAdd: true, isFirstStart: true, isFirstPause: false, isFirstSuspend: false, addTime: Date.now(), startTime: Date.now(), pauseTime: 0, unpauseTime: 0, suspendTime: 0, unsuspendTime: 0, goalEndTime: Date.now(), }; await eventHandler(event.params, liveInfo); } export function _addEvents(eventBlocks, listOptions) { // If the parents liveEventId is provided, then it creates a chain with the same id, which makes it a subChain // This means the event will automatically finish when the subchain finishes const liveId = listOptions.liveId; const chainId = liveId ?? listOptions.chainId ?? repondEventsMeta.defaultChainId ?? makeNewChainId(); const parentChainId = listOptions.parentChainId; const isFast = listOptions.isFast ?? false; const isForSubChain = !!liveId; // If it's fast mode, it will run the events immediately // and not add live events to the state if (isFast) { console.warn("fast mode not supported yet for _addEvents"); const fastChainMeta = repondEventsMeta.fastChain; // If the parent chain is not a fast chain, then set up the new fastChain meta info const parentChainIsFast = parentChainId ? !!fastChainMeta.nowFastChainsInfoMap[parentChainId] : false; // If its the first fast chain, set the root fast chain id if (!parentChainIsFast) { fastChainMeta.nowRootFastChainId = chainId; fastChainMeta.nowRootFastChainParentId = parentChainId; fastChainMeta.nowFastChainsInfoMap = {}; fastChainMeta.nowFastChainsInfoMap[chainId] = { nowChildFastChainId: undefined, isCanceled: false, parentFastChainId: undefined, variablesMap: {}, }; } else { // If the parent chain is a fast chain, then set up the new fastChain meta info if (parentChainId) { const parentFastChainInfo = fastChainMeta.nowFastChainsInfoMap[parentChainId]; parentFastChainInfo.nowChildFastChainId = chainId; } fastChainMeta.nowFastChainsInfoMap[chainId] = { nowChildFastChainId: undefined, isCanceled: false, parentFastChainId: parentChainId, variablesMap: {}, }; } // Run through all the events in fast mode, while the chain isn't canceled for (const event of eventBlocks) { if (fastChainMeta.nowFastChainsInfoMap[chainId].isCanceled) { break; } runEventFastMode(event, listOptions); } // If it's the first fast chain, when it finishes, clear the fast chain meta info if (!parentChainIsFast) { fastChainMeta.nowRootFastChainId = undefined; fastChainMeta.nowRootFastChainParentId = undefined; fastChainMeta.nowFastChainsInfoMap = {}; } return chainId; } const addTheLiveEvents = () => { const events = eventBlocks.map((event) => { const eventType = getEventTypeDefinition(event.group, event.name); if (!eventType) { console.warn(`no eventType found for ${event.group}.${event.name}`); } return { ...event, options: { ...listOptions, liveId: undefined, ...event.options, addedBy: event.options?.addedBy ?? listOptions?.addedBy, isParallel: event.options?.isParallel ?? listOptions?.isParallel ?? eventType?.isParallel, chainId, // NOTE the chainId is always the same for all events added at once timePath: event.options?.timePath ?? listOptions.timePath ?? eventType?.timePath, duration: event.options?.duration ?? listOptions.duration ?? eventType?.duration, }, }; }); const chainDoesntExist = !getItemWillExist("chains", chainId); // Check if the parent liveEvent is already running const existingParentLiveEventForSubChain = liveId ? getState("liveEvents", liveId) : null; const parentLiveEventRunMode = existingParentLiveEventForSubChain?.nowRunMode ?? null; const parentLiveEventHasNonAddRunMode = liveId ? parentLiveEventRunMode && parentLiveEventRunMode !== "add" : false; if (chainDoesntExist) { // if the chain doesn’t exist, create the chain with the events // dont auto start the chain if it’s a subChain, it will start when the parent liveEvent starts let canAutoActivate = !isForSubChain; if (parentLiveEventHasNonAddRunMode) { // If the parent liveEvent is already running, start the subChain immediately canAutoActivate = true; } // TODO needs parentChainId const newChainState = { id: chainId, liveEventIds: [], canAutoActivate, parentChainId }; addItem("chains", chainId, newChainState); } const newDuplicateLiveEventsMap = {}; const eventsWithoutDuplicates = events.filter((event) => { const liveId = event.options.liveId ?? _makeLiveIdFromEventBlock(event); if (getItemWillExist("liveEvents", liveId)) { newDuplicateLiveEventsMap[liveId] = event; return false; } return true; }); const newLiveIds = []; eventsWithoutDuplicates.forEach((event, index) => { const eventOptions = event.options; const chainId = listOptions.chainId ?? eventOptions.chainId; // NOTE I think this aleways has to be the same if (!chainId) return console.warn(`no chainId found for ${event.group}.${event.name}`), {}; const eventWithNewOptions = { ...event, options: { ...event.options, chainId } }; const liveId = eventOptions.liveId ?? _makeLiveIdFromEventBlock(eventWithNewOptions); newLiveIds.push(liveId); const eventWithLiveId = { ...eventWithNewOptions, options: { ...eventWithNewOptions.options, liveId } }; addItem("liveEvents", liveId, _makeLiveEventStateFromEvent(eventWithLiveId)); }); whenSettingStates(() => { const chainState = getState("chains", chainId); // NOTE make sure the chain is added before setting the chain state if (!chainState) return console.warn(`chain ${chainId} doesn't exist`), {}; const nowChainEventIds = chainState.liveEventIds; const newChainEventIds = listOptions.hasPriority ? getInsertEventIdsAfterActive(nowChainEventIds, newLiveIds) : [...nowChainEventIds, ...newLiveIds]; setState("chains.liveEventIds", newChainEventIds, chainId); setState("chains.duplicateEventsToAdd", { ...chainState.duplicateEventsToAdd, ...newDuplicateLiveEventsMap }, chainId); // If events are added to a subChain, set the goalEndTime for the parent event to Infinity, to wait for the subChain to finish if (isForSubChain) { setState("liveEvents.goalEndTime", Infinity, chainId); if (parentLiveEventHasNonAddRunMode && parentLiveEventRunMode !== "start") { forEach(newChainEventIds, (liveId) => { setState("liveEvents.nowRunMode", "add", liveId); }); } } const liveEventsToCancel = Object.keys(chainState.duplicateEventsToAdd); liveEventsToCancel.forEach((liveId) => { const event = chainState.duplicateEventsToAdd[liveId]; const eventState = getState("liveEvents", liveId); if (!eventState) return console.warn(`no liveEvent found for ${liveId}`); setState("liveEvents.nowRunMode", "cancel", liveId); }); }); }; if (isForSubChain && getItemWillExist("liveEvents", liveId)) { // If its a subChain, add it immediately, since the parent liveEvent is already running addTheLiveEvents(); } else { onNextTick(addTheLiveEvents); } return chainId; // NOTE events are auto started by the chain after the liveEventIds are set } export function _addEvent(event, options) { const eventBlock = { ...event, options }; const newChainId = _addEvents([eventBlock], { ...options, liveId: undefined }); // remove the liveId options for all the events, otherwise they will be counted as sub events return newChainId; } export function _setStatesToRunEventsInMode({ runMode, targetLiveIds, chainId, runOptions, }) { const runOptionKeys = Object.keys(runOptions ?? {}); const foundSubChains = []; const liveIdsByChain = {}; let allFoundLiveIds = targetLiveIds ?? []; if (chainId && targetLiveIds) { liveIdsByChain[chainId] = [...targetLiveIds]; } else if (chainId && !targetLiveIds) { const chainState = getState("chains", chainId); targetLiveIds = chainState?.liveEventIds; allFoundLiveIds = targetLiveIds ?? []; if (!targetLiveIds) return {}; // `no liveEventIds found for chain ${chainId}` liveIdsByChain[chainId] = [...targetLiveIds]; } else if (!chainId && targetLiveIds) { for (const liveId of targetLiveIds) { const chainId = getChainIdFromLiveEventId(liveId); if (!chainId) return {}; // `no chain found for ${liveId}` if (!liveIdsByChain[chainId]) liveIdsByChain[chainId] = []; liveIdsByChain[chainId].push(liveId); } } for (const liveId of allFoundLiveIds) { // check if there's a chain with the same id as the liveId, meaning it's a subChain if (getItemWillExist("chains", liveId)) foundSubChains.push(liveId); } // add the liveIds of the subChains for (const subChainId of foundSubChains) { const subChainState = getState("chains", subChainId); const subChainLiveIds = subChainState?.liveEventIds; if (!subChainLiveIds) return {}; // `no liveEventIds found for chain ${subChainId}` liveIdsByChain[subChainId] = [...subChainLiveIds]; } const foundChainIds = Object.keys(liveIdsByChain); // const newPartialLiveEventsState: Record<string, Partial<ItemState<"liveEvents">>> = {}; for (const chainId of foundChainIds) { let targetChainLiveIdsUnordered = liveIdsByChain[chainId]; const nowChainLiveIds = getState("chains", chainId)?.liveEventIds; if (!nowChainLiveIds || !targetChainLiveIdsUnordered) continue; // get the liveIds that are in the chain and also in the liveIds array in order, or all if liveIds is not set const targetChainLiveIds = nowChainLiveIds.filter((liveId) => targetChainLiveIdsUnordered.includes(liveId)); for (const liveEventId of targetChainLiveIds) { const liveEventState = getState("liveEvents", liveEventId); if (runMode === "skip") { // newPartialLiveEventsState[liveEventId] = { runModeOptionsWhenReady: { runMode, ...runOptions } }; setState("liveEvents.runModeOptionsWhenReady", { runMode, ...runOptions }, liveEventId); } else { // newPartialLiveEventsState[liveEventId] = { nowRunMode: runMode, ...runOptions }; setState("liveEvents.nowRunMode", runMode, liveEventId); } // Add the extra run optinos to the items state if (runOptions) { forEach(runOptionKeys, (key) => setState(`liveEvents.${key}`, runOptions[key], liveEventId)); } } } // return { liveEvents: newPartialLiveEventsState }; }