Skip to content

feat(ui): add Mosaic state machine — core library, hook, and tests#8947

Open
alexcarpenter wants to merge 3 commits into
mainfrom
feat/mosaic-machine-core
Open

feat(ui): add Mosaic state machine — core library, hook, and tests#8947
alexcarpenter wants to merge 3 commits into
mainfrom
feat/mosaic-machine-core

Conversation

@alexcarpenter

@alexcarpenter alexcarpenter commented Jun 22, 2026

Copy link
Copy Markdown
Member

Summary

Introduces packages/ui/src/mosaic/machine: a tiny, dependency-free state machine runtime for modelling multi-step UI flows (confirm dialogs, sign-in wizards, async operations) without XState or any external dependency.

  • createMachine — define states, transitions, guards, and async invocations with full TypeScript inference
  • createActor — runtime that drives the machine; restarts cleanly through React StrictMode double-mount
  • setup() — typed builder with after (delayed transitions) and fromPromise (typed async invoke)
  • assign — immutable context updater helper
  • useMachine / useActor / useSelector — React bindings with stale-closure protection and setContext for syncing external context changes

Tests

  • machine.test.ts — transitions, guards, context, error states
  • useMachine.test.tsx — StrictMode behaviour, setContext, onDone
  • patterns.test.ts — common usage patterns
  • types.test-d.ts — compile-time type assertions
  • wizard-migration.test.tsx — full before/after for migrating a hand-rolled reducer flow

Stack

  1. This PR — core library
  2. feat/mosaic-machine-sections — delete/leave-org section migrations
  3. feat/mosaic-machine-signin — sign-in flow machines + adoption guide

Test plan

  • turbo test --filter @clerk/ui passes on this branch in isolation

Summary by CodeRabbit

  • New Features

    • New Mosaic state machine system for UI state, transitions, guards, async invokes, and actor runtime control
    • Added React hooks integration: useMachine, useActor, useSelector, plus optional transition logging
    • Added helper APIs for building typed machines, including setup, createMachine, assign, and promise-based invokes
    • Added actor utilities, including snapshot “teleporting” via a mock actor
  • Documentation

    • Updated and expanded guides with “first machine” examples and end-to-end migration walkthroughs
    • Added skill guidance for authoring Mosaic-style machines
  • Tests

    • Extensive new test coverage for runtime behavior, hooks, typing, and wizard/pattern parity

Introduces packages/ui/src/mosaic/machine: a tiny, dependency-free
state machine runtime for modelling multi-step flows in the Mosaic UI.

- createMachine / createActor — define states, transitions, and async
  invocations (fromPromise) with full TypeScript inference
- setup() — typed builder with after (delayed transitions) and context
  helpers
- assign — immutable context updater
- useMachine / useActor / useSelector — React bindings with StrictMode
  safety, stale-closure protection, and setContext for syncing external
  context changes
- types.test-d.ts + three test suites covering the full API and the
  wizard-migration pattern
@changeset-bot

changeset-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: de57772

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

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

Not sure what this means? Click here to learn what changesets are.

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

@vercel

vercel Bot commented Jun 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jun 22, 2026 7:18pm
swingset Ready Ready Preview, Comment Jun 22, 2026 7:18pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

Introduces a new dependency-free, XState v5-style state machine runtime (packages/ui/src/mosaic/machine) including a TypeScript type system, createMachine/assign/setup factories, a full createActor runtime with async invoke/after-timer/recheck support, React hooks (useMachine, useActor, useSelector), comprehensive Vitest tests, and a documentation README plus Claude SKILL guide.

Changes

Mosaic State Machine Library

Layer / File(s) Summary
Type system and contracts
packages/ui/src/mosaic/machine/types.ts
Defines all shared interfaces, types, and constants: event shapes, guard/action primitives, TransitionConfig, invoke/after event types, StateConfig, MachineConfig, StateMachine, Actor interface, Snapshot, and CreateActorOptions.
assign, createMachine, and setup factories
packages/ui/src/mosaic/machine/assign.ts, packages/ui/src/mosaic/machine/createMachine.ts, packages/ui/src/mosaic/machine/setup.ts
Implements assign action creator with isAssignAction guard, createMachine descriptor factory, and setup<TContext, TEvent>() that pre-binds generics for createMachine, assign, and typed fromPromise.
Actor runtime
packages/ui/src/mosaic/machine/createActor.ts
Full actor lifecycle: state entry/exit with action sequencing, async invoke with cancellation tokens, after timer scheduling/cancellation, guarded always transitions, public API (start, stop, send, can, setContext, recheck), snapshot caching/notification, and mockActor for inert teleported initialization.
React hooks
packages/ui/src/mosaic/machine/useMachine.ts
Implements useActor (via useSyncExternalStore), useMachine (actor ownership with lifecycle, live context injection via useLayoutEffect, onDone callback), useSelector (memoized projection with custom equality), and useMachineLogger.
TypeScript type-safety tests
packages/ui/src/mosaic/machine/types.test-d.ts
Verifies compile-time constraints via expectTypeOf and @ts-expect-error: event-union narrowing on send/can, snapshot shape, assign updater signatures, createMachine state-key constraints, setup pre-binding, and fromPromise typed output inference.
Actor runtime unit tests
packages/ui/src/mosaic/machine/__tests__/machine.test.ts
Covers machine introspection, pre-start safety, transitions/guards/actions, always, async invoke, final states, recheck(), observable contract, context initialization precedence, mockActor, pure helpers, StrictMode restart, and after-timer behavior with fake timers.
Real-world UI pattern tests
packages/ui/src/mosaic/machine/__tests__/patterns.test.ts
Verifies eight Clerk UI coordination patterns: multi-entry loading, resend cooldown, error-clear-on-input, conditional initial state, post-invoke routing, typed fromPromise output, on-mount async invoke, and reactive value auto-switch.
React hook and wizard migration tests
packages/ui/src/mosaic/machine/__tests__/useMachine.test.tsx, packages/ui/src/mosaic/machine/__tests__/wizard-migration.test.tsx
Tests useMachine/useActor/useSelector integration (context injection, teleport, re-render scoping, onDone, live context). Builds and tests a createWizardMachine covering derived initial, NEXT/PREV/GOTO navigation, deriveStepper introspection, recheck() re-seating, boundary-signal proxy, and mockActor gated-step teleport.
Documentation and changeset
packages/ui/src/mosaic/machine/README.md, .claude/skills/mosaic-machine/SKILL.md, .changeset/mosaic-state-machine.md
Full machine README with first-machine walkthrough, building blocks, API table, no-op/recheck semantics, and ConfigureSSO Wizard migration example. Claude SKILL guide with do/don't conventions, setup typing guide, and React consumption patterns. Changeset version marker.

Sequence Diagram(s)

sequenceDiagram
  participant UI
  participant Hook
  participant Runtime
  participant Machine
  UI->>Hook: mount with machine and options
  Hook->>Runtime: createActor
  Runtime->>Machine: resolve initial state and context
  Hook->>Runtime: start
  UI->>Hook: send event
  Hook->>Runtime: send
  Runtime->>Machine: pick and take transition
  Runtime-->>Hook: publish snapshot
  Hook-->>UI: render with snapshot
  UI->>Hook: unmount
  Hook->>Runtime: stop
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested labels

core-3

Suggested reviewers

  • brkalow
  • Ephem

Poem

🐇 Hop, hop! A machine is born today,
States and events to light the way.
assign patches context neat and clean,
always guards flow through the in-between.
useMachine mounts, then stop() on leave —
A rabbit-built runtime you can trust and believe! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(ui): add Mosaic state machine — core library, hook, and tests' accurately describes the main change: introduction of a new state machine library with core APIs, React hooks, and comprehensive test coverage for managing multi-step UI flows.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

@pkg-pr-new

pkg-pr-new Bot commented Jun 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8947

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8947

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8947

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8947

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8947

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8947

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8947

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8947

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8947

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8947

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8947

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8947

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8947

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8947

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8947

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8947

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8947

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8947

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8947

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8947

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8947

commit: de57772

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-06-22T19:19:47.984Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on de57772.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (2)
packages/ui/src/mosaic/machine/setup.ts (1)

77-82: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Prefer a typed wrapper over double-casting in fromPromise (Line 81).

fn as unknown as ... hides contract drift. A tiny adapter preserves the same runtime behavior and keeps type checking honest.

💡 Proposed change
-      src: fn as unknown as InvokeConfig<TContext, TEvent, TOutput, TStates>['src'],
+      src: (context, _event) => fn(context),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/machine/setup.ts` around lines 77 - 82, The
double-casting pattern in the src property assignment within fromPromise hides
type contract drift and should be replaced with a typed wrapper function. Create
a wrapper function that takes the context and event parameters (matching
InvokeConfig src signature) but only passes the context parameter to the
original fn, discarding the event parameter. This wrapper preserves the exact
same runtime behavior while maintaining proper type safety without hidden casts.
Assign this wrapper function to src instead of using the double-cast pattern.
packages/ui/src/mosaic/machine/__tests__/machine.test.ts (1)

220-314: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add two invoke regression tests: sync-throw and guard-blocked completion no-op.

The invoke suite is strong, but it should also pin:

  1. invoke.src synchronous throw routes via onError (no actor crash), and
  2. onDone/onError transition blocked by target entry guard is a true no-op (no notify/ref churn).

As per coding guidelines, “Unit tests are required for all new functionality” and “Verify proper error handling and edge cases.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/machine/__tests__/machine.test.ts` around lines 220 -
314, Add two new test cases to the existing describe block 'createActor — invoke
(async)'. First, add a test that verifies when invoke.src throws synchronously,
the error is properly routed to onError and the actor does not crash. Second,
add a test that verifies when an onDone or onError transition has a target with
an entry guard that blocks the transition, it results in a true no-op without
any notification or reference management churn. Both tests should follow the
same pattern as the existing tests in the suite, using createMachine,
createActor, and tick utilities to verify the expected behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ui/src/mosaic/machine/createActor.ts`:
- Around line 145-154: The error handler for invoke.onError is calling commit()
unconditionally after takeTransition(), but takeTransition() can return false
when a transition is blocked by an entry guard. Modify the code to check the
return value of takeTransition() and only call commit() if it returns true. This
same pattern should also be applied to the onDone handler above this code block
to ensure no unnecessary commits occur on blocked transitions.
- Around line 139-156: The current Promise.resolve(invoke.src(context, event as
never)) eagerly evaluates the invoke.src call, which means synchronous errors
thrown from invoke.src escape the promise chain instead of being caught and
routed to the onError handler. To fix this, wrap the invoke.src call in a
deferred microtask by changing Promise.resolve(invoke.src(...)) to
Promise.resolve().then(() => invoke.src(...)) so that both synchronous and
asynchronous errors are consistently caught by the error rejection handler that
calls the onError transition.

In `@packages/ui/src/mosaic/machine/setup.ts`:
- Around line 41-85: The exported setup function at line 41 lacks an explicit
return type annotation, which violates the repository's TypeScript guidelines
for public APIs. Add an explicit return type to the setup function that
describes the object structure it returns, which includes the createMachine,
assign, and fromPromise methods with their respective signatures. The return
type should be defined immediately after the function's closing parenthesis and
before the opening brace of the function body.

---

Nitpick comments:
In `@packages/ui/src/mosaic/machine/__tests__/machine.test.ts`:
- Around line 220-314: Add two new test cases to the existing describe block
'createActor — invoke (async)'. First, add a test that verifies when invoke.src
throws synchronously, the error is properly routed to onError and the actor does
not crash. Second, add a test that verifies when an onDone or onError transition
has a target with an entry guard that blocks the transition, it results in a
true no-op without any notification or reference management churn. Both tests
should follow the same pattern as the existing tests in the suite, using
createMachine, createActor, and tick utilities to verify the expected behavior.

In `@packages/ui/src/mosaic/machine/setup.ts`:
- Around line 77-82: The double-casting pattern in the src property assignment
within fromPromise hides type contract drift and should be replaced with a typed
wrapper function. Create a wrapper function that takes the context and event
parameters (matching InvokeConfig src signature) but only passes the context
parameter to the original fn, discarding the event parameter. This wrapper
preserves the exact same runtime behavior while maintaining proper type safety
without hidden casts. Assign this wrapper function to src instead of using the
double-cast pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: d5481399-00a3-4632-9c85-99189d93cb08

📥 Commits

Reviewing files that changed from the base of the PR and between 7e3174a and dce39c1.

📒 Files selected for processing (14)
  • .changeset/mosaic-state-machine.md
  • .claude/skills/mosaic-machine/SKILL.md
  • packages/ui/src/mosaic/machine/README.md
  • packages/ui/src/mosaic/machine/__tests__/machine.test.ts
  • packages/ui/src/mosaic/machine/__tests__/patterns.test.ts
  • packages/ui/src/mosaic/machine/__tests__/useMachine.test.tsx
  • packages/ui/src/mosaic/machine/__tests__/wizard-migration.test.tsx
  • packages/ui/src/mosaic/machine/assign.ts
  • packages/ui/src/mosaic/machine/createActor.ts
  • packages/ui/src/mosaic/machine/createMachine.ts
  • packages/ui/src/mosaic/machine/setup.ts
  • packages/ui/src/mosaic/machine/types.test-d.ts
  • packages/ui/src/mosaic/machine/types.ts
  • packages/ui/src/mosaic/machine/useMachine.ts

Comment thread packages/ui/src/mosaic/machine/createActor.ts Outdated
Comment thread packages/ui/src/mosaic/machine/createActor.ts Outdated
Comment thread packages/ui/src/mosaic/machine/setup.ts Outdated
- createActor: wrap invoke src in Promise constructor so a synchronous
  throw routes to onError rather than escaping as an unhandled exception
  (Promise.resolve(fn()) evaluates fn() before the chain begins)
- createActor: guard commit() on takeTransition return value for
  onDone/onError handlers, matching the existing after-timer pattern
- setup: add SetupTools interface and explicit return type annotation
Promise.resolve(src()) returns the same native Promise with zero extra
microtask hops; the previous new Promise(r => r(thenable)) added two
hops via the thenable resolution procedure, causing the cooldown-timer
tests to read state before onDone had fired.

Add delete-organization-machine.ts test fixture to the core branch so
machine.test.ts and useMachine.test.tsx compile without a dep on the
sections layer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant