From 638b252cd556fa5af76c4058e90334204a557d11 Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 14:19:55 +0900 Subject: [PATCH 01/18] Mutex impl --- core/src/utils/Mutex.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 core/src/utils/Mutex.ts diff --git a/core/src/utils/Mutex.ts b/core/src/utils/Mutex.ts new file mode 100644 index 000000000..f58c0825c --- /dev/null +++ b/core/src/utils/Mutex.ts @@ -0,0 +1,24 @@ +export class Mutex { + private latestlyBookedSession: Promise = Promise.resolve(); + + acquire(): Promise<{ release: () => void }> { + return new Promise((resolveSessionHandle) => { + this.latestlyBookedSession = this.latestlyBookedSession.then( + () => + new Promise((resolveSession) => + resolveSessionHandle({ release: () => resolveSession() }), + ), + ); + }); + } + + async runExclusively(thunk: () => Promise): Promise { + const { release } = await this.acquire(); + + try { + return await thunk(); + } finally { + release(); + } + } +} From e13cdff5e22588f27f2cdc1d557bb2f083ac68a0 Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 14:19:58 +0900 Subject: [PATCH 02/18] Task queue --- core/src/utils/TaskQueue/ExclusiveTaskQueue.ts | 12 ++++++++++++ core/src/utils/TaskQueue/TaskQueue.ts | 7 +++++++ 2 files changed, 19 insertions(+) create mode 100644 core/src/utils/TaskQueue/ExclusiveTaskQueue.ts create mode 100644 core/src/utils/TaskQueue/TaskQueue.ts diff --git a/core/src/utils/TaskQueue/ExclusiveTaskQueue.ts b/core/src/utils/TaskQueue/ExclusiveTaskQueue.ts new file mode 100644 index 000000000..68962b86e --- /dev/null +++ b/core/src/utils/TaskQueue/ExclusiveTaskQueue.ts @@ -0,0 +1,12 @@ +import { Mutex } from "utils/Mutex"; +import type { QueuedTask, TaskQueue } from "./TaskQueue"; + +export class ExclusiveTaskQueue implements TaskQueue { + private taskRunLock: Mutex = new Mutex(); + + enqueue(task: () => Promise): QueuedTask { + return { + finished: this.taskRunLock.runExclusively(task), + }; + } +} diff --git a/core/src/utils/TaskQueue/TaskQueue.ts b/core/src/utils/TaskQueue/TaskQueue.ts new file mode 100644 index 000000000..9dd4eb6cc --- /dev/null +++ b/core/src/utils/TaskQueue/TaskQueue.ts @@ -0,0 +1,7 @@ +export interface TaskQueue { + enqueue(task: () => Promise): QueuedTask; +} + +export interface QueuedTask { + finished: Promise; +} From a5207b752bfc45d84c125a472e8f1707f2a4a11d Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 14:30:53 +0900 Subject: [PATCH 03/18] use Aggregator --- core/src/Aggregator/Aggregator.ts | 11 +++++++++ core/src/makeCoreStore.ts | 40 +++++++------------------------ 2 files changed, 19 insertions(+), 32 deletions(-) create mode 100644 core/src/Aggregator/Aggregator.ts diff --git a/core/src/Aggregator/Aggregator.ts b/core/src/Aggregator/Aggregator.ts new file mode 100644 index 000000000..0237e9eba --- /dev/null +++ b/core/src/Aggregator/Aggregator.ts @@ -0,0 +1,11 @@ +import type { Effect } from "Effect"; +import type { DomainEvent } from "event-types"; +import type { Stack } from "../Stack"; + +export interface Aggregator { + getStack(): Stack; + dispatchEvent(event: DomainEvent): void; + subscribeChanges: ( + listener: (effects: Effect[], stack: Stack) => void, + ) => () => void; +} diff --git a/core/src/makeCoreStore.ts b/core/src/makeCoreStore.ts index cbdc99aaa..65c9af5c7 100644 --- a/core/src/makeCoreStore.ts +++ b/core/src/makeCoreStore.ts @@ -1,4 +1,5 @@ import isEqual from "react-fast-compare"; +import type { Aggregator } from "./Aggregator/Aggregator"; import { aggregate } from "./aggregate"; import type { DomainEvent, PushedEvent, StepPushedEvent } from "./event-types"; import { makeEvent } from "./event-utils"; @@ -76,39 +77,20 @@ export function makeCoreStore(options: MakeCoreStoreOptions): CoreStore { options.handlers?.onInitialActivityNotFound?.(); } - const events: { value: DomainEvent[] } = { - value: [...initialRemainingEvents, ...initialPushedEvents], - }; + const aggregator: Aggregator = undefined as any; - const stack = { - value: aggregate(events.value, new Date().getTime()), - }; + aggregator.subscribeChanges((effects) => { + triggerPostEffectHooks(effects, pluginInstances, actions); + }); const actions: StackflowActions = { getStack() { - return stack.value; + return aggregator.getStack(); }, 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); + aggregator.dispatchEvent(newEvent); }, push: () => {}, replace: () => {}, @@ -120,12 +102,6 @@ export function makeCoreStore(options: MakeCoreStoreOptions): CoreStore { resume: () => {}, }; - 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, @@ -145,7 +121,7 @@ export function makeCoreStore(options: MakeCoreStoreOptions): CoreStore { }); }); }), - pullEvents: () => events.value, + pullEvents: () => aggregator.getStack().events, subscribe(listener) { storeListeners.push(listener); From 116de187e7ff67080f3e85baacfed10ec95c61dc Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 14:51:55 +0900 Subject: [PATCH 04/18] Publisher --- core/src/utils/Publisher/Publisher.ts | 4 ++ core/src/utils/Publisher/QueuingPublisher.ts | 42 ++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 core/src/utils/Publisher/Publisher.ts create mode 100644 core/src/utils/Publisher/QueuingPublisher.ts diff --git a/core/src/utils/Publisher/Publisher.ts b/core/src/utils/Publisher/Publisher.ts new file mode 100644 index 000000000..fc2150426 --- /dev/null +++ b/core/src/utils/Publisher/Publisher.ts @@ -0,0 +1,4 @@ +export interface Publisher { + publish(value: T): void; + subscribe(subscriber: (value: T) => void): () => void; +} diff --git a/core/src/utils/Publisher/QueuingPublisher.ts b/core/src/utils/Publisher/QueuingPublisher.ts new file mode 100644 index 000000000..f778fb989 --- /dev/null +++ b/core/src/utils/Publisher/QueuingPublisher.ts @@ -0,0 +1,42 @@ +import type { TaskQueue } from "../TaskQueue/TaskQueue"; +import type { Publisher } from "./Publisher"; + +export class QueuingPublisher implements Publisher { + private taskQueue: TaskQueue; + private subscribers: ((value: T) => void)[] = []; + private errorHandler: (error: unknown) => void; + + constructor( + taskQueue: TaskQueue, + options?: { errorHandler?: (error: unknown) => void }, + ) { + this.taskQueue = taskQueue; + this.errorHandler = options?.errorHandler ?? (() => {}); + } + + publish( + value: T, + options?: { onPublishError?: (error: unknown) => void }, + ): void { + const subscribers = this.subscribers.slice(); + const publishTask = this.taskQueue.enqueue(async () => { + for (const subscriber of subscribers) { + try { + subscriber(value); + } catch (error) { + options?.onPublishError?.(error); + } + } + }); + + publishTask.finished.catch(this.errorHandler); + } + + subscribe(subscriber: (value: T) => void): () => void { + this.subscribers.push(subscriber); + + return () => { + this.subscribers = this.subscribers.filter((s) => s !== subscriber); + }; + } +} From 6a0681963f139682af4895a9f243712887ebef53 Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 16:02:13 +0900 Subject: [PATCH 05/18] Test --- core/src/Aggregator/SyncAggregator.ts | 128 ++++++++++++++++++ core/src/Stack.ts | 1 + .../activity-utils/makeActivitiesReducer.ts | 8 +- .../activity-utils/makeActivityFromEvent.ts | 2 + .../src/activity-utils/makeActivityReducer.ts | 2 + core/src/makeCoreStore.ts | 12 +- core/src/produceEffects.ts | 10 +- 7 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 core/src/Aggregator/SyncAggregator.ts diff --git a/core/src/Aggregator/SyncAggregator.ts b/core/src/Aggregator/SyncAggregator.ts new file mode 100644 index 000000000..18a2cd032 --- /dev/null +++ b/core/src/Aggregator/SyncAggregator.ts @@ -0,0 +1,128 @@ +import { produceEffects } from "produceEffects"; +import { aggregate } from "../aggregate"; +import type { Effect } from "../Effect"; +import type { + DomainEvent, + PoppedEvent, + PushedEvent, + ReplacedEvent, +} from "../event-types"; +import type { Stack } from "../Stack"; +import type { Publisher } from "../utils/Publisher/Publisher"; +import type { Aggregator } from "./Aggregator"; + +export class SyncAggregator implements Aggregator { + private events: DomainEvent[]; + private changePublisher: Publisher<{ effects: Effect[]; stack: Stack }>; + private autoUpdateTask: DynamicallyScheduledTask; + private previousStack: Stack; + + constructor( + events: DomainEvent[], + changePublisher: Publisher<{ effects: Effect[]; stack: Stack }>, + ) { + this.events = events; + this.changePublisher = changePublisher; + this.autoUpdateTask = new DynamicallyScheduledTask(async () => { + this.updateStack(); + }); + this.previousStack = this.computeStack(); + } + + getStack(): Stack { + return this.computeStack(); + } + + dispatchEvent(event: DomainEvent): void { + this.events.push(event); + this.updateStack(); + } + + subscribeChanges( + listener: (effects: Effect[], stack: Stack) => void, + ): () => void { + return this.changePublisher.subscribe(({ effects, stack }) => { + listener(effects, stack); + }); + } + + private computeStack(): Stack { + return aggregate(this.events, Date.now()); + } + + private predictUpcomingTransitionStateUpdate(): { + event: DomainEvent; + timestamp: number; + } | null { + const stack = this.computeStack(); + const activeActivities = stack.activities.filter( + (activity) => + activity.transitionState === "enter-active" || + activity.transitionState === "exit-active", + ); + const mostRecentlyActivatedActivity = activeActivities.sort( + (a, b) => b.estimatedTransitionEnd! - a.estimatedTransitionEnd!, + )[0]; + + return mostRecentlyActivatedActivity + ? { + event: + mostRecentlyActivatedActivity.exitedBy ?? + mostRecentlyActivatedActivity.enteredBy, + timestamp: mostRecentlyActivatedActivity.estimatedTransitionEnd!, + } + : null; + } + + private updateStack(): void { + const previousStack = this.previousStack; + const currentStack = this.computeStack(); + const effects = produceEffects(previousStack, currentStack); + + if (effects.length > 0) { + this.changePublisher.publish({ effects, stack: currentStack }); + + this.previousStack = currentStack; + + const upcomingTransitionStateUpdate = + this.predictUpcomingTransitionStateUpdate(); + + if (upcomingTransitionStateUpdate) { + this.autoUpdateTask.schedule(upcomingTransitionStateUpdate.timestamp); + } + } + } +} + +class DynamicallyScheduledTask { + private task: () => Promise; + private scheduleId: number | null; + + constructor(task: () => Promise) { + this.task = task; + this.scheduleId = null; + } + + schedule(timestamp: number): void { + if (this.scheduleId !== null) { + clearTimeout(this.scheduleId); + this.scheduleId = null; + } + + const timeoutId = setTimeout( + () => { + if (this.scheduleId !== timeoutId) return; + if (Date.now() < timestamp) { + this.schedule(timestamp); + return; + } + + this.task(); + this.scheduleId = null; + }, + Math.max(0, timestamp - Date.now()), + ); + + this.scheduleId = timeoutId; + } +} diff --git a/core/src/Stack.ts b/core/src/Stack.ts index fe2c0dfc2..7381e61df 100644 --- a/core/src/Stack.ts +++ b/core/src/Stack.ts @@ -29,6 +29,7 @@ export type Activity = { id: string; name: string; transitionState: ActivityTransitionState; + estimatedTransitionEnd?: number; params: { [key: string]: string | undefined; }; diff --git a/core/src/activity-utils/makeActivitiesReducer.ts b/core/src/activity-utils/makeActivitiesReducer.ts index 13cbfdc2f..f72f22715 100644 --- a/core/src/activity-utils/makeActivitiesReducer.ts +++ b/core/src/activity-utils/makeActivitiesReducer.ts @@ -27,6 +27,8 @@ export function makeActivitiesReducer({ Pushed(activities: Activity[], event: PushedEvent): Activity[] { const isTransitionDone = now - (resumedAt ?? event.eventDate) >= transitionDuration; + const estimatedTransitionEnd = + (resumedAt ?? event.eventDate) + transitionDuration; const transitionState: ActivityTransitionState = event.skipEnterActiveState || isTransitionDone @@ -37,7 +39,7 @@ export function makeActivitiesReducer({ return [ ...activities.slice(0, reservedIndex), - makeActivityFromEvent(event, transitionState), + makeActivityFromEvent(event, transitionState, estimatedTransitionEnd), ...activities.slice(reservedIndex + 1), ]; }, @@ -48,6 +50,8 @@ export function makeActivitiesReducer({ Replaced(activities: Activity[], event: ReplacedEvent): Activity[] { const isTransitionDone = now - (resumedAt ?? event.eventDate) >= transitionDuration; + const estimatedTransitionEnd = + (resumedAt ?? event.eventDate) + transitionDuration; const reservedIndex = findNewActivityIndex(activities, event); @@ -60,7 +64,7 @@ export function makeActivitiesReducer({ return [ ...activities.slice(0, reservedIndex), - makeActivityFromEvent(event, transitionState), + makeActivityFromEvent(event, transitionState, estimatedTransitionEnd), ...activities.slice(reservedIndex + 1), ]; }, diff --git a/core/src/activity-utils/makeActivityFromEvent.ts b/core/src/activity-utils/makeActivityFromEvent.ts index 58a74d3e7..615729db1 100644 --- a/core/src/activity-utils/makeActivityFromEvent.ts +++ b/core/src/activity-utils/makeActivityFromEvent.ts @@ -4,11 +4,13 @@ import type { Activity, ActivityTransitionState } from "../Stack"; export function makeActivityFromEvent( event: PushedEvent | ReplacedEvent, transitionState: ActivityTransitionState, + estimatedTransitionEnd: number, ): Activity { return { id: event.activityId, name: event.activityName, transitionState, + estimatedTransitionEnd, params: event.activityParams, context: event.activityContext, steps: [ diff --git a/core/src/activity-utils/makeActivityReducer.ts b/core/src/activity-utils/makeActivityReducer.ts index fac85e2d7..7dafa3713 100644 --- a/core/src/activity-utils/makeActivityReducer.ts +++ b/core/src/activity-utils/makeActivityReducer.ts @@ -49,6 +49,8 @@ export function makeActivityReducer(context: { ...activity, exitedBy: event, transitionState, + estimatedTransitionEnd: + (context.resumedAt ?? event.eventDate) + context.transitionDuration, params: transitionState === "exit-done" ? activity.steps[0].params diff --git a/core/src/makeCoreStore.ts b/core/src/makeCoreStore.ts index 65c9af5c7..012d6f4c7 100644 --- a/core/src/makeCoreStore.ts +++ b/core/src/makeCoreStore.ts @@ -1,13 +1,12 @@ -import isEqual from "react-fast-compare"; +import { ExclusiveTaskQueue } from "utils/TaskQueue/ExclusiveTaskQueue"; import type { Aggregator } from "./Aggregator/Aggregator"; -import { aggregate } from "./aggregate"; +import { SyncAggregator } from "./Aggregator/SyncAggregator"; import type { DomainEvent, PushedEvent, StepPushedEvent } from "./event-types"; import { makeEvent } from "./event-utils"; import type { StackflowActions, StackflowPlugin } from "./interfaces"; -import { produceEffects } from "./produceEffects"; -import type { Stack } from "./Stack"; import { divideBy, once } from "./utils"; import { makeActions } from "./utils/makeActions"; +import { QueuingPublisher } from "./utils/Publisher/QueuingPublisher"; import { triggerPostEffectHooks } from "./utils/triggerPostEffectHooks"; const SECOND = 1000; @@ -77,7 +76,10 @@ export function makeCoreStore(options: MakeCoreStoreOptions): CoreStore { options.handlers?.onInitialActivityNotFound?.(); } - const aggregator: Aggregator = undefined as any; + const aggregator: Aggregator = new SyncAggregator( + [...initialRemainingEvents, ...initialPushedEvents], + new QueuingPublisher(new ExclusiveTaskQueue()), + ); aggregator.subscribeChanges((effects) => { triggerPostEffectHooks(effects, pluginInstances, actions); diff --git a/core/src/produceEffects.ts b/core/src/produceEffects.ts index 02331b203..04cc2bd4c 100644 --- a/core/src/produceEffects.ts +++ b/core/src/produceEffects.ts @@ -9,12 +9,14 @@ export function produceEffects(prevOutput: Stack, nextOutput: Stack): Effect[] { const somethingChanged = !isEqual(prevOutput, nextOutput); - if (somethingChanged) { - output.push({ - _TAG: "%SOMETHING_CHANGED%", - }); + if (!somethingChanged) { + return output; } + output.push({ + _TAG: "%SOMETHING_CHANGED%", + }); + const isPaused = prevOutput.globalTransitionState !== "paused" && nextOutput.globalTransitionState === "paused"; From e9904c8443e39dfe80e761cfe4582a9055939143 Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 16:05:20 +0900 Subject: [PATCH 06/18] fix --- core/src/Aggregator/SyncAggregator.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/core/src/Aggregator/SyncAggregator.ts b/core/src/Aggregator/SyncAggregator.ts index 18a2cd032..920e013a8 100644 --- a/core/src/Aggregator/SyncAggregator.ts +++ b/core/src/Aggregator/SyncAggregator.ts @@ -1,12 +1,7 @@ import { produceEffects } from "produceEffects"; import { aggregate } from "../aggregate"; import type { Effect } from "../Effect"; -import type { - DomainEvent, - PoppedEvent, - PushedEvent, - ReplacedEvent, -} from "../event-types"; +import type { DomainEvent } from "../event-types"; import type { Stack } from "../Stack"; import type { Publisher } from "../utils/Publisher/Publisher"; import type { Aggregator } from "./Aggregator"; @@ -30,7 +25,7 @@ export class SyncAggregator implements Aggregator { } getStack(): Stack { - return this.computeStack(); + return this.previousStack; } dispatchEvent(event: DomainEvent): void { From 894ec746ea0bd8a39589237409f866b89714de5b Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 16:12:31 +0900 Subject: [PATCH 07/18] fix --- core/src/Aggregator/SyncAggregator.ts | 1 + core/src/activity-utils/makeActivityReducer.ts | 1 + core/src/aggregate.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/core/src/Aggregator/SyncAggregator.ts b/core/src/Aggregator/SyncAggregator.ts index 920e013a8..9d756cd0e 100644 --- a/core/src/Aggregator/SyncAggregator.ts +++ b/core/src/Aggregator/SyncAggregator.ts @@ -29,6 +29,7 @@ export class SyncAggregator implements Aggregator { } dispatchEvent(event: DomainEvent): void { + console.log("dispatchEvent", event); this.events.push(event); this.updateStack(); } diff --git a/core/src/activity-utils/makeActivityReducer.ts b/core/src/activity-utils/makeActivityReducer.ts index 7dafa3713..82d043b7e 100644 --- a/core/src/activity-utils/makeActivityReducer.ts +++ b/core/src/activity-utils/makeActivityReducer.ts @@ -30,6 +30,7 @@ export function makeActivityReducer(context: { ...activity, exitedBy: event, transitionState: "exit-done", + estimatedTransitionEnd: context.now, }), /** diff --git a/core/src/aggregate.ts b/core/src/aggregate.ts index d8db38d98..0a2972d36 100644 --- a/core/src/aggregate.ts +++ b/core/src/aggregate.ts @@ -83,6 +83,7 @@ export function aggregate(inputEvents: DomainEvent[], now: number): Stack { id: activity.id, name: activity.name, transitionState: activity.transitionState, + estimatedTransitionEnd: activity.estimatedTransitionEnd, params: activity.params, steps, enteredBy: activity.enteredBy, From 3e235cc3e1361560dec6b162ad0fb588cf2e355d Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 16:12:58 +0900 Subject: [PATCH 08/18] remove log --- core/src/Aggregator/SyncAggregator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/Aggregator/SyncAggregator.ts b/core/src/Aggregator/SyncAggregator.ts index 9d756cd0e..920e013a8 100644 --- a/core/src/Aggregator/SyncAggregator.ts +++ b/core/src/Aggregator/SyncAggregator.ts @@ -29,7 +29,6 @@ export class SyncAggregator implements Aggregator { } dispatchEvent(event: DomainEvent): void { - console.log("dispatchEvent", event); this.events.push(event); this.updateStack(); } From bebfbb627142106531ecadf7b6af90c5c2eef14c Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 16:56:04 +0900 Subject: [PATCH 09/18] simpl --- core/src/produceEffects.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/src/produceEffects.ts b/core/src/produceEffects.ts index 04cc2bd4c..348097ac1 100644 --- a/core/src/produceEffects.ts +++ b/core/src/produceEffects.ts @@ -7,10 +7,8 @@ import { omit } from "./utils"; export function produceEffects(prevOutput: Stack, nextOutput: Stack): Effect[] { const output: Effect[] = []; - const somethingChanged = !isEqual(prevOutput, nextOutput); - - if (!somethingChanged) { - return output; + if (isEqual(prevOutput, nextOutput)) { + return []; } output.push({ From 660db05678a6d9fc82eddcd54dcad44e223082df Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 17:01:05 +0900 Subject: [PATCH 10/18] unused imports --- core/src/Stack.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/Stack.ts b/core/src/Stack.ts index 7381e61df..5fb51a490 100644 --- a/core/src/Stack.ts +++ b/core/src/Stack.ts @@ -1,4 +1,3 @@ -import type { BaseDomainEvent } from "event-types/_base"; import type { DomainEvent, PoppedEvent, From 7ea723351dfcdd2bb769d7db3427c614c16b62fb Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 17:07:45 +0900 Subject: [PATCH 11/18] estimated transition end --- core/src/Stack.ts | 2 +- core/src/activity-utils/makeActivitiesReducer.ts | 6 ++---- core/src/activity-utils/makeActivityReducer.ts | 11 +++++------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/core/src/Stack.ts b/core/src/Stack.ts index 5fb51a490..4bb242db0 100644 --- a/core/src/Stack.ts +++ b/core/src/Stack.ts @@ -28,7 +28,7 @@ export type Activity = { id: string; name: string; transitionState: ActivityTransitionState; - estimatedTransitionEnd?: number; + estimatedTransitionEnd: number; params: { [key: string]: string | undefined; }; diff --git a/core/src/activity-utils/makeActivitiesReducer.ts b/core/src/activity-utils/makeActivitiesReducer.ts index f72f22715..aef15ede9 100644 --- a/core/src/activity-utils/makeActivitiesReducer.ts +++ b/core/src/activity-utils/makeActivitiesReducer.ts @@ -25,10 +25,9 @@ export function makeActivitiesReducer({ * Push new activity to activities */ Pushed(activities: Activity[], event: PushedEvent): Activity[] { - const isTransitionDone = - now - (resumedAt ?? event.eventDate) >= transitionDuration; const estimatedTransitionEnd = (resumedAt ?? event.eventDate) + transitionDuration; + const isTransitionDone = estimatedTransitionEnd <= now; const transitionState: ActivityTransitionState = event.skipEnterActiveState || isTransitionDone @@ -48,10 +47,9 @@ export function makeActivitiesReducer({ * Replace activity at reservedIndex with new activity */ Replaced(activities: Activity[], event: ReplacedEvent): Activity[] { - const isTransitionDone = - now - (resumedAt ?? event.eventDate) >= transitionDuration; const estimatedTransitionEnd = (resumedAt ?? event.eventDate) + transitionDuration; + const isTransitionDone = estimatedTransitionEnd <= now; const reservedIndex = findNewActivityIndex(activities, event); diff --git a/core/src/activity-utils/makeActivityReducer.ts b/core/src/activity-utils/makeActivityReducer.ts index 82d043b7e..8adbffdae 100644 --- a/core/src/activity-utils/makeActivityReducer.ts +++ b/core/src/activity-utils/makeActivityReducer.ts @@ -30,16 +30,16 @@ export function makeActivityReducer(context: { ...activity, exitedBy: event, transitionState: "exit-done", - estimatedTransitionEnd: context.now, + estimatedTransitionEnd: context.resumedAt ?? event.eventDate, }), /** * Change transition state to exit-done or exit-active depending on skipExitActiveState */ Popped: (activity: Activity, event: PoppedEvent): Activity => { - const isTransitionDone = - context.now - (context.resumedAt ?? event.eventDate) >= - context.transitionDuration; + const estimatedTransitionEnd = + (context.resumedAt ?? event.eventDate) + context.transitionDuration; + const isTransitionDone = estimatedTransitionEnd <= context.now; const transitionState: ActivityTransitionState = event.skipExitActiveState || isTransitionDone @@ -50,8 +50,7 @@ export function makeActivityReducer(context: { ...activity, exitedBy: event, transitionState, - estimatedTransitionEnd: - (context.resumedAt ?? event.eventDate) + context.transitionDuration, + estimatedTransitionEnd, params: transitionState === "exit-done" ? activity.steps[0].params From b8053a17c2389199acaffc61ca95eee376c60f95 Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 17:16:41 +0900 Subject: [PATCH 12/18] skip test type check --- core/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/tsconfig.json b/core/tsconfig.json index 6ce58df83..170b23f9b 100644 --- a/core/tsconfig.json +++ b/core/tsconfig.json @@ -4,5 +4,5 @@ "baseUrl": "./src", "outDir": "./dist" }, - "exclude": ["./dist"] + "exclude": ["./dist", "./src/**/*.spec.ts"] } From 4978fa73043c0bed30f74f29d7563c33042305d3 Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 17:33:55 +0900 Subject: [PATCH 13/18] =?UTF-8?q?=E3=84=B1=E3=84=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/Aggregator/SyncAggregator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/Aggregator/SyncAggregator.ts b/core/src/Aggregator/SyncAggregator.ts index 920e013a8..12b8e20d2 100644 --- a/core/src/Aggregator/SyncAggregator.ts +++ b/core/src/Aggregator/SyncAggregator.ts @@ -56,7 +56,7 @@ export class SyncAggregator implements Aggregator { activity.transitionState === "exit-active", ); const mostRecentlyActivatedActivity = activeActivities.sort( - (a, b) => b.estimatedTransitionEnd! - a.estimatedTransitionEnd!, + (a, b) => b.estimatedTransitionEnd - a.estimatedTransitionEnd, )[0]; return mostRecentlyActivatedActivity @@ -64,7 +64,7 @@ export class SyncAggregator implements Aggregator { event: mostRecentlyActivatedActivity.exitedBy ?? mostRecentlyActivatedActivity.enteredBy, - timestamp: mostRecentlyActivatedActivity.estimatedTransitionEnd!, + timestamp: mostRecentlyActivatedActivity.estimatedTransitionEnd, } : null; } From b27f778465fc3c665f7698a5814b21dd9c3ccdc1 Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 17:35:44 +0900 Subject: [PATCH 14/18] fix order --- core/src/Aggregator/SyncAggregator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/Aggregator/SyncAggregator.ts b/core/src/Aggregator/SyncAggregator.ts index 12b8e20d2..acc0d1a4f 100644 --- a/core/src/Aggregator/SyncAggregator.ts +++ b/core/src/Aggregator/SyncAggregator.ts @@ -56,7 +56,7 @@ export class SyncAggregator implements Aggregator { activity.transitionState === "exit-active", ); const mostRecentlyActivatedActivity = activeActivities.sort( - (a, b) => b.estimatedTransitionEnd - a.estimatedTransitionEnd, + (a, b) => a.estimatedTransitionEnd - b.estimatedTransitionEnd, )[0]; return mostRecentlyActivatedActivity From cfee453368008161f7ee6db7d477c8e06b278925 Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 17:38:56 +0900 Subject: [PATCH 15/18] opt --- core/src/Aggregator/SyncAggregator.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/Aggregator/SyncAggregator.ts b/core/src/Aggregator/SyncAggregator.ts index acc0d1a4f..e82ba1592 100644 --- a/core/src/Aggregator/SyncAggregator.ts +++ b/core/src/Aggregator/SyncAggregator.ts @@ -49,8 +49,7 @@ export class SyncAggregator implements Aggregator { event: DomainEvent; timestamp: number; } | null { - const stack = this.computeStack(); - const activeActivities = stack.activities.filter( + const activeActivities = this.previousStack.activities.filter( (activity) => activity.transitionState === "enter-active" || activity.transitionState === "exit-active", From 5cdc120c270f866a09a93cb6e716648e9728dd19 Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Fri, 19 Dec 2025 18:43:26 +0900 Subject: [PATCH 16/18] fix --- core/src/Aggregator/SyncAggregator.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/Aggregator/SyncAggregator.ts b/core/src/Aggregator/SyncAggregator.ts index e82ba1592..9f21e1ebe 100644 --- a/core/src/Aggregator/SyncAggregator.ts +++ b/core/src/Aggregator/SyncAggregator.ts @@ -106,13 +106,15 @@ class DynamicallyScheduledTask { const timeoutId = setTimeout( () => { if (this.scheduleId !== timeoutId) return; + + this.scheduleId = null; + if (Date.now() < timestamp) { this.schedule(timestamp); return; } this.task(); - this.scheduleId = null; }, Math.max(0, timestamp - Date.now()), ); From faa3fc9c19a376676cf988f7fcc453added20dea Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Mon, 22 Dec 2025 14:45:11 +0900 Subject: [PATCH 17/18] Delete unused variables --- core/src/makeCoreStore.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/src/makeCoreStore.ts b/core/src/makeCoreStore.ts index 012d6f4c7..0aff33e35 100644 --- a/core/src/makeCoreStore.ts +++ b/core/src/makeCoreStore.ts @@ -9,11 +9,6 @@ import { makeActions } from "./utils/makeActions"; import { QueuingPublisher } from "./utils/Publisher/QueuingPublisher"; import { triggerPostEffectHooks } from "./utils/triggerPostEffectHooks"; -const SECOND = 1000; - -// 60FPS -const INTERVAL_MS = SECOND / 60; - export type MakeCoreStoreOptions = { initialEvents: DomainEvent[]; initialContext?: any; From ff683c2ab17028f3d6e89b350d75e8ca85a6dc84 Mon Sep 17 00:00:00 2001 From: anakin_karrot Date: Mon, 22 Dec 2025 14:52:31 +0900 Subject: [PATCH 18/18] fix imports --- core/src/Aggregator/Aggregator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/Aggregator/Aggregator.ts b/core/src/Aggregator/Aggregator.ts index 0237e9eba..5cf3b7912 100644 --- a/core/src/Aggregator/Aggregator.ts +++ b/core/src/Aggregator/Aggregator.ts @@ -1,5 +1,5 @@ -import type { Effect } from "Effect"; -import type { DomainEvent } from "event-types"; +import type { Effect } from "../Effect"; +import type { DomainEvent } from "../event-types"; import type { Stack } from "../Stack"; export interface Aggregator {