diff --git a/core/src/makeCoreStore.ts b/core/src/makeCoreStore.ts index 24b6453b..1ada06c3 100644 --- a/core/src/makeCoreStore.ts +++ b/core/src/makeCoreStore.ts @@ -1,14 +1,14 @@ import isEqual from "react-fast-compare"; -import type { Effect } from "./Effect"; import type { Stack } from "./Stack"; import { aggregate } from "./aggregate"; import type { DomainEvent, PushedEvent, StepPushedEvent } from "./event-types"; -import type { BaseDomainEvent } from "./event-types/_base"; import { makeEvent } from "./event-utils"; import type { StackflowActions, StackflowPlugin } from "./interfaces"; import { produceEffects } from "./produceEffects"; import { divideBy, once } from "./utils"; +import { makeActions } from "./utils/makeActions"; +import { triggerPostEffectHooks } from "./utils/triggerPostEffectHooks"; const SECOND = 1000; @@ -77,9 +77,7 @@ export function makeCoreStore(options: MakeCoreStoreOptions): CoreStore { options.handlers?.onInitialActivityNotFound?.(); } - const events: { - value: DomainEvent[]; - } = { + const events: { value: DomainEvent[] } = { value: [...initialRemainingEvents, ...initialPushedEvents], }; @@ -87,540 +85,58 @@ export function makeCoreStore(options: MakeCoreStoreOptions): CoreStore { value: aggregate(events.value, new Date().getTime()), }; - const setStackValue = (nextStackValue: Stack) => { - const effects = produceEffects(stack.value, nextStackValue); - - stack.value = nextStackValue; - - triggerPostEffectHooks(effects, pluginInstances); - }; - - const dispatchEvent: StackflowActions["dispatchEvent"] = (name, params) => { - const newEvent = makeEvent(name, params); - - const nextStackValue = aggregate( - [...events.value, newEvent], - new Date().getTime(), - ); - - events.value.push(newEvent); - setStackValue(nextStackValue); - - const interval = setInterval(() => { - const nextStackValue = aggregate(events.value, new Date().getTime()); - - if (!isEqual(stack.value, nextStackValue)) { - setStackValue(nextStackValue); - } - - if (nextStackValue.globalTransitionState === "idle") { - clearInterval(interval); - } - }, INTERVAL_MS); - }; - - type TriggerPreEffectHooksInput = - K extends DomainEvent["name"] - ? { - actionName: K; - actionParams: Omit< - Extract, - keyof BaseDomainEvent - >; - pluginInstances: ReturnType[]; - } - : unknown; - - function triggerPreEffectHooks( - input: TriggerPreEffectHooksInput, - ): { - isPrevented: boolean; - nextActionParams: (typeof input)["actionParams"]; - } { - switch (input.actionName) { - case "Pushed": { - let isPrevented = false; - - let nextActionParams = { - ...input.actionParams, - }; - - const preventDefault = () => { - isPrevented = true; - }; - - const overrideActionParams = ( - partialActionParams: typeof input.actionParams, - ) => { - nextActionParams = { - ...nextActionParams, - ...partialActionParams, - }; - }; - - for (const pluginInstance of input.pluginInstances) { - pluginInstance.onBeforePush?.({ - actionParams: { - ...nextActionParams, - }, - actions: { - ...actions, - preventDefault, - overrideActionParams, - }, - }); - } - - return { - isPrevented, - nextActionParams, - }; - } - case "Replaced": { - let isPrevented = false; - - let nextActionParams = { - ...input.actionParams, - }; - - const preventDefault = () => { - isPrevented = true; - }; - - const overrideActionParams = ( - partialActionParams: typeof input.actionParams, - ) => { - nextActionParams = { - ...nextActionParams, - ...partialActionParams, - }; - }; - - for (const pluginInstance of input.pluginInstances) { - pluginInstance.onBeforeReplace?.({ - actionParams: { - ...nextActionParams, - }, - actions: { - ...actions, - preventDefault, - overrideActionParams, - }, - }); - } - - return { - isPrevented, - nextActionParams, - }; - } - case "Popped": { - let isPrevented = false; - - let nextActionParams = { - ...input.actionParams, - }; - - const preventDefault = () => { - isPrevented = true; - }; - - const overrideActionParams = ( - partialActionParams: typeof input.actionParams, - ) => { - nextActionParams = { - ...nextActionParams, - ...partialActionParams, - }; - }; - - for (const pluginInstance of input.pluginInstances) { - pluginInstance.onBeforePop?.({ - actionParams: { - ...nextActionParams, - }, - actions: { - ...actions, - preventDefault, - overrideActionParams, - }, - }); - } - - return { - isPrevented, - nextActionParams, - }; - } - case "StepPushed": { - let isPrevented = false; - - let nextActionParams = { - ...input.actionParams, - }; - - const preventDefault = () => { - isPrevented = true; - }; - - const overrideActionParams = ( - partialActionParams: typeof input.actionParams, - ) => { - nextActionParams = { - ...nextActionParams, - ...partialActionParams, - }; - }; - - for (const pluginInstance of input.pluginInstances) { - pluginInstance.onBeforeStepPush?.({ - actionParams: { - ...nextActionParams, - }, - actions: { - ...actions, - preventDefault, - overrideActionParams, - }, - }); - } - - return { - isPrevented, - nextActionParams, - }; - } - case "StepReplaced": { - let isPrevented = false; - - let nextActionParams = { - ...input.actionParams, - }; - - const preventDefault = () => { - isPrevented = true; - }; - - const overrideActionParams = ( - partialActionParams: typeof input.actionParams, - ) => { - nextActionParams = { - ...nextActionParams, - ...partialActionParams, - }; - }; - - for (const pluginInstance of input.pluginInstances) { - pluginInstance.onBeforeStepReplace?.({ - actionParams: { - ...nextActionParams, - }, - actions: { - ...actions, - preventDefault, - overrideActionParams, - }, - }); - } - - return { - isPrevented, - nextActionParams, - }; - } - case "StepPopped": { - let isPrevented = false; - - let nextActionParams = { - ...input.actionParams, - }; - - const preventDefault = () => { - isPrevented = true; - }; - - const overrideActionParams = ( - partialActionParams: typeof input.actionParams, - ) => { - nextActionParams = { - ...nextActionParams, - ...partialActionParams, - }; - }; - - for (const pluginInstance of input.pluginInstances) { - pluginInstance.onBeforeStepPop?.({ - actionParams: { - ...nextActionParams, - }, - actions: { - ...actions, - preventDefault, - overrideActionParams, - }, - }); - } - - return { - isPrevented, - nextActionParams, - }; - } - case "Paused": { - let isPrevented = false; - - let nextActionParams = { - ...input.actionParams, - }; - - const preventDefault = () => { - isPrevented = true; - }; - - const overrideActionParams = ( - partialActionParams: typeof input.actionParams, - ) => { - nextActionParams = { - ...nextActionParams, - ...partialActionParams, - }; - }; - - for (const pluginInstance of input.pluginInstances) { - pluginInstance.onBeforePause?.({ - actionParams: { - ...nextActionParams, - }, - actions: { - ...actions, - preventDefault, - overrideActionParams, - }, - }); - } - - return { - isPrevented, - nextActionParams, - }; - } - case "Resumed": { - let isPrevented = false; - - let nextActionParams = { - ...input.actionParams, - }; - - const preventDefault = () => { - isPrevented = true; - }; - - const overrideActionParams = ( - partialActionParams: typeof input.actionParams, - ) => { - nextActionParams = { - ...nextActionParams, - ...partialActionParams, - }; - }; - - for (const pluginInstance of input.pluginInstances) { - pluginInstance.onBeforeResume?.({ - actionParams: { - ...nextActionParams, - }, - actions: { - ...actions, - preventDefault, - overrideActionParams, - }, - }); - } - - return { - isPrevented, - nextActionParams, - }; - } - default: { - return { - isPrevented: false, - nextActionParams: input.actionParams, - }; - } - } - } - - function triggerPostEffectHooks( - effects: Effect[], - plugins: ReturnType[], - ) { - effects.forEach((effect) => { - plugins.forEach((plugin) => { - switch (effect._TAG) { - case "PUSHED": - return plugin.onPushed?.({ - actions, - effect, - }); - case "REPLACED": - return plugin.onReplaced?.({ - actions, - effect, - }); - case "POPPED": - return plugin.onPopped?.({ - actions, - effect, - }); - case "STEP_PUSHED": - return plugin.onStepPushed?.({ - actions, - effect, - }); - case "STEP_REPLACED": - return plugin.onStepReplaced?.({ - actions, - effect, - }); - case "STEP_POPPED": - return plugin.onStepPopped?.({ - actions, - effect, - }); - case "PAUSED": - return plugin.onPaused?.({ - actions, - effect, - }); - case "RESUMED": - return plugin.onResumed?.({ - actions, - effect, - }); - case "%SOMETHING_CHANGED%": - return plugin.onChanged?.({ - actions, - effect, - }); - default: - return undefined; - } - }); - }); - } - const actions: StackflowActions = { getStack() { return stack.value; }, - dispatchEvent, - push(params) { - const { isPrevented, nextActionParams } = triggerPreEffectHooks({ - actionName: "Pushed", - actionParams: params, - pluginInstances, - }); + dispatchEvent(name, params) { + const newEvent = makeEvent(name, params); + const nextStackValue = aggregate( + [...events.value, newEvent], + new Date().getTime(), + ); - if (isPrevented) { - return; - } - - dispatchEvent("Pushed", nextActionParams); - }, - replace(params) { - const { isPrevented, nextActionParams } = triggerPreEffectHooks({ - actionName: "Replaced", - actionParams: params, - pluginInstances, - }); + events.value.push(newEvent); + setStackValue(nextStackValue); - if (isPrevented) { - return; - } + const interval = setInterval(() => { + const nextStackValue = aggregate(events.value, new Date().getTime()); - dispatchEvent("Replaced", nextActionParams); - }, - pop(params) { - const { isPrevented, nextActionParams } = triggerPreEffectHooks({ - actionName: "Popped", - actionParams: params ?? {}, - pluginInstances, - }); - - if (isPrevented) { - return; - } - - dispatchEvent("Popped", nextActionParams); - }, - stepPush(params) { - const { isPrevented, nextActionParams } = triggerPreEffectHooks({ - actionName: "StepPushed", - actionParams: params, - pluginInstances, - }); - - if (isPrevented) { - return; - } - - dispatchEvent("StepPushed", nextActionParams); - }, - stepReplace(params) { - const { isPrevented, nextActionParams } = triggerPreEffectHooks({ - actionName: "StepReplaced", - actionParams: params, - pluginInstances, - }); - - if (isPrevented) { - return; - } - - dispatchEvent("StepReplaced", nextActionParams); - }, - stepPop(params) { - const { isPrevented, nextActionParams } = triggerPreEffectHooks({ - actionName: "StepPopped", - actionParams: params ?? {}, - pluginInstances, - }); - - if (isPrevented) { - return; - } - - dispatchEvent("StepPopped", nextActionParams); - }, - pause(params) { - const { isPrevented, nextActionParams } = triggerPreEffectHooks({ - actionName: "Paused", - actionParams: params ?? {}, - pluginInstances, - }); - - if (isPrevented) { - return; - } + if (!isEqual(stack.value, nextStackValue)) { + setStackValue(nextStackValue); + } - dispatchEvent("Paused", nextActionParams); + if (nextStackValue.globalTransitionState === "idle") { + clearInterval(interval); + } + }, INTERVAL_MS); }, - resume(params) { - const { isPrevented, nextActionParams } = triggerPreEffectHooks({ - actionName: "Resumed", - actionParams: params ?? {}, - pluginInstances, - }); - - if (isPrevented) { - return; - } + push: () => {}, + replace: () => {}, + pop: () => {}, + stepPush: () => {}, + stepReplace: () => {}, + stepPop: () => {}, + pause: () => {}, + resume: () => {}, + }; - dispatchEvent("Resumed", nextActionParams); - }, + const setStackValue = (nextStackValue: Stack) => { + const effects = produceEffects(stack.value, nextStackValue); + stack.value = nextStackValue; + triggerPostEffectHooks(effects, pluginInstances, actions); }; + // Initialize action methods after actions object is fully created + Object.assign( + actions, + makeActions({ + dispatchEvent: actions.dispatchEvent, + pluginInstances, + actions, + }), + ); + return { actions, init: once(() => { diff --git a/core/src/utils/makeActions.ts b/core/src/utils/makeActions.ts new file mode 100644 index 00000000..7360ad02 --- /dev/null +++ b/core/src/utils/makeActions.ts @@ -0,0 +1,130 @@ +import type { StackflowActions } from "../interfaces"; +import type { StackflowPlugin } from "../interfaces"; +import { triggerPreEffectHook } from "./triggerPreEffectHooks"; + +type ActionCreatorOptions = { + dispatchEvent: StackflowActions["dispatchEvent"]; + pluginInstances: ReturnType[]; + actions: StackflowActions; +}; + +export function makeActions({ + dispatchEvent, + pluginInstances, + actions, +}: ActionCreatorOptions): Omit { + return { + push(params) { + const { isPrevented, nextActionParams } = triggerPreEffectHook( + "Pushed", + params, + pluginInstances, + actions, + ); + + if (isPrevented) { + return; + } + + dispatchEvent("Pushed", nextActionParams); + }, + replace(params) { + const { isPrevented, nextActionParams } = triggerPreEffectHook( + "Replaced", + params, + pluginInstances, + actions, + ); + + if (isPrevented) { + return; + } + + dispatchEvent("Replaced", nextActionParams); + }, + pop(params = {}) { + const { isPrevented, nextActionParams } = triggerPreEffectHook( + "Popped", + params, + pluginInstances, + actions, + ); + + if (isPrevented) { + return; + } + + dispatchEvent("Popped", nextActionParams); + }, + stepPush(params) { + const { isPrevented, nextActionParams } = triggerPreEffectHook( + "StepPushed", + params, + pluginInstances, + actions, + ); + + if (isPrevented) { + return; + } + + dispatchEvent("StepPushed", nextActionParams); + }, + stepReplace(params) { + const { isPrevented, nextActionParams } = triggerPreEffectHook( + "StepReplaced", + params, + pluginInstances, + actions, + ); + + if (isPrevented) { + return; + } + + dispatchEvent("StepReplaced", nextActionParams); + }, + stepPop(params = {}) { + const { isPrevented, nextActionParams } = triggerPreEffectHook( + "StepPopped", + params, + pluginInstances, + actions, + ); + + if (isPrevented) { + return; + } + + dispatchEvent("StepPopped", nextActionParams); + }, + pause(params = {}) { + const { isPrevented, nextActionParams } = triggerPreEffectHook( + "Paused", + params, + pluginInstances, + actions, + ); + + if (isPrevented) { + return; + } + + dispatchEvent("Paused", nextActionParams); + }, + resume(params = {}) { + const { isPrevented, nextActionParams } = triggerPreEffectHook( + "Resumed", + params, + pluginInstances, + actions, + ); + + if (isPrevented) { + return; + } + + dispatchEvent("Resumed", nextActionParams); + }, + }; +} diff --git a/core/src/utils/triggerPostEffectHooks.ts b/core/src/utils/triggerPostEffectHooks.ts new file mode 100644 index 00000000..c425cd0b --- /dev/null +++ b/core/src/utils/triggerPostEffectHooks.ts @@ -0,0 +1,42 @@ +import type { Effect } from "../Effect"; +import type { StackflowActions, StackflowPlugin } from "../interfaces"; + +export function triggerPostEffectHooks( + effects: Effect[], + plugins: ReturnType[], + actions: StackflowActions, +): void { + effects.forEach((effect) => { + plugins.forEach((plugin) => { + switch (effect._TAG) { + case "PUSHED": + plugin.onPushed?.({ actions, effect }); + break; + case "REPLACED": + plugin.onReplaced?.({ actions, effect }); + break; + case "POPPED": + plugin.onPopped?.({ actions, effect }); + break; + case "STEP_PUSHED": + plugin.onStepPushed?.({ actions, effect }); + break; + case "STEP_REPLACED": + plugin.onStepReplaced?.({ actions, effect }); + break; + case "STEP_POPPED": + plugin.onStepPopped?.({ actions, effect }); + break; + case "PAUSED": + plugin.onPaused?.({ actions, effect }); + break; + case "RESUMED": + plugin.onResumed?.({ actions, effect }); + break; + case "%SOMETHING_CHANGED%": + plugin.onChanged?.({ actions, effect }); + break; + } + }); + }); +} diff --git a/core/src/utils/triggerPreEffectHooks.ts b/core/src/utils/triggerPreEffectHooks.ts new file mode 100644 index 00000000..5755ad62 --- /dev/null +++ b/core/src/utils/triggerPreEffectHooks.ts @@ -0,0 +1,69 @@ +import type { DomainEvent } from "../event-types"; +import type { BaseDomainEvent } from "../event-types/_base"; +import type { StackflowPlugin } from "../interfaces"; +import type { StackflowActions } from "../interfaces"; +import type { StackflowPluginPreEffectHook } from "../interfaces/StackflowPluginHook"; + +type PreEffectHookResult = { + isPrevented: boolean; + nextActionParams: T; +}; + +type EventNameToParams = Omit< + Extract, + keyof BaseDomainEvent +>; + +const PLUGIN_HOOK_MAP = { + Pushed: "onBeforePush", + Replaced: "onBeforeReplace", + Popped: "onBeforePop", + StepPushed: "onBeforeStepPush", + StepReplaced: "onBeforeStepReplace", + StepPopped: "onBeforeStepPop", + Paused: "onBeforePause", + Resumed: "onBeforeResume", +} as const; + +type ActionName = Exclude< + DomainEvent["name"], + "Initialized" | "ActivityRegistered" +>; + +export function triggerPreEffectHook( + actionName: K, + actionParams: EventNameToParams, + pluginInstances: ReturnType[], + actions: StackflowActions, +): PreEffectHookResult> { + let isPrevented = false; + let nextActionParams = { ...actionParams }; + + for (const pluginInstance of pluginInstances) { + const hook = pluginInstance[PLUGIN_HOOK_MAP[actionName]] as + | StackflowPluginPreEffectHook> + | undefined; + if (hook) { + hook({ + actionParams: { ...nextActionParams }, + actions: { + ...actions, + preventDefault: () => { + isPrevented = true; + }, + overrideActionParams: (partialActionParams: EventNameToParams) => { + nextActionParams = { + ...nextActionParams, + ...partialActionParams, + }; + }, + }, + }); + } + } + + return { + isPrevented, + nextActionParams, + }; +}