repond-events
Version:
events for repond
518 lines (517 loc) • 24.7 kB
JavaScript
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 };
}