-
Notifications
You must be signed in to change notification settings - Fork 7
feat!: Complete XState v5 native rewrite #12
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
Conversation
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>
Summary of ChangesHello @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
🧠 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 AssistThe 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
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 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
|
There was a problem hiding this 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
| 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 }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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 }, |
There was a problem hiding this 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(), andsendParent() - Removed the
duckularize()type generation pattern (breaking change) - Replaced
wrap()with simplerfrom()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.
| * 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. | ||
| */ |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
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.
src/validate.ts
Outdated
| 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() |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
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.
src/mailbox.ts
Outdated
| }, | ||
| guards: { | ||
| hasQueuedMessages: ({ context }) => context.queue.length > 0, | ||
| isNotMailboxEvent: ({ event }) => !isMailboxType(event.type) && !event.type.startsWith('xstate.'), |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
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.
- 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>
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
setup(),createActor(),sendTo(),sendParent(),assign()directlycreateRequirehack with standard ES module importsvalidate()function to check machines satisfy Mailbox protocolSymbol.observableBreaking Changes (v1.0.0)
duckularize()wrap()from()insteadtypesafe-actionsdependencyas constassertionsMigration Example
Before (v0.x with duckularize):
After (v1.0 with manual pattern):
Files Changed
mailbox.ts(core v5 impl),validate.ts(new),nil.ts,mods/src/duckula/(514 lines),src/actions/,src/from.ts,src/wrap.tsTest Results
Dependencies
Reduced to 3 runtime dependencies:
rxjs^7.5.5symbol-observable^4.0.0xstate^5.25.0Removed:
typesafe-actions(was only needed for duckularize)Test plan
from()validate()correctly detects invalid machines🤖 Generated with Claude Code
Fix #4 #7 #9