Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core, react): Supports dynamic import for activities, and delays transition effects while loading an activity or waiting for a loader response #542

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c88297b
feat(core): add `PausedEvent`, `ResumedEvent`
tonyfromundefined Dec 3, 2024
5fc1b71
remove console
tonyfromundefined Dec 3, 2024
b467cba
fix event imports
tonyfromundefined Dec 3, 2024
bd4f62a
paused when loader loaded
tonyfromundefined Dec 4, 2024
b8befab
refactor(core): `triggerPreEffectHook`
tonyfromundefined Dec 10, 2024
2e6aeca
feat(react): add `{ load: ... }` definition on react components map
tonyfromundefined Dec 10, 2024
124f9fc
feat: fix re-rendering issue
tonyfromundefined Dec 10, 2024
68f550a
rollback
tonyfromundefined Dec 10, 2024
9f27452
remove logging
tonyfromundefined Dec 10, 2024
7804fba
remove unused vars
tonyfromundefined Dec 10, 2024
c289849
replace
tonyfromundefined Dec 10, 2024
b564ed4
changeset
tonyfromundefined Dec 10, 2024
c4fc1a9
refactor: `load` -> `lazy`
tonyfromundefined Dec 11, 2024
ea9a253
fix: add `lazy()` function
tonyfromundefined Dec 13, 2024
54cf79e
refactor: remove delay for testing
tonyfromundefined Dec 18, 2024
f412abc
fix: add promise cache
tonyfromundefined Dec 18, 2024
361e6a8
feat(core): `Paused`, `Resumed` effect
tonyfromundefined Dec 24, 2024
b6314c4
WIP
tonyfromundefined Dec 24, 2024
db719fc
remove
tonyfromundefined Dec 26, 2024
f3350a6
fix
tonyfromundefined Dec 26, 2024
7d0a3e3
refactor
tonyfromundefined Dec 26, 2024
27e37a0
WIP
tonyfromundefined Dec 26, 2024
8aa5b37
fix
tonyfromundefined Dec 26, 2024
aff5713
pass all tests
tonyfromundefined Dec 26, 2024
7e64889
refactor(core): renaming
tonyfromundefined Dec 26, 2024
17e6e41
fix
tonyfromundefined Dec 26, 2024
761b26d
refactor
tonyfromundefined Dec 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/chilled-hats-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@stackflow/plugin-basic-ui": minor
"@stackflow/react": minor
"@stackflow/core": minor
---

Supports dynamic import for activities, and delays transition effects while loading an activity or waiting for a loader response
2 changes: 1 addition & 1 deletion core/src/Stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,5 @@ export type Stack = {
activities: Activity[];
registeredActivities: RegisteredActivity[];
transitionDuration: number;
globalTransitionState: "idle" | "loading";
globalTransitionState: "idle" | "loading" | "paused";
};
12 changes: 12 additions & 0 deletions core/src/activity-utils/makeActivitiesReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import type {
ActivityRegisteredEvent,
DomainEvent,
InitializedEvent,
PausedEvent,
PoppedEvent,
PushedEvent,
ReplacedEvent,
ResumedEvent,
StepPoppedEvent,
StepPushedEvent,
StepReplacedEvent,
Expand Down Expand Up @@ -89,4 +91,14 @@ export const makeActivitiesReducers = (isTransitionDone: boolean) =>
*/
StepPopped: (activities: Activity[], event: StepPoppedEvent): Activity[] =>
activities,

/**
* noop
*/
Paused: (activities: Activity[], event: PausedEvent) => activities,

/**
* noop
*/
Resumed: (activities: Activity[], event: ResumedEvent) => activities,
});
10 changes: 10 additions & 0 deletions core/src/activity-utils/makeActivityReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,14 @@ export const makeActivityReducers = (isTransitionDone: boolean) =>
params: beforeActivityParams ?? activity.params,
};
},

/**
* noop
*/
Paused: (activity: Activity) => activity,

/**
* noop
*/
Resumed: (activity: Activity) => activity,
} as const);
152 changes: 152 additions & 0 deletions core/src/aggregate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3931,3 +3931,155 @@ test("aggregate - StepPushedEvent must be ignored when top activity is not targe
globalTransitionState: "idle",
});
});

test("aggregate - Pause되면 이벤트가 반영되지 않고, globalTransitionState를 paused으로 바꿉니다", () => {
let pushedEvent1: PushedEvent;
let pushedEvent2: PushedEvent;

const events = [
initializedEvent({
transitionDuration: 300,
}),
registeredEvent({
activityName: "a",
}),
registeredEvent({
activityName: "b",
}),
(pushedEvent1 = makeEvent("Pushed", {
activityId: "activity-1",
activityName: "a",
eventDate: enoughPastTime(),
activityParams: {},
})),
makeEvent("Paused", {}),
(pushedEvent2 = makeEvent("Pushed", {
activityId: "activity-2",
activityName: "b",
activityParams: {},
})),
];

const output = aggregate(events, nowTime());

expect(output).toStrictEqual({
activities: [
activity({
id: "activity-1",
name: "a",
transitionState: "enter-done",
params: {},
steps: [
{
id: "activity-1",
params: {},
enteredBy: pushedEvent1,
},
],
enteredBy: pushedEvent1,
isActive: true,
isTop: true,
isRoot: true,
zIndex: 0,
}),
],
registeredActivities: [
{
name: "a",
},
{
name: "b",
},
],
transitionDuration: 300,
globalTransitionState: "paused",
});
});

test("aggregate - Resumed 되면 해당 시간 이후로 Transition이 정상작동합니다", () => {
let pushedEvent1: PushedEvent;
let pushedEvent2: PushedEvent;

const events = [
initializedEvent({
transitionDuration: 300,
}),
registeredEvent({
activityName: "a",
}),
registeredEvent({
activityName: "b",
}),
(pushedEvent1 = makeEvent("Pushed", {
activityId: "activity-1",
activityName: "a",
eventDate: enoughPastTime(),
activityParams: {},
})),
makeEvent("Paused", {
eventDate: enoughPastTime(),
}),
(pushedEvent2 = makeEvent("Pushed", {
activityId: "activity-2",
activityName: "b",
eventDate: enoughPastTime(),
activityParams: {},
})),
makeEvent("Resumed", {
eventDate: nowTime() - 150,
}),
];

const output = aggregate(events, nowTime());

expect(output).toStrictEqual({
activities: [
activity({
id: "activity-1",
name: "a",
transitionState: "enter-done",
params: {},
steps: [
{
id: "activity-1",
params: {},
enteredBy: expect.anything(),
},
],
enteredBy: expect.anything(),
isActive: false,
isTop: false,
isRoot: true,
zIndex: 0,
}),
activity({
id: "activity-2",
name: "b",
transitionState: "enter-active",
params: {},
steps: [
{
id: "activity-2",
params: {},
enteredBy: expect.anything(),
},
],
enteredBy: expect.anything(),
isActive: true,
isTop: true,
isRoot: false,
zIndex: 1,
}),
],
registeredActivities: [
{
name: "a",
},
{
name: "b",
},
],
transitionDuration: 300,
globalTransitionState: "loading",
});
});
80 changes: 66 additions & 14 deletions core/src/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,69 @@ import type { DomainEvent } from "./event-types";
import { filterEvents, validateEvents } from "./event-utils";
import { compareBy, uniqBy } from "./utils";

export function aggregate(events: DomainEvent[], now: number): Stack {
export function aggregate(inputEvents: DomainEvent[], now: number): Stack {
/**
* 1. Sorting by event ID (K-Sortable)
*
* The backward action may cause past events to be put behind, so when those events are dispatched, move them forward.
*/
const sortedEvents = uniqBy(
[...events].sort((a, b) => compareBy(a, b, (e) => e.id)),
[...inputEvents].sort((a, b) => compareBy(a, b, (e) => e.id)),
(e) => e.id,
);

validateEvents(sortedEvents);

const initEvent = filterEvents(sortedEvents, "Initialized")[0];
const activityRegisteredEvents = filterEvents(events, "ActivityRegistered");
/**
* 2. Handle `PausedEvent` and `ResumedEvent`
*
* 2-1. If a `PausedEvent` is fired, handle it so that all events after the pause are deleted
* (Transitions of events that have already happened will ensure normal behavior)
* 2-2. If a `ResumedEvent` is fired, handle it by delaying the `eventCreatedAt` of events
* after the pause by that much (`dt`)
*/
const pauseAndResumeHandledEvents: DomainEvent[] = [];
let eventBufferAfterPaused: DomainEvent[] = [];

let pausedAt: number | null = null;

for (const event of sortedEvents) {
if (event.name === "Paused") {
pausedAt = event.eventDate;
continue;
}
if (event.name === "Resumed" && pausedAt) {
const dt = event.eventDate - pausedAt;

for (const pausedEvent of eventBufferAfterPaused) {
pauseAndResumeHandledEvents.push({
...pausedEvent,
eventDate: pausedEvent.eventDate + dt,
});
}

pausedAt = null;
eventBufferAfterPaused = [];
continue;
}

if (pausedAt) {
eventBufferAfterPaused.push(event);
} else {
pauseAndResumeHandledEvents.push(event);
}
}

const events = pauseAndResumeHandledEvents;

validateEvents(events);

const initEvent = filterEvents(events, "Initialized")[0];
const activityRegisteredEvents = filterEvents(
inputEvents,
"ActivityRegistered",
);
tonyfromundefined marked this conversation as resolved.
Show resolved Hide resolved
const { transitionDuration } = initEvent;

const activities = sortedEvents.reduce(
const activities = events.reduce(
(activities: Activity[], event: DomainEvent) => {
const isTransitionDone = now - event.eventDate >= transitionDuration;

Expand Down Expand Up @@ -63,13 +113,15 @@ export function aggregate(events: DomainEvent[], now: number): Stack {
const lastVisibleActivity = visibleActivities[visibleActivities.length - 1];
const lastEnteredActivity = enteredActivities[enteredActivities.length - 1];

const globalTransitionState = activities.find(
(activity) =>
activity.transitionState === "enter-active" ||
activity.transitionState === "exit-active",
)
? "loading"
: "idle";
const globalTransitionState = pausedAt
? "paused"
: activities.find(
(activity) =>
activity.transitionState === "enter-active" ||
activity.transitionState === "exit-active",
)
? "loading"
: "idle";

const output: Stack = {
activities: uniqActivities
Expand Down
3 changes: 3 additions & 0 deletions core/src/event-types/PausedEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { BaseDomainEvent } from "./_base";

export type PausedEvent = BaseDomainEvent<"Paused", {}>;
3 changes: 3 additions & 0 deletions core/src/event-types/ResumedEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { BaseDomainEvent } from "./_base";

export type ResumedEvent = BaseDomainEvent<"Resumed", {}>;
8 changes: 7 additions & 1 deletion core/src/event-types/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { ActivityRegisteredEvent } from "./ActivityRegisteredEvent";
import type { InitializedEvent } from "./InitializedEvent";
import type { PausedEvent } from "./PausedEvent";
import type { PoppedEvent } from "./PoppedEvent";
import type { PushedEvent } from "./PushedEvent";
import type { ReplacedEvent } from "./ReplacedEvent";
import type { ResumedEvent } from "./ResumedEvent";
import type { StepPoppedEvent } from "./StepPoppedEvent";
import type { StepPushedEvent } from "./StepPushedEvent";
import type { StepReplacedEvent } from "./StepReplacedEvent";
Expand All @@ -15,7 +17,9 @@ export type DomainEvent =
| StepReplacedEvent
| PoppedEvent
| PushedEvent
| ReplacedEvent;
| ReplacedEvent
| PausedEvent
| ResumedEvent;

export * from "./ActivityRegisteredEvent";
export * from "./InitializedEvent";
Expand All @@ -25,3 +29,5 @@ export * from "./ReplacedEvent";
export * from "./StepPoppedEvent";
export * from "./StepPushedEvent";
export * from "./StepReplacedEvent";
export * from "./PausedEvent";
export * from "./ResumedEvent";
Loading
Loading