diff --git a/core/src/Stack.ts b/core/src/Stack.ts index 3dbb5eea1..972f1f0de 100644 --- a/core/src/Stack.ts +++ b/core/src/Stack.ts @@ -1,9 +1,11 @@ import type { BaseDomainEvent } from "event-types/_base"; import type { DomainEvent, + PausedEvent, PoppedEvent, PushedEvent, ReplacedEvent, + ResumedEvent, StepPoppedEvent, StepPushedEvent, StepReplacedEvent, @@ -35,6 +37,7 @@ export type Activity = { context?: {}; enteredBy: PushedEvent | ReplacedEvent; exitedBy?: ReplacedEvent | PoppedEvent; + resumedBy?: ResumedEvent; steps: ActivityStep[]; isTop: boolean; isActive: boolean; diff --git a/core/src/activity-utils/findTargetActivityIndices.ts b/core/src/activity-utils/findTargetActivityIndices.ts index 52e277693..77ca85ef6 100644 --- a/core/src/activity-utils/findTargetActivityIndices.ts +++ b/core/src/activity-utils/findTargetActivityIndices.ts @@ -105,6 +105,17 @@ export function findTargetActivityIndices( break; } + case "Resumed": + case "Paused": { + const activity = activities.find( + (activity) => activity.id === event.activityId, + ); + + if (activity) { + targetActivities.push(activities.indexOf(activity)); + } + break; + } default: break; } diff --git a/core/src/activity-utils/makeActivityReducer.ts b/core/src/activity-utils/makeActivityReducer.ts index 11a96a3d0..a6524137b 100644 --- a/core/src/activity-utils/makeActivityReducer.ts +++ b/core/src/activity-utils/makeActivityReducer.ts @@ -1,8 +1,10 @@ import type { Activity, ActivityTransitionState } from "../Stack"; import type { DomainEvent, + PausedEvent, PoppedEvent, ReplacedEvent, + ResumedEvent, StepPoppedEvent, StepPushedEvent, StepReplacedEvent, @@ -124,7 +126,30 @@ export function makeActivityReducer(context: { Initialized: noop, ActivityRegistered: noop, Pushed: noop, - Paused: noop, - Resumed: noop, + Paused: (activity: Activity, event: PausedEvent): Activity => { + if (activity.exitedBy || activity.resumedBy) { + return activity; + } + + return { + ...activity, + transitionState: "enter-active", + }; + }, + Resumed: (activity: Activity, event: ResumedEvent): Activity => { + if (activity.exitedBy || activity.resumedBy) { + return activity; + } + + const isTransitionDone = + context.now - (context.resumedAt ?? event.eventDate) >= + context.transitionDuration; + + return { + ...activity, + transitionState: isTransitionDone ? "enter-done" : "enter-active", + resumedBy: event, + }; + }, } as const); } diff --git a/core/src/activity-utils/makeStackReducer.ts b/core/src/activity-utils/makeStackReducer.ts index dd957e78c..4b51a3d2b 100644 --- a/core/src/activity-utils/makeStackReducer.ts +++ b/core/src/activity-utils/makeStackReducer.ts @@ -11,6 +11,23 @@ import { makeActivitiesReducer } from "./makeActivitiesReducer"; import { makeActivityReducer } from "./makeActivityReducer"; import { makeReducer } from "./makeReducer"; +function calculateGlobalTransitionState( + activities: Activity[], + currentState: Stack["globalTransitionState"], +): Stack["globalTransitionState"] { + if (currentState === "paused") { + return "paused"; + } + + const hasActiveTransition = activities.some( + (activity) => + activity.transitionState === "enter-active" || + activity.transitionState === "exit-active", + ); + + return hasActiveTransition ? "loading" : "idle"; +} + function withPauseReducer( reducer: (stack: Stack, event: T) => Stack, ) { @@ -63,20 +80,25 @@ function withActivitiesReducer( ); } - const isLoading = activities.find( - (activity) => - activity.transitionState === "enter-active" || - activity.transitionState === "exit-active", + const globalTransitionState = calculateGlobalTransitionState( + activities, + stack.globalTransitionState, ); - const globalTransitionState = - stack.globalTransitionState === "paused" - ? "paused" - : isLoading - ? "loading" - : "idle"; + const result = reducer( + { ...stack, activities, globalTransitionState }, + event, + ); + + const updatedGlobalTransitionState = calculateGlobalTransitionState( + result.activities, + result.globalTransitionState, + ); - return reducer({ ...stack, activities, globalTransitionState }, event); + return { + ...result, + globalTransitionState: updatedGlobalTransitionState, + }; }; } @@ -123,6 +145,7 @@ export function makeStackReducer(context: { return { ...stack, globalTransitionState: "paused", + pausedEvents: stack.pausedEvents ?? [], }; }, context), ), diff --git a/core/src/aggregate.spec.ts b/core/src/aggregate.spec.ts index 466971d82..3b16493ed 100644 --- a/core/src/aggregate.spec.ts +++ b/core/src/aggregate.spec.ts @@ -4088,6 +4088,78 @@ test("aggregate - Pause되면 이벤트가 반영되지 않고, globalTransition }); }); +test("aggregate - PausedEvent inserts source activity entering event into pausedEvents", () => { + let pushedEvent1: PushedEvent; + let pushedEvent2: PushedEvent; + + const t = nowTime(); + + const events = [ + initializedEvent({ + transitionDuration: 300, + }), + registeredEvent({ + activityName: "a", + }), + registeredEvent({ + activityName: "b", + }), + (pushedEvent1 = makeEvent("Pushed", { + activityId: "activity-1", + activityName: "a", + eventDate: enoughPastTime(), + activityParams: {}, + })), + (pushedEvent2 = makeEvent("Pushed", { + activityId: "activity-2", + activityName: "b", + activityParams: {}, + eventDate: enoughPastTime(), + })), + makeEvent("Paused", { + eventDate: t - 150, + activityId: "activity-2", + }), + ]; + + const output = aggregate(events, t); + + expect(output).toStrictEqual({ + activities: [ + activity({ + id: "activity-1", + name: "a", + transitionState: "enter-done", + params: {}, + steps: [ + { + id: "activity-1", + params: {}, + enteredBy: pushedEvent1, + zIndex: 0, + }, + ], + enteredBy: pushedEvent1, + isActive: true, + isTop: true, + isRoot: true, + zIndex: 0, + }), + ], + registeredActivities: [ + { + name: "a", + }, + { + name: "b", + }, + ], + transitionDuration: 300, + globalTransitionState: "paused", + pausedEvents: [pushedEvent2], + }); +}); + test("aggregate - Resumed 되면 해당 시간 이후로 Transition이 정상작동합니다", () => { let pushedEvent1: PushedEvent; let pushedEvent2: PushedEvent; @@ -4109,6 +4181,7 @@ test("aggregate - Resumed 되면 해당 시간 이후로 Transition이 정상작 activityParams: {}, })), makeEvent("Paused", { + activityId: "activity-1", eventDate: enoughPastTime(), }), (pushedEvent2 = makeEvent("Pushed", { @@ -4118,6 +4191,7 @@ test("aggregate - Resumed 되면 해당 시간 이후로 Transition이 정상작 activityParams: {}, })), makeEvent("Resumed", { + activityId: "activity-1", eventDate: nowTime() - 150, }), ]; diff --git a/core/src/event-types/PausedEvent.ts b/core/src/event-types/PausedEvent.ts index af6c1e9ca..cd8761f35 100644 --- a/core/src/event-types/PausedEvent.ts +++ b/core/src/event-types/PausedEvent.ts @@ -1,3 +1,8 @@ import type { BaseDomainEvent } from "./_base"; -export type PausedEvent = BaseDomainEvent<"Paused", {}>; +export type PausedEvent = BaseDomainEvent< + "Paused", + { + activityId: string; + } +>; diff --git a/core/src/event-types/ResumedEvent.ts b/core/src/event-types/ResumedEvent.ts index a19839e92..566337cf4 100644 --- a/core/src/event-types/ResumedEvent.ts +++ b/core/src/event-types/ResumedEvent.ts @@ -1,3 +1,8 @@ import type { BaseDomainEvent } from "./_base"; -export type ResumedEvent = BaseDomainEvent<"Resumed", {}>; +export type ResumedEvent = BaseDomainEvent< + "Resumed", + { + activityId: string; + } +>; diff --git a/integrations/react/src/__internal__/suspensePlugin.tsx b/integrations/react/src/__internal__/suspensePlugin.tsx new file mode 100644 index 000000000..9fbf30f0e --- /dev/null +++ b/integrations/react/src/__internal__/suspensePlugin.tsx @@ -0,0 +1,30 @@ +import { Suspense, useEffect } from "react"; +import type { StackflowReactPlugin } from "./StackflowReactPlugin"; +import { useCoreActions } from "./core"; + +export function suspensePlugin(): StackflowReactPlugin { + return () => ({ + key: "plugin-suspense", + wrapActivity: ({ activity }) => { + return ( + }>{activity.render()} + ); + }, + }); +} + +export function SuspenseFallback() { + const { pause, resume } = useCoreActions(); + + useEffect(() => { + console.log("lets pause"); + pause(); + + return () => { + console.log("lets resume"); + resume(); + }; + }, []); + + return null; +} diff --git a/integrations/react/src/future/loader/loaderPlugin.tsx b/integrations/react/src/future/loader/loaderPlugin.tsx index 744768db8..703e8902a 100644 --- a/integrations/react/src/future/loader/loaderPlugin.tsx +++ b/integrations/react/src/future/loader/loaderPlugin.tsx @@ -84,10 +84,7 @@ function createBeforeRouteHandler< [activityName in RegisteredActivityName]: ActivityComponentType; }, >(input: StackflowInput): OnBeforeRoute { - return ({ - actionParams, - actions: { overrideActionParams, pause, resume }, - }) => { + return ({ actionParams, actions: { overrideActionParams } }) => { const { activityName, activityParams, activityContext } = actionParams; const matchActivity = input.config.activities.find( @@ -106,28 +103,15 @@ function createBeforeRouteHandler< const loaderDataPromise = loaderData instanceof Promise ? loaderData : undefined; - const lazyComponentPromise = - "_load" in matchActivityComponent - ? matchActivityComponent._load?.() - : undefined; - if (loaderDataPromise || lazyComponentPromise) { - pause(); - } - Promise.allSettled([loaderDataPromise, lazyComponentPromise]) - .then(([loaderDataPromiseResult, lazyComponentPromiseResult]) => { + Promise.allSettled([loaderDataPromise]).then( + ([loaderDataPromiseResult]) => { printLoaderDataPromiseError({ promiseResult: loaderDataPromiseResult, activityName: matchActivity.name, }); - printLazyComponentPromiseError({ - promiseResult: lazyComponentPromiseResult, - activityName: matchActivity.name, - }); - }) - .finally(() => { - resume(); - }); + }, + ); overrideActionParams({ ...actionParams, @@ -153,18 +137,3 @@ function printLoaderDataPromiseError({ ); } } - -function printLazyComponentPromiseError({ - promiseResult, - activityName, -}: { - promiseResult: PromiseSettledResult; - activityName: string; -}) { - if (promiseResult.status === "rejected") { - console.error(promiseResult.reason); - console.error( - `The above error occurred while loading a lazy react component of the "${activityName}" activity`, - ); - } -} diff --git a/integrations/react/src/future/stackflow.tsx b/integrations/react/src/future/stackflow.tsx index a5807619d..89759c293 100644 --- a/integrations/react/src/future/stackflow.tsx +++ b/integrations/react/src/future/stackflow.tsx @@ -16,6 +16,7 @@ import MainRenderer from "../__internal__/MainRenderer"; import { makeActivityId } from "../__internal__/activity"; import { CoreProvider } from "../__internal__/core"; import { PluginsProvider } from "../__internal__/plugins"; +import { suspensePlugin } from "../__internal__/suspensePlugin"; import { isBrowser, makeRef } from "../__internal__/utils"; import type { StackflowReactPlugin } from "../stable"; import type { Actions } from "./Actions"; @@ -57,11 +58,8 @@ export function stackflow< ...(input.plugins ?? []) .flat(Number.POSITIVE_INFINITY as 0) .map((p) => p as StackflowReactPlugin), - - /** - * `loaderPlugin()` must be placed after `historySyncPlugin()` - */ loaderPlugin(input), + suspensePlugin(), ]; const enoughPastTime = () =>