Skip to content

Conversation

@huan
Copy link
Owner

@huan huan commented Jan 8, 2026

Summary

This PR is a complete rewrite of the Mailbox library for XState v5, replacing all XState v4 compatibility code with native v5 APIs.

Key Changes

  • Native XState v5 APIs: Uses setup(), createActor(), sendTo(), sendParent(), assign() directly
  • Standard ESM imports: Replaced fragile createRequire hack with standard ES module imports
  • Removed Duckularize: Breaking change - removed the over-engineered type generation pattern
  • Added validation utility: New validate() function to check machines satisfy Mailbox protocol
  • Observable API: Full RxJS interop via Symbol.observable

Breaking Changes (v1.0.0)

Removed Replacement
duckularize() Use native XState v5 typed events with manual pattern
wrap() Use from() instead
Internal context utilities XState v5 handles this natively
typesafe-actions dependency Plain TypeScript as const assertions

Migration Example

Before (v0.x with duckularize):

import { createAction } from 'typesafe-actions'
const Event = { DING: createAction('DING')() }
const duckula = Mailbox.duckularize({ id: 'test', events: Event, ... })

After (v1.0 with manual pattern):

const Type = { DING: 'DING' } as const
const Event = { DING: () => ({ type: Type.DING }) as const }
const machine = setup({
  types: { events: {} as { type: 'DING' } }
}).createMachine({ ... })

Files Changed

Category Changes
Source mailbox.ts (core v5 impl), validate.ts (new), nil.ts, mods/
Deleted src/duckula/ (514 lines), src/actions/, src/from.ts, src/wrap.ts
Tests Updated all specs, added Observable API tests, added validation tests
Docs Updated README with breaking changes and migration guide

Test Results

  • 49 tests passing across 17 test files
  • 0 skipped tests
  • Type checking passes

Dependencies

Reduced to 3 runtime dependencies:

  • rxjs ^7.5.5
  • symbol-observable ^4.0.0
  • xstate ^5.25.0

Removed:

  • typesafe-actions (was only needed for duckularize)

Test plan

  • All 49 unit tests pass
  • TypeScript type checking passes
  • Observable API works with RxJS from()
  • validate() correctly detects invalid machines
  • Baby machine converted to manual pattern works correctly
  • README documents all breaking changes

🤖 Generated with Claude Code

Fix #4 #7 #9

BREAKING CHANGES:
- Requires XState v5 (no longer compatible with v4)
- Removed `duckularize()` and `typesafe-actions` dependency
- Removed `wrap()` function (use `from()` instead)
- Removed internal context utilities (XState v5 handles natively)
- Simplified API surface

## What Changed

### New Implementation
- Pure XState v5 native implementation using `setup()`, `createActor()`, `sendTo()`
- Uses XState v5's native event typing (no external dependencies needed)
- Leverages XState v5's inspection API for event tracking
- Standard ESM imports throughout (removed createRequire hack)

### Removed (~4,600 lines deleted)
- `src/duckula/` - Replaced by native XState v5 typed events
- `src/context/` - Queue management now handled by XState v5
- `src/actions/` - Legacy action implementations
- `src/impls/` - Old mailbox implementation
- `src/duck/` - Internal state enums
- `src/is/`, `src/wrap.ts`, `src/from.ts`, `src/validate.ts` (old)
- `typesafe-actions` dependency

### Added
- `src/mailbox.ts` - Single-file v5 native implementation
- `src/validate.ts` - Machine validation utility (rewritten for v5)
- Observable API tests (RxJS `from(mailbox)` interop)
- Breaking changes documentation in README

### Final Structure
```
src/
├── mailbox.ts       # Core implementation
├── mods/mod.ts      # Main exports
├── nil.ts           # Null objects
├── validate.ts      # Validation utility
└── version.ts       # Version export
```

### Stats
- 108 files changed
- 1,858 insertions, 6,484 deletions
- 49 tests passing
- 3 runtime dependencies (rxjs, symbol-observable, xstate)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 8, 2026 04:56
@gemini-code-assist
Copy link

Summary of Changes

Hello @huan, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request marks a significant architectural shift for the Mailbox library, fully embracing XState v5's native capabilities. The rewrite aims to enhance performance, simplify the codebase, and improve maintainability by aligning with the latest XState patterns. This update also introduces clearer API usage, robust validation, and better integration with modern JavaScript ecosystems like RxJS, ensuring the library remains a powerful tool for managing sequential message processing in actor-based state machines.

Highlights

  • Complete XState v5 Native Rewrite: The entire Mailbox library has been rewritten to leverage native XState v5 APIs, moving away from XState v4 compatibility layers. This includes direct use of setup(), createActor(), sendTo(), sendParent(), and assign().
  • Removal of duckularize and wrap: The duckularize() utility, previously used for type generation, and the wrap() function have been removed. Users should now utilize native XState v5 typed events and the new Mailbox.from() factory function.
  • Simplified API and Dependency Reduction: The library's API has been streamlined for a cleaner, more focused interface. Runtime dependencies have been significantly reduced, notably removing typesafe-actions and utility-types.
  • New Validation Utility: A new validate() function has been introduced to ensure that XState machines adhere to the Mailbox protocol, helping developers catch integration issues early.
  • Full RxJS Interoperability: The Mailbox now fully supports RxJS interoperability via the Symbol.observable protocol, allowing seamless integration with RxJS streams.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is an excellent and comprehensive rewrite of the library for XState v5. The move to native v5 APIs, removal of the duckularize pattern, and simplification of the overall architecture are major improvements that significantly enhance maintainability and ease of use. The updated documentation is much clearer, and the addition of the validate() utility is a great feature for users. The code quality is very high, and the migration to vitest for testing is a solid choice. I have one suggestion to improve the type safety of the Quick Start example in the README, but otherwise, this is an impressive and well-executed major update.

README.md Outdated
Comment on lines 30 to 36
import { createMachine, assign } from 'xstate'

const machine = createMachine({
// Create a machine that processes work one item at a time
const workerMachine = createMachine({
id: 'worker',
initial: 'idle',
context: { result: null },

Choose a reason for hiding this comment

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

medium

For better type safety and to align with modern XState v5 practices, it's recommended to use setup() to define the types for your machine's events and context. This will ensure that properties like event.data are correctly typed, preventing potential TypeScript errors for users following this example.

Suggested change
import { createMachine, assign } from 'xstate'
const machine = createMachine({
// Create a machine that processes work one item at a time
const workerMachine = createMachine({
id: 'worker',
initial: 'idle',
context: { result: null },
import { setup, assign } from 'xstate'
// Create a machine that processes work one item at a time
const workerMachine = setup({
types: {} as {
events: { type: 'WORK'; data: string } | { type: 'DONE'; result: string | null };
context: { result: string | null };
},
}).createMachine({
id: 'worker',
initial: 'idle',
context: { result: null },

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR performs a complete rewrite of the Mailbox library to use native XState v5 APIs, removing all v4 compatibility shims and the over-engineered duckularize() pattern. The rewrite simplifies the codebase significantly by leveraging XState v5's improved features like setup(), native typed events, and improved actor communication.

Key changes:

  • Native XState v5 implementation using setup(), createActor(), sendTo(), and sendParent()
  • Removed the duckularize() type generation pattern (breaking change)
  • Replaced wrap() with simpler from() API
  • Added validate() utility for checking Mailbox protocol compliance
  • Full RxJS Observable interop via Symbol.observable
  • Migrated 49 tests to v5 patterns with educational comments

Reviewed changes

Copilot reviewed 111 out of 112 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/mailbox.ts New 524-line native v5 implementation with improved queue management
src/validate.ts New validation utility to check machines satisfy Mailbox protocol
src/nil.ts Updated null objects for v5 compatibility
src/mods/mod.ts Reorganized exports removing deprecated patterns
tests/xstate-behaviors/*.spec.ts Migrated XState behavior tests to v5 APIs
tests/mailbox.spec.ts New comprehensive mailbox tests with Observable API coverage
tests/integration.spec.ts Rewritten integration tests for v5
tests/machine-behaviors/*.spec.ts Educational tests demonstrating message loss without Mailbox
vitest.config.ts New Vitest configuration replacing tstest
tsconfig.json Updated with vitest types and test helper paths

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 10 to 13
* Note: XState v5's assign() action is different from v4. Side effects inside
* assign callbacks may not execute in the same order or timing as v4.
* For proper action ordering tests, use standalone action functions.
*/
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The comment states "XState v5's assign() action is different from v4" but the test doesn't actually demonstrate or verify this difference. The test uses standalone action functions instead of assign, which doesn't help validate the claim about assign() behavior changes. Consider either removing the misleading comment or adding a proper test that demonstrates the assign() differences.

Copilot uses AI. Check for mistakes.
src/validate.ts Outdated
Comment on lines 107 to 118
const actor = createActor(containerMachine, {
inspect: (inspectionEvent: InspectionEvent) => {
if (inspectionEvent.type === '@xstate.event') {
const event = inspectionEvent.event as AnyEventObject
// Only capture mailbox protocol events
if (event.type === Type.ACTOR_IDLE || event.type === Type.ACTOR_REPLY) {
sentToParentEvents.push(event)
}
}
},
})
}

/**
* Throw if the machine is not a valid Mailbox-addressable machine
*
* Validate a state machine for satisfying the Mailbox address protocol:
* 1. skip all EVENTs send from mailbox itself (Mailbox.*)
* 2. send parent `events.ACTOR_IDLE()` event after each received events and back to the idle state
*
* @returns
* Success: will return true
* Failure: will throw an error
*/
function validate (
machine: StateMachine<any, any, any>,
): boolean {
/**
* invoke the machine within a parent machine
*/
const parentMachine = container(machine)

/**
* validate the machine initializing events
*/
const [ interpreter, eventList ] = validateInitializing(parentMachine)

/**
* Response each event with ACTOR_IDLE event
*
* a mailbox-addressable machine MUST send ACTOR_IDLE event to parent when it has finished process an event
* (or the mailbox will stop sending any new events to it because it stays in busy state)
*/
validateReceiveFormOtherEvent(interpreter, eventList)
/**
* Multiple events will get multiple ACTOR_IDLE event back
*/
validateReceiveFormOtherEvents(interpreter, eventList)

/**
* child machine should not reply any events.* events
*/
validateSkipMailboxEvents(interpreter, eventList)

interpreter.stop()

actor.start()
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The validate function should handle errors more gracefully. If the actor fails to start or throws an error during initialization, this will crash without a helpful error message. Consider wrapping the actor.start() call in a try-catch block and providing a clear validation error message.

Copilot uses AI. Check for mistakes.
src/mailbox.ts Outdated
},
guards: {
hasQueuedMessages: ({ context }) => context.queue.length > 0,
isNotMailboxEvent: ({ event }) => !isMailboxType(event.type) && !event.type.startsWith('xstate.'),
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The isNotMailboxEvent guard checks for both mailbox events AND xstate.* events, but xstate.* events are legitimate state machine events that should be allowed. This filtering logic appears incorrect - it will block legitimate XState system events from being processed. Consider removing the xstate.* check from this guard.

Copilot uses AI. Check for mistakes.
huan and others added 9 commits January 8, 2026 05:23
- Update README Quick Start to use setup() for proper type safety
- Remove misleading comment about assign() differences in action order test
- Add try-catch error handling in validate() for better error messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove @chatie/tsconfig and @chatie/eslint-config dependencies
- Add direct TypeScript and ESLint configuration
- Add @typescript-eslint/eslint-plugin and @typescript-eslint/parser
- Update coffee-maker-machine.ts and nested-mailbox-machine.ts to v5 native
- Simplify eslint rules (remove style rules handled by formatter)
- Package count reduced from ~1000 to ~588 dependencies

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace ESLint + plugins with Biome (single tool for lint + format)
- Remove @typescript-eslint/*, eslint, eslint-plugin-promise dependencies
- Add biome.json configuration
- Auto-format all source files with Biome
- Update npm scripts: lint:biome, format

Benefits:
- ~25x faster linting
- Single tool for lint + format
- Simpler configuration
- Reduced dependency count (removed 84 packages)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rename all test files from .spec.ts to .test.ts convention
- Update vitest.config.ts include patterns
- Update tsconfig.json include patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add inline Clock interface (no longer exported from xstate)
- Fix invoke configuration with type assertion
- Update Mailbox.actions return types to `any` for compatibility
- Fix reply callback signatures to use ({ context, event }) destructuring
- Add null checks for child actor references in tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Instead of defining a custom Clock interface that may drift from
XState's internal definition, extract the type directly from
ActorOptions<AnyActorLogic>['clock']. This ensures compatibility
with future XState versions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add .gitattributes to normalize line endings to LF on checkout
- Configure Biome formatter with lineEnding: "lf"

This fixes CI failures on Windows where Git's autocrlf setting
converts LF to CRLF, causing Biome to report formatting errors.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove @chatie/semver (unused after removing other @Chatie packages)
- Remove npm engine restriction (unnecessarily restrictive)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@huan huan merged commit 1cc9820 into main Jan 8, 2026
6 checks passed
@huan huan deleted the xstate-v5 branch January 8, 2026 11:11
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.

XState state.exit.actions & micro transitions will be executed in the next state

2 participants