Skip to content

Conversation

@parkwoocheol
Copy link

Summary

Adds prune() API to StackflowActions in @stackflow/core to manually clear past events and inactive activities.

Motivation

Stackflow's Event Sourcing pattern causes the event history to grow indefinitely. This leads to increased memory usage and re-calculation costs in long-running sessions. A mechanism to clear old history and release memory was required.

Features

  • actions.prune(): Clears event history while preserving the current stack state.
  • State Reconstruction: Reconstructs the stack using only enter-active and enter-done activities.

@changeset-bot
Copy link

changeset-bot bot commented Nov 25, 2025

⚠️ No Changeset found

Latest commit: 9583f12

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Nov 25, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added a prune() action that clears historical events while reconstructing and maintaining the current application state, helping optimize memory usage by removing past event data.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds a new prune() action to the public actions surface and implements it to rebuild a minimal event history from currently active activities, replace stored events, and re-aggregate the stack; a unit test verifying prune behavior was added.

Changes

Cohort / File(s) Change Summary
Interface
core/src/interfaces/StackflowActions.ts
Added public method prune(): void to StackflowActions with JSDoc describing clearing past events and reconstructing state from active activities.
Core store
core/src/makeCoreStore.ts
Exposed a store object into action construction (getStack, events.value, setStackValue) and surfaced prune() on the actions object.
Action utilities
core/src/utils/makeActions.ts
Extended makeActions to accept store in ActionCreatorOptions and implemented prune() which: reads current stack, identifies active activities, builds a synthetic sequence of DomainEvents (Initialized, ActivityRegistered*, Pushed/Step events, Popped as applicable), replaces store.events.value, re-aggregates via aggregate(...), and applies the new stack via setStackValue. Added imports for aggregate, DomainEvent, makeEvent, and Stack.
Tests
core/src/makeCoreStore.spec.ts
Added test "makeCoreStore - prune이 호출되면, 과거의 이벤트를 정리하고 현재 상태를 유지합니다" that seeds past events, performs pushes/pops, asserts pre-prune stack, calls prune(), then asserts only active activities remain and events trimmed to the minimal synthetic set.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Actions as Actions.prune()
    participant Store
    participant Events as Events.value
    participant Aggregate as aggregate()

    Caller->>Actions: prune()
    Actions->>Store: getStack()
    Store-->>Actions: current Stack

    rect rgb(230,245,235)
    Note over Actions: Filter active activities\n(build activity metadata list)
    Actions->>Actions: Build synthetic events:\n- Initialized\n- ActivityRegistered* (reuse metadata)\n- Pushed / StepPushed / StepReplaced\n- Popped (if needed)
    end

    Actions->>Events: events.value = [synthetic events]

    rect rgb(245,235,235)
    Actions->>Aggregate: aggregate(events.value)
    Aggregate-->>Actions: new Stack
    end

    Actions->>Store: setStackValue(new Stack)
    Store-->>Caller: stack & events updated
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Verify event ordering and flags when building synthetic events (e.g., skipEnterActiveState, activityContext).
  • Check transitionState filtering and selection of "active" activities (enter-active vs enter-done).
  • Ensure store.events.value replacement is safe (no stale references) and aggregation is deterministic.
  • Review the new test for edge cases (empty stack, single active activity, mixed states) and timing assumptions for event dates.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(core): add prune API to clear event history' clearly and accurately summarizes the main change: adding a new prune API to the core module for clearing event history, which aligns with all modified files.
Description check ✅ Passed The description is well-detailed and directly related to the changeset, explaining the motivation for the prune API, its purpose in clearing event history while preserving stack state, and the features being introduced.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9362089 and 9583f12.

📒 Files selected for processing (1)
  • core/src/utils/makeActions.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Write source in TypeScript with strict typing enabled across the codebase

Files:

  • core/src/utils/makeActions.ts
🧬 Code graph analysis (1)
core/src/utils/makeActions.ts (4)
core/src/interfaces/StackflowActions.ts (1)
  • StackflowActions (15-70)
core/src/Stack.ts (1)
  • Stack (50-57)
core/src/event-types/index.ts (1)
  • DomainEvent (12-22)
core/src/aggregate.ts (1)
  • aggregate (7-113)
🔇 Additional comments (8)
core/src/utils/makeActions.ts (8)

1-5: LGTM!

The new imports for aggregate, DomainEvent, makeEvent, and Stack are properly typed and necessary for the prune() implementation.


12-16: LGTM!

The store parameter is properly typed and provides the necessary access to stack state, events, and mutation capability for the prune() implementation.

Also applies to: 23-23


138-143: LGTM!

The guard against pruning a paused stack is appropriate and prevents loss of pausedEvents.


145-150: LGTM!

Including "exit-active" activities in the filter correctly preserves activities that are currently transitioning out, addressing the previous review concern.


152-158: LGTM!

The event date calculation ensures monotonic time progression, and preserving the original Initialized event's eventDate maintains temporal consistency.


160-202: LGTM!

The logic correctly preserves ALL activity registrations (not just active ones) including their paramsSchema metadata, addressing the previous concern about dropping inactive registrations.


204-229: LGTM!

The reconstruction logic correctly:

  • Preserves original entry semantics (Pushed vs Replaced) to maintain isRoot computation
  • Includes all activity metadata (activityContext)
  • Reconstructs steps with proper event types and metadata (hasZIndex)
  • Skips the first step appropriately (root step is represented in activity params)

This addresses previous concerns about entry semantics and step preservation.


231-240: LGTM!

Correctly extracts and preserves Popped events for exit-active activities, maintaining temporal ordering.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
core/src/makeCoreStore.spec.ts (1)

285-351: Consider adding edge case tests.

The test covers the basic scenario well. However, consider adding tests for:

  1. Prune with multiple active activities - verify ordering is preserved
  2. Prune with activities containing steps - steps appear to be lost in reconstruction
  3. Prune with no active activities - what happens when all activities are exit-done?

Also note that after pruning, the "detail" ActivityRegistered is lost. If a user tries to push a "detail" activity after prune, it may fail validation since that activity is no longer registered.

core/src/utils/makeActions.ts (1)

138-174: All registered activities not currently active are lost.

After pruning, only activity names with active instances are re-registered. If you have registered activities (e.g., "settings", "profile") that aren't currently in the stack but may be pushed later, they'll be lost and subsequent push calls for those activities may fail.

Consider preserving the full registeredActivities list from the current stack to maintain the complete activity registry.

+      const { activities, registeredActivities, transitionDuration } = store.getStack();
-      const { activities } = store.getStack();
       const activeActivities = activities.filter(
         (activity) =>
           activity.transitionState === "enter-active" ||
           activity.transitionState === "enter-done",
       );

       const now = new Date().getTime();

       const newEvents: DomainEvent[] = [
         makeEvent("Initialized", {
-          transitionDuration: 0,
+          transitionDuration,
           eventDate: now,
         }),
-        ...activeActivities.map((activity) =>
+        ...registeredActivities.map(({ name }) =>
           makeEvent("ActivityRegistered", {
-            activityName: activity.name,
+            activityName: name,
             eventDate: now,
           }),
         ),
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a136f96 and 1e4c2dc.

📒 Files selected for processing (4)
  • core/src/interfaces/StackflowActions.ts (1 hunks)
  • core/src/makeCoreStore.spec.ts (1 hunks)
  • core/src/makeCoreStore.ts (2 hunks)
  • core/src/utils/makeActions.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Write source in TypeScript with strict typing enabled across the codebase

Files:

  • core/src/interfaces/StackflowActions.ts
  • core/src/makeCoreStore.spec.ts
  • core/src/utils/makeActions.ts
  • core/src/makeCoreStore.ts
**/*.spec.ts

📄 CodeRabbit inference engine (AGENTS.md)

Name test files with the .spec.ts suffix and keep them alongside source files

Files:

  • core/src/makeCoreStore.spec.ts
🧬 Code graph analysis (1)
core/src/utils/makeActions.ts (4)
core/src/interfaces/StackflowActions.ts (1)
  • StackflowActions (15-70)
core/src/Stack.ts (1)
  • Stack (50-57)
core/src/event-types/index.ts (1)
  • DomainEvent (12-22)
core/src/aggregate.ts (1)
  • aggregate (7-113)
🔇 Additional comments (3)
core/src/interfaces/StackflowActions.ts (1)

65-69: LGTM!

The interface extension is clean and the JSDoc accurately describes the method's purpose. The no-argument signature is appropriate for this operation.

core/src/makeCoreStore.ts (1)

137-141: LGTM!

The store object properly exposes the required internals (getStack, events, setStackValue) for the prune implementation while keeping the pattern consistent with how other actions are wired.

core/src/utils/makeActions.ts (1)

159-167: Step state is indeed lost during prune—verify if this is acceptable for your use case.

The review comment is technically accurate. The Activity type includes a steps: ActivityStep[] property, but the prune() function only emits Initialized, ActivityRegistered, and Pushed events. Since StepPushed events populate the steps array during activity reconstruction, omitting them results in an empty steps array after prune.

This appears intentional—prune is documented as "clear past events and reconstruct state with active activities to release memory." However, if your application relies on step history (e.g., for URL state synchronization via the history-sync plugin), this design choice could cause issues. Confirm whether step preservation is required for your use case.

- Preserve transitionDuration from current stack instead of hardcoding to 0
- Deduplicate ActivityRegistered events when multiple activities share same name
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
core/src/utils/makeActions.ts (1)

138-144: Consider validating stack state before pruning.

The prune operation doesn't check:

  1. Whether activeActivities is empty (all activities could be exiting)
  2. Whether globalTransitionState is "idle" (pruning during transitions could cause issues)

Consider adding guards:

 prune() {
-  const { activities } = store.getStack();
+  const stack = store.getStack();
+  
+  if (stack.globalTransitionState !== "idle") {
+    console.warn("Cannot prune during active transitions");
+    return;
+  }
+  
+  const { activities } = stack;
   const activeActivities = activities.filter(
     (activity) =>
       activity.transitionState === "enter-active" ||
       activity.transitionState === "enter-done",
   );
+  
+  if (activeActivities.length === 0) {
+    console.warn("Cannot prune with no active activities");
+    return;
+  }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1e4c2dc and aaef690.

📒 Files selected for processing (1)
  • core/src/utils/makeActions.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Write source in TypeScript with strict typing enabled across the codebase

Files:

  • core/src/utils/makeActions.ts
🧬 Code graph analysis (1)
core/src/utils/makeActions.ts (4)
core/src/interfaces/StackflowActions.ts (1)
  • StackflowActions (15-70)
core/src/Stack.ts (1)
  • Stack (50-57)
core/src/event-types/index.ts (1)
  • DomainEvent (12-22)
core/src/aggregate.ts (1)
  • aggregate (7-113)
🔇 Additional comments (3)
core/src/utils/makeActions.ts (3)

1-5: LGTM: Imports are appropriate for the prune implementation.

All new imports are properly typed and necessary for the event reconstruction logic.


12-16: LGTM: Store interface provides appropriate access for pruning.

The store object cleanly exposes the minimal interface needed for the prune operation.


157-162: Prune intentionally removes inactive registered activities—this is not a bug.

The test in core/src/makeCoreStore.spec.ts:285 explicitly validates this behavior: "when prune is called, it cleans up past events and maintains current state." The prune() operation reconstructs the event history from scratch, keeping only currently active activities. This design choice allows the function to reduce event log size by discarding obsolete registrations.

The review comment treats this as unintended data loss, but the test suite confirms this is the intended behavior. No evidence in the codebase indicates that applications depend on preserving pre-registered but inactive activities after pruning.

Likely an incorrect or invalid review comment.

Comment on lines +174 to +177
store.events.value = newEvents;

const nextStackValue = aggregate(store.events.value, now);
store.setStackValue(nextStackValue);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add error handling to prevent inconsistent state.

If aggregate throws an exception, store.events.value will already be mutated but store.setStackValue won't be called, leaving the store in an inconsistent state.

Wrap in try-catch to handle errors gracefully:

-store.events.value = newEvents;
-
-const nextStackValue = aggregate(store.events.value, now);
-store.setStackValue(nextStackValue);
+try {
+  const nextStackValue = aggregate(newEvents, now);
+  store.events.value = newEvents;
+  store.setStackValue(nextStackValue);
+} catch (error) {
+  console.error("Failed to prune stack:", error);
+  // Store remains in original state
+}

This ensures atomicity—either both updates succeed or neither does.

🤖 Prompt for AI Agents
In core/src/utils/makeActions.ts around lines 174 to 177, assigning
store.events.value before calling aggregate can leave the store half-updated if
aggregate throws; compute the aggregated value before mutating or wrap the
mutation+aggregation in a try-catch that reverts store.events.value on error and
then rethrows or logs the error. Specifically, call aggregate(now) using a
non-mutated copy of events (or calculate nextStackValue first), then set
store.events.value and store.setStackValue atomically, or if you must assign
events first, catch any exception from aggregate, restore the previous
store.events.value, and surface the error so the store never remains in an
inconsistent state.

- Preserve original eventDate for temporal ordering
- Preserve activityContext and activityParamsSchema
- Reconstruct activity steps with correct event types
- Preserve hasZIndex metadata
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
core/src/utils/makeActions.ts (1)

243-246: Make aggregate + store updates atomic to avoid inconsistent state

You still assign store.events.value before calling aggregate, so if aggregate throws (due to a validation bug, unexpected event shape, etc.), the events are mutated but the stack is not, leaving the store inconsistent. This concern was raised earlier and is still applicable.

You can make the operation atomic by aggregating on the new events first and only mutating the store after success:

-      store.events.value = newEvents;
-
-      const nextStackValue = aggregate(store.events.value, now);
-      store.setStackValue(nextStackValue);
+      const nextStackValue = aggregate(newEvents, now);
+      store.events.value = newEvents;
+      store.setStackValue(nextStackValue);

This way, if aggregate fails, both events and stack remain untouched.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between aaef690 and 9362089.

📒 Files selected for processing (1)
  • core/src/utils/makeActions.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Write source in TypeScript with strict typing enabled across the codebase

Files:

  • core/src/utils/makeActions.ts
🧬 Code graph analysis (1)
core/src/utils/makeActions.ts (4)
core/src/interfaces/StackflowActions.ts (1)
  • StackflowActions (15-70)
core/src/Stack.ts (1)
  • Stack (50-57)
core/src/event-types/index.ts (1)
  • DomainEvent (12-22)
core/src/aggregate.ts (1)
  • aggregate (7-113)
🔇 Additional comments (1)
core/src/utils/makeActions.ts (1)

1-24: Imports and new store plumbing look consistent and type‑safe

The new imports (aggregate, DomainEvent, makeEvent, Stack) and the store field added to ActionCreatorOptions/makeActions are coherent with the new prune implementation and keep strict typing intact. No issues here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant