diff --git a/.changeset/oxlint-conditional-none-event-handlers.md b/.changeset/oxlint-conditional-none-event-handlers.md deleted file mode 100644 index bb18bcef..00000000 --- a/.changeset/oxlint-conditional-none-event-handlers.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@typeonce/oxlint-plugin-effect-machine": patch ---- - -`effect-machine/no-conditional-none-branch` now checks only event handlers in `on`. `Machine.can` queries events, so resolvers for invocation outcomes such as `onSnapshot` and `onDone`, and for eventless `always` transitions, may select a `none` branch conditionally: - -```ts -onSnapshot: { - branches: "progress", - resolve: ({ snapshot, select }) => snapshot.ready ? select.ready() : select.waiting() -} -``` - -Remove any suppression comments added for these resolvers. diff --git a/.changeset/transition-declarations.md b/.changeset/transition-declarations.md new file mode 100644 index 00000000..f384018c --- /dev/null +++ b/.changeset/transition-declarations.md @@ -0,0 +1,31 @@ +--- +"@typeonce/effect-machine": minor +"@typeonce/effect-machine-react": minor +"@typeonce/effect-machine-devtools": minor +"@typeonce/oxlint-plugin-effect-machine": minor +--- + +Declare conditional transitions inline, with `branches` next to `resolve`, so one transition can be read and edited alone. The compiler checks one against the other in both directions, and rejects a resolver that does not return a `select` call on every path: + +```ts +Pay: { + branches: [{ target: "Charging" }, { update: "root" }, { stay: true }, { decline: true }], + resolve: ({ event, select }) => { + if (event.amount < 0) return select.decline() + if (event.amount === 0) return select.stay() + return event.preview + ? select.update({ root: { total: event.amount } }) + : select.target("Charging", { data: { amount: event.amount } }) + } +} +``` + +To migrate: move each `make({ branches })` group into its transition and drop its keys and `title`, so `select.key(...)` becomes `select.target(path, ...)`, `select.update({ owner: value })`, `select.history(path)`, `select.initialize({ input })`, or `select.stay()`. Two keys that reached the same destination are now one entry. Remove `declinable: true`, list `{ decline: true }`, and replace the `decline()` context function with `select.decline()`. + +`none` is replaced: `{ none: true }` becomes `{ stay: true }`, `{ none: true, reenter: true }` becomes `{ reenter: true }`, and `{ none: true, resolve }` becomes `{ stay: true, commands }`. Every plain transition accepts `commands: (context, enqueue) => ...`. + +`update` is now a record with one owner, so `data` only ever means the target's value: `{ update: "root", data }` becomes `{ update: { root: data } }`, and `{ target, update: "Owner", data: () => ({ target, update }) }` becomes `{ target, data, update: { Owner: value } }`. Inside a `branches` list, an entry still names its owner as a string, `{ target, update: "Owner" }`, and the value goes in its `select` call. + +`guard` and `decline` now exist only on `on` handlers and `always`. On `onDone`, `onFailure`, `onElement`, `onSnapshot`, and `choice`, which have no other candidate, use `{ stay: true }` or select a destination. A retained transition's `branchKey` and a transition definition's `key` are now the entry as written, such as `target:Paid`, and `title` is removed. + +In the Oxlint plugin, `no-conditional-none-branch` becomes `no-conditional-stay-branch`, and `no-redundant-resolve` is removed because the compiler now rejects what it reported. Resolvers for invocation outcomes and `always` may select `stay` conditionally, because `Machine.can` queries only events. diff --git a/.oxlintrc.json b/.oxlintrc.json index 45551e82..5041edc0 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -73,10 +73,9 @@ "rules": { "effect-machine/no-async-planning-callback": "error", "effect-machine/no-browser-api-in-planning": "error", - "effect-machine/no-conditional-none-branch": "error", + "effect-machine/no-conditional-stay-branch": "error", "effect-machine/no-conflicting-invocation-identity": "error", "effect-machine/no-nondeterministic-planning": "error", - "effect-machine/no-redundant-resolve": "error", "effect-machine/prefer-inline-handle": "error" } } diff --git a/LLMS.md b/LLMS.md index f33a5aa4..48493a36 100644 --- a/LLMS.md +++ b/LLMS.md @@ -58,7 +58,7 @@ export const CounterMachine = Machine.make({ }, Running: { on: { - Increment: { update: "Running", data: ({ state }) => ({ count: state.count + 1 }) }, + Increment: { update: { Running: ({ state }) => ({ count: state.count + 1 }) } }, Stop: { target: "Idle" } } } @@ -437,24 +437,18 @@ const ReviewEvents = Machine.eventsFromSchemas( const ReviewMachine = Machine.make({ root: ReviewStates, - events: ReviewEvents, - branches: { - evaluate: { - accepted: { target: "Accepted" }, - rejected: { target: "Rejected" } - } - } + events: ReviewEvents }).handle({ initial: { target: "Pending" }, states: { Pending: { on: { Evaluate: { - branches: "evaluate", + branches: [{ target: "Accepted" }, { target: "Rejected" }], resolve: ({ event, select }) => event.score >= 80 - ? select.accepted() - : select.rejected() + ? select.target("Accepted") + : select.target("Rejected") } } }, @@ -469,31 +463,53 @@ Do not read the clock, generate randomness, call a service, or await work while choosing a transition. Receive such values in an event or produce them through state-owned work first. -A `{ none: true }` branch accepts its event without changing state, so -`Machine.can` reports `true` when a resolver selects it. When the event must be -refused, for example to disable a control, use `guard`, or declare -`declinable: true` and return `decline()` from the resolver. +`resolve` is checked against `branches` in both directions, and each `select` +call takes the values of the path it is given. An agent can edit this one +transition without reading anything else: a missing entry, an entry that is never +selected, and a path that returns nothing are all compile errors that name the fix. + +A `{ stay: true }` accepts its event without changing state, so `Machine.can` +reports `true` when a resolver selects it. When the event must be refused, for +example to disable a control, use `guard`, or list `{ decline: true }` and return +`select.decline()` from the resolver. `guard` and `decline` exist only on `on` +handlers and `always`: an invocation outcome, a completion, and a choice have no +other candidate, so they use `{ stay: true }` or select a destination. A declined +invocation outcome would strand the machine in its state. + +A `{ stay: true }` in one parallel region accepts the event, so it blocks a handler +for that event on a parallel ancestor. `{ decline: true }` lets that handler run. + +Enqueue work with `commands` on any plain transition, and with `enqueue` in +`resolve`: + +```ts +Cancelled: { + target: "Archived", + commands: (_, enqueue) => enqueue.emit(Emitted.Cancelled()) +} +``` When only an active state's value changes, use `update` to preserve its active descendants and running work: ```ts Changed: { - update: "Document", - data: ({ ancestors }) => ({ - ...ancestors.Document, - revision: ancestors.Document.revision + 1 - }) + update: { + Document: ({ ancestors }) => ({ + ...ancestors.Document, + revision: ancestors.Document.revision + 1 + }) + } } ``` Choose a valued source or retained ancestor explicitly. To change a parallel sibling, send an event handled by that sibling. Combine a destination and one -retained owner update with `{ target, update, data }`; the callback returns -`{ target: destinationInput, update: completeOwnerInput }`. Both values are -validated atomically, and destination entry sees the new owner value. A named -branch can declare the same pair when a resolver needs commands or nested -construction. Its selector requires `update: { data: ownerValue }` in the construction object. +retained owner update with `{ target, data, update: { Owner: ... } }`, where `data` +only ever means the target's value. Both values are validated atomically, and +destination entry sees the new owner value. A `branches` entry names the same pair +as `{ target, update: "Owner" }`, and its call passes the value: +`select.target(target, { data, update: { Owner: value } })`. The destination of `{ target, update, data }` can be a choice. The owner is updated before the choice resolves, so its resolver reads the new value through @@ -639,7 +655,7 @@ export const CounterMachine = Machine.make({ }, Running: { on: { - Increment: { update: "Running", data: ({ state }) => ({ count: state.count + 1 }) }, + Increment: { update: { Running: ({ state }) => ({ count: state.count + 1 }) } }, Stop: { target: "Idle" } } } @@ -732,9 +748,9 @@ that name. It is not a transition destination. `{ initialize: ... }` reconstructs root data and follows its initial declarations using fresh machine input: a value or a callback of the transition context. A machine without input declares `{ initialize: true }`. `initialize` -rejects `target`, `update`, `history`, `none`, `data`, and `decoded`. A named -branch declares `{ initialize: true }`, and its resolver passes the input with -`select.branch({ input })`. +rejects `target`, `update`, `history`, `stay`, `data`, and `decoded`. A `branches` +entry declares `{ initialize: true }`, and its resolver passes the input with +`select.initialize({ input })`. Initializing root retains its lifecycle unless it is reentered. Put a machine-wide reset handler at root and use `reenter: true` to exit and enter root as well as @@ -743,11 +759,11 @@ reentry, retained root work continues; it does not automatically acquire new construction values. Ordinary transition conflict and lifecycle rules still apply. This does not create a new machine reference or discard history records. -`{ update: "root", data: ... }` instead replaces root's complete value, +`{ update: { root: ... } }` instead replaces root's complete value, retaining its active children and running work. It takes no input and does not run initial constructors. Other source/ancestor updates follow the same rules. -### Inline and named construction +### Inline and branching construction Ordinary transitions stay inline: @@ -760,32 +776,21 @@ Load: { Targeting a compound or parallel state follows its initial declarations. Event transitions use `target`, never `initial`. `initial` names only handler startup -edges. `history` restores a declared history state; `none: true` accepts an +edges. `history` restores a declared history state; `stay: true` accepts an event without changing its target configuration. -Declare branch topology in `make` when a resolver chooses an outcome, builds an -explicit subtree, or enqueues commands: - -```ts -branches: { - open: { - checkout: { target: "Checkout" }, - unchanged: { none: true } - } -} -``` - -The resolver calls a constructor bound to that declaration: +List `branches` next to `resolve` when a transition chooses an outcome or builds +an explicit subtree. The compiler checks one against the other in both directions: ```ts Open: { - branches: "open", + branches: [{ target: "Checkout" }, { stay: true }], resolve: ({ event, select }) => event.open - ? select.checkout({ + ? select.target("Checkout", { data: { cartId: event.cartId }, states: { Review: { data: { total: event.total } } } }) - : select.unchanged() + : select.stay() } ``` @@ -800,35 +805,34 @@ requires every region. Source-local parallel construction can select a single region while retaining its active siblings, preserving the existing scope rules. Names under `states` cannot collide with construction metadata. -Declared branch targets remain inspectable without executing a resolver. +Each branch's destination remains inspectable without executing `resolve`. Dynamically selected descendants are visible after resolution; this API does not add static analysis of resolver bodies. ### Retained-owner updates -An inline combined transition keeps `target` and `update` paths separate: +An inline combined transition keeps the target's value and the owner's value +separate, so `data` only ever means the target's value: ```ts Save: { target: "Checkout.Saving", - update: "Checkout", - data: ({ event }) => ({ - target: { requestId: event.requestId }, - update: { cartId: event.cartId } - }) + data: ({ event }) => ({ requestId: event.requestId }), + update: { Checkout: ({ event }) => ({ cartId: event.cartId }) } } ``` -For the same pair declared as a named branch, construct both in one call: +A `branches` entry names the same pair, `{ target: "Checkout.Saving", update: "Checkout" }`, +and its call constructs both: ```ts -select.saved({ +select.target("Checkout.Saving", { data: { requestId: event.requestId }, - update: { data: { cartId: event.cartId } } + update: { Checkout: { cartId: event.cartId } } }) ``` -`update` is required only for a branch declaring an owner update. It is not an +`update` takes exactly one owner. It is not an arbitrary nested-state operation. The owner must be active and retained across the transition. Both complete values validate before either is committed. @@ -856,8 +860,9 @@ including required ancestors and parallel siblings. Its root data is state data, not startup input. `Machine.resume` restores a validated snapshot without rerunning startup constructors. -`guard` declines before construction. A resolver may return `decline()` only with -`declinable: true`. Effect, Stream, timer, logic, child invocation, completion, +`guard` declines before construction. A resolver may return `select.decline()` only +when `branches` lists `{ decline: true }`. Both exist only on `on` handlers and +`always`. Effect, Stream, timer, logic, child invocation, completion, entry/exit, and required outcome channels retain their existing contracts. ## Effect Atom and React @@ -1208,39 +1213,36 @@ Changes to `draftAtom` recompute acceptance. The event atom contains the event input itself rather than an `AsyncResult`. `can` is `true` whenever the active configuration selects a transition. A -`{ none: true }` branch is a selected transition: it accepts the event without -changing state. A resolver that falls back to a `none` branch therefore keeps -the button enabled: +`{ stay: true }` branch is a selected transition: it accepts the event without +changing state. A resolver that falls back to `stay` therefore keeps the button +enabled: ```ts // Before: an incomplete form still accepts Submitted, so `can` is true. -// In make: branches: { submit: { review: { target: "Reviewing" }, incomplete: { none: true } } } Submitted: { - branches: "submit", + branches: [{ target: "Reviewing" }, { stay: true }], resolve: ({ state, select }) => - state.email.length > 0 ? select.review() : select.incomplete() + state.email.length > 0 ? select.target("Reviewing") : select.stay() } ``` When the event must be refused, decline it. Use `guard` for an ordinary -transition, or `declinable: true` with `decline()` in a resolver: +transition, or `{ decline: true }` with `select.decline()` in a resolver: ```ts // After: an incomplete form declines Submitted, so `can` is false. Submitted: { target: "Reviewing", guard: ({ state }) => state.email.length > 0 } // A resolver that needs commands or construction declines explicitly. -// In make: branches: { submit: { review: { target: "Reviewing" } } } Submitted: { - branches: "submit", - declinable: true, - resolve: ({ state, select, decline }) => - state.email.length > 0 ? select.review() : decline() + branches: [{ target: "Reviewing" }, { decline: true }], + resolve: ({ state, select }) => + state.email.length > 0 ? select.target("Reviewing") : select.decline() } ``` -Keep `none` for events that are deliberately accepted and ignored. The -`effect-machine/no-conditional-none-branch` lint rule reports the conditional +Keep `stay` for events that are deliberately accepted and ignored. The +`effect-machine/no-conditional-stay-branch` lint rule reports the conditional fallback. Query acceptance during render through `AtomMachine.can(event)(machine)`. Do @@ -1504,12 +1506,10 @@ Reserve named branch groups and `resolve` for conditional outcomes, commands, or nested construction. The group lives in `make`; its resolver selects only constructors derived from those declarations. Avoid an extra branch group for an ordinary transition. Use `reenter: true` when the source must restart. -An empty targetless resolver simplifies to `{ none: true }`. - -A `{ none: true }` branch accepts its event, so `Machine.can` and +A `{ stay: true }` branch accepts its event, so `Machine.can` and `AtomMachine.can` return `true` when a resolver selects it. Review check: for every event that enables or disables a control, refuse it with `guard` or with -`declinable: true` and `decline()`. Keep `none` only for events that are +`{ decline: true }` and `select.decline()`. Keep `stay` only for events that are deliberately accepted and ignored. ### Choose React ownership or keyed family lookup diff --git a/ai-docs/src/10_modeling/01_create-a-machine.ts b/ai-docs/src/10_modeling/01_create-a-machine.ts index 7edc3460..cf533e2a 100644 --- a/ai-docs/src/10_modeling/01_create-a-machine.ts +++ b/ai-docs/src/10_modeling/01_create-a-machine.ts @@ -34,7 +34,7 @@ export const CounterMachine = Machine.make({ }, Running: { on: { - Increment: { update: "Running", data: ({ state }) => ({ count: state.count + 1 }) }, + Increment: { update: { Running: ({ state }) => ({ count: state.count + 1 }) } }, Stop: { target: "Idle" } } } diff --git a/ai-docs/src/10_modeling/index.md b/ai-docs/src/10_modeling/index.md index 5d3a4cc7..0f62ab71 100644 --- a/ai-docs/src/10_modeling/index.md +++ b/ai-docs/src/10_modeling/index.md @@ -50,7 +50,7 @@ export const CounterMachine = Machine.make({ }, Running: { on: { - Increment: { update: "Running", data: ({ state }) => ({ count: state.count + 1 }) }, + Increment: { update: { Running: ({ state }) => ({ count: state.count + 1 }) } }, Stop: { target: "Idle" } } } @@ -429,24 +429,18 @@ const ReviewEvents = Machine.eventsFromSchemas( const ReviewMachine = Machine.make({ root: ReviewStates, - events: ReviewEvents, - branches: { - evaluate: { - accepted: { target: "Accepted" }, - rejected: { target: "Rejected" } - } - } + events: ReviewEvents }).handle({ initial: { target: "Pending" }, states: { Pending: { on: { Evaluate: { - branches: "evaluate", + branches: [{ target: "Accepted" }, { target: "Rejected" }], resolve: ({ event, select }) => event.score >= 80 - ? select.accepted() - : select.rejected() + ? select.target("Accepted") + : select.target("Rejected") } } }, @@ -461,31 +455,53 @@ Do not read the clock, generate randomness, call a service, or await work while choosing a transition. Receive such values in an event or produce them through state-owned work first. -A `{ none: true }` branch accepts its event without changing state, so -`Machine.can` reports `true` when a resolver selects it. When the event must be -refused, for example to disable a control, use `guard`, or declare -`declinable: true` and return `decline()` from the resolver. +`resolve` is checked against `branches` in both directions, and each `select` +call takes the values of the path it is given. An agent can edit this one +transition without reading anything else: a missing entry, an entry that is never +selected, and a path that returns nothing are all compile errors that name the fix. + +A `{ stay: true }` accepts its event without changing state, so `Machine.can` +reports `true` when a resolver selects it. When the event must be refused, for +example to disable a control, use `guard`, or list `{ decline: true }` and return +`select.decline()` from the resolver. `guard` and `decline` exist only on `on` +handlers and `always`: an invocation outcome, a completion, and a choice have no +other candidate, so they use `{ stay: true }` or select a destination. A declined +invocation outcome would strand the machine in its state. + +A `{ stay: true }` in one parallel region accepts the event, so it blocks a handler +for that event on a parallel ancestor. `{ decline: true }` lets that handler run. + +Enqueue work with `commands` on any plain transition, and with `enqueue` in +`resolve`: + +```ts +Cancelled: { + target: "Archived", + commands: (_, enqueue) => enqueue.emit(Emitted.Cancelled()) +} +``` When only an active state's value changes, use `update` to preserve its active descendants and running work: ```ts Changed: { - update: "Document", - data: ({ ancestors }) => ({ - ...ancestors.Document, - revision: ancestors.Document.revision + 1 - }) + update: { + Document: ({ ancestors }) => ({ + ...ancestors.Document, + revision: ancestors.Document.revision + 1 + }) + } } ``` Choose a valued source or retained ancestor explicitly. To change a parallel sibling, send an event handled by that sibling. Combine a destination and one -retained owner update with `{ target, update, data }`; the callback returns -`{ target: destinationInput, update: completeOwnerInput }`. Both values are -validated atomically, and destination entry sees the new owner value. A named -branch can declare the same pair when a resolver needs commands or nested -construction. Its selector requires `update: { data: ownerValue }` in the construction object. +retained owner update with `{ target, data, update: { Owner: ... } }`, where `data` +only ever means the target's value. Both values are validated atomically, and +destination entry sees the new owner value. A `branches` entry names the same pair +as `{ target, update: "Owner" }`, and its call passes the value: +`select.target(target, { data, update: { Owner: value } })`. The destination of `{ target, update, data }` can be a choice. The owner is updated before the choice resolves, so its resolver reads the new value through diff --git a/ai-docs/src/20_root-api/index.md b/ai-docs/src/20_root-api/index.md index 1b398620..78e60876 100644 --- a/ai-docs/src/20_root-api/index.md +++ b/ai-docs/src/20_root-api/index.md @@ -74,9 +74,9 @@ that name. It is not a transition destination. `{ initialize: ... }` reconstructs root data and follows its initial declarations using fresh machine input: a value or a callback of the transition context. A machine without input declares `{ initialize: true }`. `initialize` -rejects `target`, `update`, `history`, `none`, `data`, and `decoded`. A named -branch declares `{ initialize: true }`, and its resolver passes the input with -`select.branch({ input })`. +rejects `target`, `update`, `history`, `stay`, `data`, and `decoded`. A `branches` +entry declares `{ initialize: true }`, and its resolver passes the input with +`select.initialize({ input })`. Initializing root retains its lifecycle unless it is reentered. Put a machine-wide reset handler at root and use `reenter: true` to exit and enter root as well as @@ -85,11 +85,11 @@ reentry, retained root work continues; it does not automatically acquire new construction values. Ordinary transition conflict and lifecycle rules still apply. This does not create a new machine reference or discard history records. -`{ update: "root", data: ... }` instead replaces root's complete value, +`{ update: { root: ... } }` instead replaces root's complete value, retaining its active children and running work. It takes no input and does not run initial constructors. Other source/ancestor updates follow the same rules. -### Inline and named construction +### Inline and branching construction Ordinary transitions stay inline: @@ -102,32 +102,21 @@ Load: { Targeting a compound or parallel state follows its initial declarations. Event transitions use `target`, never `initial`. `initial` names only handler startup -edges. `history` restores a declared history state; `none: true` accepts an +edges. `history` restores a declared history state; `stay: true` accepts an event without changing its target configuration. -Declare branch topology in `make` when a resolver chooses an outcome, builds an -explicit subtree, or enqueues commands: - -```ts -branches: { - open: { - checkout: { target: "Checkout" }, - unchanged: { none: true } - } -} -``` - -The resolver calls a constructor bound to that declaration: +List `branches` next to `resolve` when a transition chooses an outcome or builds +an explicit subtree. The compiler checks one against the other in both directions: ```ts Open: { - branches: "open", + branches: [{ target: "Checkout" }, { stay: true }], resolve: ({ event, select }) => event.open - ? select.checkout({ + ? select.target("Checkout", { data: { cartId: event.cartId }, states: { Review: { data: { total: event.total } } } }) - : select.unchanged() + : select.stay() } ``` @@ -142,35 +131,34 @@ requires every region. Source-local parallel construction can select a single region while retaining its active siblings, preserving the existing scope rules. Names under `states` cannot collide with construction metadata. -Declared branch targets remain inspectable without executing a resolver. +Each branch's destination remains inspectable without executing `resolve`. Dynamically selected descendants are visible after resolution; this API does not add static analysis of resolver bodies. ### Retained-owner updates -An inline combined transition keeps `target` and `update` paths separate: +An inline combined transition keeps the target's value and the owner's value +separate, so `data` only ever means the target's value: ```ts Save: { target: "Checkout.Saving", - update: "Checkout", - data: ({ event }) => ({ - target: { requestId: event.requestId }, - update: { cartId: event.cartId } - }) + data: ({ event }) => ({ requestId: event.requestId }), + update: { Checkout: ({ event }) => ({ cartId: event.cartId }) } } ``` -For the same pair declared as a named branch, construct both in one call: +A `branches` entry names the same pair, `{ target: "Checkout.Saving", update: "Checkout" }`, +and its call constructs both: ```ts -select.saved({ +select.target("Checkout.Saving", { data: { requestId: event.requestId }, - update: { data: { cartId: event.cartId } } + update: { Checkout: { cartId: event.cartId } } }) ``` -`update` is required only for a branch declaring an owner update. It is not an +`update` takes exactly one owner. It is not an arbitrary nested-state operation. The owner must be active and retained across the transition. Both complete values validate before either is committed. @@ -198,6 +186,7 @@ including required ancestors and parallel siblings. Its root data is state data, not startup input. `Machine.resume` restores a validated snapshot without rerunning startup constructors. -`guard` declines before construction. A resolver may return `decline()` only with -`declinable: true`. Effect, Stream, timer, logic, child invocation, completion, +`guard` declines before construction. A resolver may return `select.decline()` only +when `branches` lists `{ decline: true }`. Both exist only on `on` handlers and +`always`. Effect, Stream, timer, logic, child invocation, completion, entry/exit, and required outcome channels retain their existing contracts. diff --git a/ai-docs/src/30_react/index.md b/ai-docs/src/30_react/index.md index 5732d91d..8ed737a5 100644 --- a/ai-docs/src/30_react/index.md +++ b/ai-docs/src/30_react/index.md @@ -346,39 +346,36 @@ Changes to `draftAtom` recompute acceptance. The event atom contains the event input itself rather than an `AsyncResult`. `can` is `true` whenever the active configuration selects a transition. A -`{ none: true }` branch is a selected transition: it accepts the event without -changing state. A resolver that falls back to a `none` branch therefore keeps -the button enabled: +`{ stay: true }` branch is a selected transition: it accepts the event without +changing state. A resolver that falls back to `stay` therefore keeps the button +enabled: ```ts // Before: an incomplete form still accepts Submitted, so `can` is true. -// In make: branches: { submit: { review: { target: "Reviewing" }, incomplete: { none: true } } } Submitted: { - branches: "submit", + branches: [{ target: "Reviewing" }, { stay: true }], resolve: ({ state, select }) => - state.email.length > 0 ? select.review() : select.incomplete() + state.email.length > 0 ? select.target("Reviewing") : select.stay() } ``` When the event must be refused, decline it. Use `guard` for an ordinary -transition, or `declinable: true` with `decline()` in a resolver: +transition, or `{ decline: true }` with `select.decline()` in a resolver: ```ts // After: an incomplete form declines Submitted, so `can` is false. Submitted: { target: "Reviewing", guard: ({ state }) => state.email.length > 0 } // A resolver that needs commands or construction declines explicitly. -// In make: branches: { submit: { review: { target: "Reviewing" } } } Submitted: { - branches: "submit", - declinable: true, - resolve: ({ state, select, decline }) => - state.email.length > 0 ? select.review() : decline() + branches: [{ target: "Reviewing" }, { decline: true }], + resolve: ({ state, select }) => + state.email.length > 0 ? select.target("Reviewing") : select.decline() } ``` -Keep `none` for events that are deliberately accepted and ignored. The -`effect-machine/no-conditional-none-branch` lint rule reports the conditional +Keep `stay` for events that are deliberately accepted and ignored. The +`effect-machine/no-conditional-stay-branch` lint rule reports the conditional fallback. Query acceptance during render through `AtomMachine.can(event)(machine)`. Do diff --git a/ai-docs/src/40_review/index.md b/ai-docs/src/40_review/index.md index 103b1482..d851df49 100644 --- a/ai-docs/src/40_review/index.md +++ b/ai-docs/src/40_review/index.md @@ -41,12 +41,10 @@ Reserve named branch groups and `resolve` for conditional outcomes, commands, or nested construction. The group lives in `make`; its resolver selects only constructors derived from those declarations. Avoid an extra branch group for an ordinary transition. Use `reenter: true` when the source must restart. -An empty targetless resolver simplifies to `{ none: true }`. - -A `{ none: true }` branch accepts its event, so `Machine.can` and +A `{ stay: true }` branch accepts its event, so `Machine.can` and `AtomMachine.can` return `true` when a resolver selects it. Review check: for every event that enables or disables a control, refuse it with `guard` or with -`declinable: true` and `decline()`. Keep `none` only for events that are +`{ decline: true }` and `select.decline()`. Keep `stay` only for events that are deliberately accepted and ignored. ### Choose React ownership or keyed family lookup diff --git a/api-reference.config.json b/api-reference.config.json index b4a5f6b9..4809ddd3 100644 --- a/api-reference.config.json +++ b/api-reference.config.json @@ -172,12 +172,12 @@ "label": "Inline transitions" }, { - "reflection": "Machine.BranchDeclaration", - "label": "Named branch declarations" + "reflection": "Machine.BranchEntry", + "label": "Branch entries" }, { - "reflection": "Machine.DeclineCapability", - "label": "Declinable transitions" + "reflection": "Machine.BranchSelect", + "label": "Branch selection" }, { "reflection": "Enqueue", diff --git a/packages/devtools/src/MachineDocument.ts b/packages/devtools/src/MachineDocument.ts index f495d633..ca9b74b5 100644 --- a/packages/devtools/src/MachineDocument.ts +++ b/packages/devtools/src/MachineDocument.ts @@ -149,8 +149,7 @@ export const Branch = Schema.Union([ Schema.Struct({ ...BranchFields, type: Schema.tag("branch"), - key: Schema.String, - title: Schema.String + key: Schema.String }) ]) diff --git a/packages/devtools/src/internal/browser/chart-model.ts b/packages/devtools/src/internal/browser/chart-model.ts index 49c905a5..1c2cca23 100644 --- a/packages/devtools/src/internal/browser/chart-model.ts +++ b/packages/devtools/src/internal/browser/chart-model.ts @@ -89,7 +89,7 @@ const activityLabel = (activity: VisualizationActivity): string => { const transitionLabel = (transition: VisualizationTransition, branch: VisualizationBranch): string => { const trigger = triggerLabel(transition) - return branch.type === "branch" ? `${trigger} · ${branch.title}` : trigger + return branch.type === "branch" ? `${trigger} · ${branch.key}` : trigger } interface TriggerPresentation { @@ -118,7 +118,7 @@ const triggerPresentation = (transition: VisualizationTransition): TriggerPresen const visibleLabel = (transition: VisualizationTransition, branches: ReadonlyArray): string => { const presentation = triggerPresentation(transition) if (branches.length !== 1 || branches[0]?.type !== "branch") return presentation.label - return [presentation.label, branches[0].title].filter((part) => part.length > 0).join(" · ") + return [presentation.label, branches[0].key].filter((part) => part.length > 0).join(" · ") } const labelBadges = ( diff --git a/packages/devtools/src/internal/browser/example-machine.ts b/packages/devtools/src/internal/browser/example-machine.ts index 2c42b793..b84f06f9 100644 --- a/packages/devtools/src/internal/browser/example-machine.ts +++ b/packages/devtools/src/internal/browser/example-machine.ts @@ -96,11 +96,6 @@ const initialWorkflow = (): Machine.Machine.CompleteSnapshotContaining<{ readonly "": typeof States.node }, "application.workflow"> => snapshot export const machine = Machine.make({ - branches: { - transition1: { - destination: { target: "application.workflow.running", update: "application.workflow" } - } - }, id: "inspection-example", root: States, events: Machine.eventsFromSchemas(Start, Finish, Disconnect, Refresh) @@ -138,19 +133,27 @@ export const machine = Machine.make({ idle: { on: { Start: { - branches: "transition1", - resolve: ({ select: { destination: target } }) => - target({ + branches: [{ target: "application.workflow.running", update: "application.workflow" }], + resolve: ({ select }) => + select.target("application.workflow.running", { data: new Running({}), decoded: true, states: { editing: { data: new Editing({}), decoded: true } }, - update: { data: new Workflow({ document: "Machine.ts", unsavedChanges: 3 }), decoded: true } + update: { + "application.workflow": { + decoded: true, + data: new Workflow({ document: "Machine.ts", unsavedChanges: 3 }) + } + } }) }, Refresh: { - update: "application.workflow", - decoded: true, - data: () => (new Workflow({ document: "Machine.ts", unsavedChanges: 0 })) + update: { + "application.workflow": { + decoded: true, + data: () => (new Workflow({ document: "Machine.ts", unsavedChanges: 0 })) + } + } } } }, diff --git a/packages/devtools/src/internal/browser/hierarchy-routing-example.ts b/packages/devtools/src/internal/browser/hierarchy-routing-example.ts index 4a35281b..e4bce13e 100644 --- a/packages/devtools/src/internal/browser/hierarchy-routing-example.ts +++ b/packages/devtools/src/internal/browser/hierarchy-routing-example.ts @@ -29,12 +29,6 @@ const ReviewEvents = Machine.eventsFromSchemas(Schema.TaggedUnion({ const saveReview: Effect.Effect = Effect.succeed("deterministic-chart") const publishReview = Effect.succeed("deterministic-chart") export const hierarchyRoutingMachine = Machine.make({ - branches: { - transition1: { - save: { target: "Workflow.Saving", title: "Save the review" }, - invalid: { target: "Workflow.Review.Failed", title: "Show validation failure" } - } - }, effects: { source1: Effect.suspend(() => saveReview), source2: Effect.suspend(() => publishReview) }, id: "hierarchy-routing", root: ReviewStates, @@ -56,11 +50,11 @@ export const hierarchyRoutingMachine = Machine.make({ }, on: { Submit: { - branches: "transition1", + branches: [{ target: "Workflow.Saving" }, { target: "Workflow.Review.Failed" }], resolve: ({ event, select }) => event.route === "save" - ? select.save({}) - : select.invalid({ data: { message: "Add a title before continuing." } }) + ? select.target("Workflow.Saving", {}) + : select.target("Workflow.Review.Failed", { data: { message: "Add a title before continuing." } }) } }, states: { diff --git a/packages/devtools/src/internal/browser/invoke-outcomes-example.ts b/packages/devtools/src/internal/browser/invoke-outcomes-example.ts index da3869f2..c115e58e 100644 --- a/packages/devtools/src/internal/browser/invoke-outcomes-example.ts +++ b/packages/devtools/src/internal/browser/invoke-outcomes-example.ts @@ -122,12 +122,6 @@ const GalleryStates = Machine.state({ } }) export const invokeOutcomesMachine = Machine.make({ - branches: { - transition13: { - ready: { target: "Completed", title: "Worker reports ready" }, - waiting: { none: true, title: "Worker is still starting" } - } - }, effects: { source1: ({ state }: { readonly state: LoadingDocument @@ -209,8 +203,8 @@ export const invokeOutcomesMachine = Machine.make({ src: "source2", id: "document-updates", onElement: { - none: true, - resolve: ({ element }, enqueue) => { + stay: true, + commands: ({ element }, enqueue) => { enqueue.raise(GalleryInternalEvents.StreamValue({ value: element })) } }, @@ -255,11 +249,14 @@ export const invokeOutcomesMachine = Machine.make({ data: ({ error }) => (new Failed({ source: "process", message: String(error) })) }, onSnapshot: { - branches: "transition13", + branches: [{ target: "Completed" }, { stay: true }], resolve: ({ select, snapshot }) => snapshot.state === "ready" - ? select.ready({ data: new Completed({ source: "process", result: "ready" }), decoded: true }) - : select.waiting() + ? select.target("Completed", { + data: new Completed({ source: "process", result: "ready" }), + decoded: true + }) + : select.stay() } } }, diff --git a/packages/devtools/src/internal/browser/layout-resilience-example.ts b/packages/devtools/src/internal/browser/layout-resilience-example.ts index bbc8e876..8be455c9 100644 --- a/packages/devtools/src/internal/browser/layout-resilience-example.ts +++ b/packages/devtools/src/internal/browser/layout-resilience-example.ts @@ -48,16 +48,6 @@ const AuthEvents = Machine.eventsFromSchemas(Schema.TaggedUnion({ Submit: { route: Schema.Literals(["verification", "login", "invalid"]) } })) export const layoutResilienceMachine = Machine.make({ - branches: { - transition4: { - requestVerification: { - target: "Editing.RequestingVerification", - title: "Request a verification code" - }, - login: { target: "Editing.SubmittingLogin", title: "Submit password login" }, - invalid: { target: "Editing.Form.Failed", title: "Show validation failure" } - } - }, effects: { source1: ({ containingState }: { readonly containingState: { @@ -101,27 +91,34 @@ export const layoutResilienceMachine = Machine.make({ }, on: { EmailChanged: { - update: "Editing", - data: ({ ancestors: { "Editing": current }, event }) => ({ ...current, email: event.value }) + update: { + Editing: ({ ancestors: { "Editing": current }, event }) => ({ ...current, email: event.value }) + } }, LoginMethodChanged: { - update: "Editing", - data: ({ ancestors: { "Editing": current }, event }) => ({ ...current, loginMethod: event.value }) + update: { + Editing: ({ ancestors: { "Editing": current }, event }) => ({ ...current, loginMethod: event.value }) + } }, PasswordChanged: { - update: "Editing", - data: ({ ancestors: { "Editing": current }, event }) => ({ ...current, password: event.value }) + update: { + Editing: ({ ancestors: { "Editing": current }, event }) => ({ ...current, password: event.value }) + } }, Submit: { - branches: "transition4", + branches: [{ target: "Editing.RequestingVerification" }, { target: "Editing.SubmittingLogin" }, { + target: "Editing.Form.Failed" + }], resolve: ({ event, select }) => { if (event.route === "login") { - return select.login({}) + return select.target("Editing.SubmittingLogin", {}) } if (event.route === "verification") { - return select.requestVerification({}) + return select.target("Editing.RequestingVerification", {}) } - return select.invalid({ data: { message: "Enter valid authentication details." } }) + return select.target("Editing.Form.Failed", { + data: { message: "Enter valid authentication details." } + }) } } }, diff --git a/packages/devtools/src/internal/browser/parallel-completion-example.ts b/packages/devtools/src/internal/browser/parallel-completion-example.ts index 40dd5a68..68268613 100644 --- a/packages/devtools/src/internal/browser/parallel-completion-example.ts +++ b/packages/devtools/src/internal/browser/parallel-completion-example.ts @@ -206,8 +206,8 @@ export const parallelCompletionMachine = Machine.make({ src: "source1", id: "packing-sla", onDone: { - none: true, - resolve: (_, enqueue) => { + stay: true, + commands: (_, enqueue) => { enqueue.raise(ParallelInternalEvents.AutoShip()) } } diff --git a/packages/devtools/src/internal/browser/parallel-owner-routing-example.ts b/packages/devtools/src/internal/browser/parallel-owner-routing-example.ts index db519f1c..00318f4d 100644 --- a/packages/devtools/src/internal/browser/parallel-owner-routing-example.ts +++ b/packages/devtools/src/internal/browser/parallel-owner-routing-example.ts @@ -61,8 +61,8 @@ export const parallelOwnerRoutingChildMachine = Machine.make({ src: "source1", id: "signal-parent", input: (context) => context, - onDone: { none: true }, - onFailure: { none: true } + onDone: { stay: true }, + onFailure: { stay: true } } } } diff --git a/packages/devtools/src/internal/browser/planner-example.ts b/packages/devtools/src/internal/browser/planner-example.ts index 44b48c67..70689bdd 100644 --- a/packages/devtools/src/internal/browser/planner-example.ts +++ b/packages/devtools/src/internal/browser/planner-example.ts @@ -110,12 +110,6 @@ const States = Machine.state({ } }) export const plannerMachine = Machine.make({ - branches: { - transition1: { - urgent: { target: "Working", title: "Finish immediately" }, - normal: { target: "Working", title: "Wait in working" } - } - }, effects: { source1: Effect.suspend(() => Effect.never) }, id: "planner-example", root: States, @@ -163,17 +157,18 @@ export const plannerMachine = Machine.make({ Idle: { on: { Begin: { - branches: "transition1", + branches: [{ target: "Working" }, { target: "Finished" }], resolve: ({ event, self, select, state }, enqueue) => { + if (!event.approved) { + return select.target("Finished", { data: new Finished({ job: event.job }), decoded: true }) + } enqueue.emit(new Planned({ job: event.job })) enqueue.sendTo(self, Events.Cancel({ reason: "planner command example" })) if (event.priority === "urgent") { enqueue.raise(InternalEvents.AutoFinish()) } const working = new Working({ owner: state.owner, job: event.job }) - return event.priority === "urgent" - ? select.urgent({ data: working, decoded: true }) - : select.normal({ data: working, decoded: true }) + return select.target("Working", { data: working, decoded: true }) } } } diff --git a/packages/devtools/src/internal/browser/protocol-events-example.ts b/packages/devtools/src/internal/browser/protocol-events-example.ts index a2ad284c..23dfcaf7 100644 --- a/packages/devtools/src/internal/browser/protocol-events-example.ts +++ b/packages/devtools/src/internal/browser/protocol-events-example.ts @@ -58,10 +58,6 @@ const ChildStates = Machine.state({ } }) export const requiredParentChildMachine = Machine.make({ - branches: { - transition1: { destination: { target: "Working" } }, - transition3: { destination: { target: "Working" } } - }, effects: { source1: ({ parent, state }: { readonly parent: Machine.MachineTarget> @@ -84,11 +80,11 @@ export const requiredParentChildMachine = Machine.make({ Idle: { on: { BeginChildWork: { - branches: "transition1", - resolve: ({ event, select: { destination: target } }, enqueue) => { + branches: [{ target: "Working" }], + resolve: ({ event, select }, enqueue) => { enqueue.raise(ChildInternalEvents.Heartbeat({ percent: 25 })) enqueue.emit(ChildEmissions.ChildTrace({ message: `started ${event.job}` })) - return target({ data: new ChildWorking({ job: event.job, progress: 0 }), decoded: true }) + return select.target("Working", { data: new ChildWorking({ job: event.job, progress: 0 }), decoded: true }) } } } @@ -98,7 +94,7 @@ export const requiredParentChildMachine = Machine.make({ src: "source1", id: "report-progress-to-parent", input: (context) => context, - onDone: { none: true }, + onDone: { stay: true }, onFailure: { target: "Cancelled", decoded: true, @@ -107,10 +103,13 @@ export const requiredParentChildMachine = Machine.make({ }, on: { Heartbeat: { - branches: "transition3", - resolve: ({ event, state, select: { destination: target } }, enqueue) => { + branches: [{ target: "Working" }], + resolve: ({ event, state, select }, enqueue) => { enqueue.raise(ChildInternalEvents.CommitChildWork()) - return target({ data: new ChildWorking({ job: state.job, progress: event.percent }), decoded: true }) + return select.target("Working", { + data: new ChildWorking({ job: state.job, progress: event.percent }), + decoded: true + }) } }, CommitChildWork: { @@ -161,10 +160,6 @@ const ParentStates = Machine.state({ } }) export const parentProtocolMachine = Machine.make({ - branches: { - transition1: { destination: { target: "Supervising" } }, - transition3: { destination: { target: "Complete" } } - }, children: { source1: ProtocolChild }, id: "parent-child-protocol", root: ParentStates, @@ -235,7 +230,6 @@ class PublishOutside extends Schema.TaggedClass("OptionalParentP } const OptionalParentStates = Machine.state({ states: { Detached, Published } }) export const optionalParentMachine = Machine.make({ - branches: { transition1: { destination: { target: "Published" } } }, id: "optional-parent-protocol", root: OptionalParentStates, events: Machine.eventsFromSchemas(PublishOutside), @@ -250,12 +244,15 @@ export const optionalParentMachine = Machine.make({ Detached: { on: { PublishOutside: { - branches: "transition1", - resolve: ({ event, parent, select: { destination: target } }, enqueue) => { + branches: [{ target: "Published" }], + resolve: ({ event, parent, select }, enqueue) => { if (parent !== undefined) { enqueue.sendTo(parent, ParentEvents.ChildFinished({ result: event.result })) } - return target({ data: new Published({ deliveredToParent: parent !== undefined }), decoded: true }) + return select.target("Published", { + data: new Published({ deliveredToParent: parent !== undefined }), + decoded: true + }) } } } diff --git a/packages/devtools/src/internal/browser/text-tree.ts b/packages/devtools/src/internal/browser/text-tree.ts index ff538146..91be488a 100644 --- a/packages/devtools/src/internal/browser/text-tree.ts +++ b/packages/devtools/src/internal/browser/text-tree.ts @@ -62,7 +62,7 @@ const branchLabel = (branch: VisualizationBranch): string | undefined => { if (branch.target === null && branch.updates.length > 0) { return branch.type === "direct" ? `update ${branch.updates.join(", ")}` - : `[${branch.title}] update ${branch.updates.join(", ")}` + : `[${branch.key}] update ${branch.updates.join(", ")}` } if (branch.target === null) return undefined @@ -70,7 +70,7 @@ const branchLabel = (branch: VisualizationBranch): string | undefined => { const updates = branch.updates.length === 0 ? "" : ` / update ${branch.updates.join(", ")}` return branch.type === "direct" ? `→ ${target}${updates}` - : `[${branch.title}] → ${target}${updates}` + : `[${branch.key}] → ${target}${updates}` } const activityLabel = (definition: VisualizationActivity): string => { diff --git a/packages/devtools/src/internal/browser/transition-semantics-example.ts b/packages/devtools/src/internal/browser/transition-semantics-example.ts index 7d29272f..1411a633 100644 --- a/packages/devtools/src/internal/browser/transition-semantics-example.ts +++ b/packages/devtools/src/internal/browser/transition-semantics-example.ts @@ -129,18 +129,6 @@ const defaultWorkspaceSnapshot = () => ({ } }) export const transitionSemanticsMachine = Machine.make({ - branches: { - transition4: { - draft: { target: "Workspace.Draft", title: "Preferred route is draft" }, - review: { target: "Workspace.Review", title: "Preferred route is review" } - }, - transition7: { - review: { target: "Workspace.Review", title: "Enter the review flow" }, - publish: { target: "Workspace.Finished", title: "Publish without review" } - }, - transition14: { destination: { history: "Workspace.recent" } }, - transition15: { destination: { history: "Workspace.exact" } } - }, id: "transition-semantics", root: TransitionStates, events: Machine.eventsFromSchemas( @@ -183,12 +171,15 @@ export const transitionSemanticsMachine = Machine.make({ data: ({ event }) => (new Paused({ reason: event.reason })) }, BumpWorkspace: { - update: "Workspace", - decoded: true, - data: ({ state: current }) => (new Workspace({ - revision: current.revision + 1, - preferredRoute: current.preferredRoute - })) + update: { + Workspace: { + decoded: true, + data: ({ state: current }) => (new Workspace({ + revision: current.revision + 1, + preferredRoute: current.preferredRoute + })) + } + } } }, onDone: { @@ -199,11 +190,14 @@ export const transitionSemanticsMachine = Machine.make({ states: { Routing: { choice: { - branches: "transition4", + branches: [{ target: "Workspace.Draft" }, { target: "Workspace.Review" }], resolve: ({ containingState, select }) => containingState.preferredRoute === "review" - ? select.review({ data: new Review({ requestedBy: "initial route" }), decoded: true }) - : select.draft({ data: new Draft({ text: "", autosaves: 0 }), decoded: true }) + ? select.target("Workspace.Review", { + data: new Review({ requestedBy: "initial route" }), + decoded: true + }) + : select.target("Workspace.Draft", { data: new Draft({ text: "", autosaves: 0 }), decoded: true }) } }, Draft: { @@ -215,18 +209,23 @@ export const transitionSemanticsMachine = Machine.make({ }, Save: { target: "Workspace.AutoSaving", decoded: true, data: () => (new AutoSaving({})) }, Submit: { - branches: "transition7", + branches: [{ target: "Workspace.Review" }, { target: "Workspace.Finished" }], resolve: ({ event, select }) => event.mode === "publish" - ? select.publish({ data: new WorkspaceFinished({ result: "published directly" }), decoded: true }) - : select.review({ data: new Review({ requestedBy: event.requestedBy }), decoded: true }) + ? select.target("Workspace.Finished", { + data: new WorkspaceFinished({ result: "published directly" }), + decoded: true + }) + : select.target("Workspace.Review", { + data: new Review({ requestedBy: event.requestedBy }), + decoded: true + }) }, - Refresh: { none: true, reenter: true }, - Ignore: { none: true }, + Refresh: { reenter: true }, + Ignore: { stay: true }, MaybeHandle: { - none: true, - resolve: ({ decline, event }) => event.accept ? undefined : decline(), - declinable: true + branches: [{ stay: true }, { decline: true }], + resolve: ({ select, event }) => event.accept ? select.stay() : select.decline() } } }, @@ -285,8 +284,14 @@ export const transitionSemanticsMachine = Machine.make({ decoded: true, data: ({ event }) => (new Workspace({ revision: 0, preferredRoute: event.route })) }, - ResumeShallow: { branches: "transition14", resolve: ({ select: { destination: target } }) => target() }, - ResumeDeep: { branches: "transition15", resolve: ({ select: { destination: target } }) => target() }, + ResumeShallow: { + branches: [{ history: "Workspace.recent" }], + resolve: ({ select }) => select.history("Workspace.recent") + }, + ResumeDeep: { + branches: [{ history: "Workspace.exact" }], + resolve: ({ select }) => select.history("Workspace.exact") + }, Restart: { target: "Workspace", decoded: true, diff --git a/packages/devtools/src/internal/browser/visualizer-app.ts b/packages/devtools/src/internal/browser/visualizer-app.ts index be92516d..91e8ccf6 100644 --- a/packages/devtools/src/internal/browser/visualizer-app.ts +++ b/packages/devtools/src/internal/browser/visualizer-app.ts @@ -81,7 +81,7 @@ const renderBranch = ( ): HTMLElement => { const row = createElement("div", "branch-row") const main = createElement("div", "branch-main") - if (branch.type === "branch") main.append(badge(branch.title, "condition")) + if (branch.type === "branch") main.append(badge(branch.key, "condition")) if (branch.target !== null) { main.append(createElement("span", "branch-arrow", "→"), stateLink(branch.target, branch.target, navigate)) } else if (branch.selection.kind === "update") { @@ -158,7 +158,7 @@ const renderIncomingTransition = ( if (api === undefined) { details.push(["Selection", incoming.branch.selection.kind], ["Scope", incoming.branch.selection.scope]) } - if (incoming.branch.type === "branch") details.unshift(["Title", incoming.branch.title]) + if (incoming.branch.type === "branch") details.unshift(["Branch", incoming.branch.key]) card.append(metadata(details)) return card } diff --git a/packages/devtools/src/internal/browser/visualizer-model.ts b/packages/devtools/src/internal/browser/visualizer-model.ts index 3edec364..569baeab 100644 --- a/packages/devtools/src/internal/browser/visualizer-model.ts +++ b/packages/devtools/src/internal/browser/visualizer-model.ts @@ -77,7 +77,7 @@ const pathName = (path: string): string => JSON.stringify(path === "" ? "root" : /** Public declaration represented by a retained transition branch. */ export const branchTargetApi = (branch: VisualizationBranch): string | undefined => { const { selection } = branch - if (selection.kind === "none") return "{ none: true }" + if (selection.kind === "none") return "{ stay: true }" const path = selection.path if (path === null) return undefined if (selection.scope === "initial") return "initialConfiguration" diff --git a/packages/devtools/src/internal/machineDocument.ts b/packages/devtools/src/internal/machineDocument.ts index fa921529..8f18c5c6 100644 --- a/packages/devtools/src/internal/machineDocument.ts +++ b/packages/devtools/src/internal/machineDocument.ts @@ -152,7 +152,7 @@ export const make = ( } return branch.type === "direct" ? { ...common, type: "direct" } - : { ...common, type: "branch", key: branch.key, title: branch.title } + : { ...common, type: "branch", key: branch.key } }) } }) diff --git a/packages/devtools/src/internal/machineWalkthrough.ts b/packages/devtools/src/internal/machineWalkthrough.ts index 4d289588..fbb5116d 100644 --- a/packages/devtools/src/internal/machineWalkthrough.ts +++ b/packages/devtools/src/internal/machineWalkthrough.ts @@ -226,7 +226,7 @@ export const choices = (self: MachineWalkthrough.Session): ReadonlyArray { choice.trigger.type === "event" && choice.trigger.event === "Begin" ) assert.strictEqual(begin.length, 2) - assert.deepStrictEqual(begin.map(({ title }) => title), ["Finish immediately", "Wait in working"]) + assert.deepStrictEqual(begin.map(({ title }) => title), ["target:Working", "target:Finished"]) assert.isTrue(begin.every(({ decisions }) => decisions.includes("conditional-branch"))) assert.isTrue(begin.every(({ input }) => input !== null)) }) diff --git a/packages/devtools/test/internal/browser/InteractiveTextVisualization.test.ts b/packages/devtools/test/internal/browser/InteractiveTextVisualization.test.ts index ac2af167..e6c328d1 100644 --- a/packages/devtools/test/internal/browser/InteractiveTextVisualization.test.ts +++ b/packages/devtools/test/internal/browser/InteractiveTextVisualization.test.ts @@ -35,8 +35,7 @@ describe("Interactive text visualization", () => { branches: [{ id: "application.workflow.idle:transition:0:branch:0", type: "branch", - key: "destination", - title: "destination", + key: "target:application.workflow.running+update:application.workflow", target: "application.workflow.running", selection: { path: "application.workflow.running", kind: "state", scope: "branch" }, updates: ["application.workflow"] @@ -128,7 +127,7 @@ describe("Interactive text visualization", () => { assert.deepStrictEqual( begin?.branches.map((branch) => branchTargetApi(branch)), - ["{ target: \"Working\" }", "{ target: \"Working\" }"] + ["{ target: \"Working\" }", "{ target: \"Finished\" }"] ) assert.strictEqual( @@ -185,7 +184,7 @@ describe("Interactive text visualization", () => { kind: "none", scope: "local" }), - "{ none: true }" + "{ stay: true }" ) assert.strictEqual(api("application", document.initial.selection), "initialConfiguration") }) diff --git a/packages/devtools/test/internal/browser/StaticChart.test.ts b/packages/devtools/test/internal/browser/StaticChart.test.ts index 1c8e0d72..19c084ad 100644 --- a/packages/devtools/test/internal/browser/StaticChart.test.ts +++ b/packages/devtools/test/internal/browser/StaticChart.test.ts @@ -453,15 +453,14 @@ describe("Static chart", () => { accessibleLabel: edge.accessibleLabel, badges: edge.badges })), - [{ - label: "Begin", - accessibleLabel: "Begin · 2 branches", - badges: [{ type: "branches", count: 2 }] - }] + [ + { label: "Begin · target:Working", accessibleLabel: "Begin · target:Working", badges: [] }, + { label: "Begin · target:Finished", accessibleLabel: "Begin · target:Finished", badges: [] } + ] ) assert.deepStrictEqual( model.edges.find((edge) => edge.transitionId === "Idle:transition:0")?.branchIds, - ["Idle:transition:0:branch:0", "Idle:transition:0:branch:1"] + ["Idle:transition:0:branch:0"] ) assert.deepStrictEqual(model.initials.map(({ target }) => target), ["", "Idle"]) }) @@ -506,8 +505,7 @@ describe("Static chart", () => { "choice", "failure", "element", - "snapshot", - "branches" + "snapshot" ]) ) @@ -770,7 +768,7 @@ describe("Static chart", () => { const source = layout.nodes.find(({ node }) => node.path === "Workflow.Review") const target = layout.nodes.find(({ node }) => node.path === "Workflow.Review.Failed") const parentTransition = layout.edges.find((edge) => - edge.kind === "transition" && edge.edge.label === "Submit · Show validation failure" + edge.kind === "transition" && edge.edge.label === "Submit · target:Workflow.Review.Failed" ) const externalFailure = layout.edges.find((edge) => edge.kind === "transition" && diff --git a/packages/effect-machine-react/test/RootContext.test.tsx b/packages/effect-machine-react/test/RootContext.test.tsx index 754d55be..8d166c48 100644 --- a/packages/effect-machine-react/test/RootContext.test.tsx +++ b/packages/effect-machine-react/test/RootContext.test.tsx @@ -19,7 +19,7 @@ const counter = Machine.make({ target: "Open" }, root: ({ input }) => ({ count: input }), - on: { Increment: { update: "root", data: ({ root: current }) => ({ count: current.count + 1 }) } }, + on: { Increment: { update: { root: ({ root: current }) => ({ count: current.count + 1 }) } } }, states: { Open: { on: { Close: { target: "Closed" } } }, Closed: {} diff --git a/packages/effect-machine-react/test/ServerRendering.test.tsx b/packages/effect-machine-react/test/ServerRendering.test.tsx index 8f7854e1..805ef078 100644 --- a/packages/effect-machine-react/test/ServerRendering.test.tsx +++ b/packages/effect-machine-react/test/ServerRendering.test.tsx @@ -285,7 +285,7 @@ const QuantityMachine = Machine.make({ }).handle({ root: ({ input }) => ({ quantity: input }), initial: { target: "Counting" }, - on: { Set: { update: "root", data: ({ event }) => ({ quantity: event.quantity }) } }, + on: { Set: { update: { root: ({ event }) => ({ quantity: event.quantity }) } } }, states: { Counting: {} } }) const QuantityChild = Machine.child("quantity", QuantityMachine) diff --git a/packages/effect-machine-react/typetest/MachineAtom.tst.ts b/packages/effect-machine-react/typetest/MachineAtom.tst.ts index 536111f4..cf383e9f 100644 --- a/packages/effect-machine-react/typetest/MachineAtom.tst.ts +++ b/packages/effect-machine-react/typetest/MachineAtom.tst.ts @@ -22,7 +22,7 @@ const machine = Machine.make({ states: { Idle: { on: { - Continue: { none: true } + Continue: { stay: true } } } } diff --git a/packages/effect-machine/README.md b/packages/effect-machine/README.md index aab48300..69aaf710 100644 --- a/packages/effect-machine/README.md +++ b/packages/effect-machine/README.md @@ -61,8 +61,7 @@ const Counter = Machine.make({ root: () => ({ count: 0 }), on: { Increment: { - update: "root", - data: ({ root, event }) => ({ count: root.count + event.by }) + update: { root: ({ root, event }) => ({ count: root.count + event.by }) } } } }) @@ -72,7 +71,7 @@ const program = Effect.scoped(Effect.gen(function*() { })) ``` -Root data is shared state. `update: "root"` replaces that value while +Root data is shared state. `update: { root: ... }` replaces that value while retaining the active child configuration and its running work. Ordinary transitions target a descendant; the root itself is not a destination. @@ -199,24 +198,18 @@ before any resolver executes: ```ts const machine = Machine.make({ root: Root, - events: Events, - branches: { - search: { - loading: { target: "Loading", title: "Query supplied" }, - idle: { target: "Idle", title: "Empty query" } - } - } + events: Events }).handle({ initial: { target: "Idle" }, states: { Idle: { on: { Search: { - branches: "search", + branches: [{ target: "Loading" }, { target: "Idle" }], resolve: ({ event, select }) => event.query.length > 0 - ? select.loading({ data: { query: event.query } }) - : select.idle() + ? select.target("Loading", { data: { query: event.query } }) + : select.target("Idle") } } } @@ -224,19 +217,24 @@ const machine = Machine.make({ }) ``` -Each selector is bound to its declared destination. Its constructor rejects a -payload belonging to another branch. A single-target group works the same way; -there is no second path declaration in the resolver. For a compound target, -`select.checkout({ data: parentValues, states: { Review: { data: childValues } } })` +The compiler checks `resolve` against `branches` in both directions: selecting an +undeclared entry and declaring an entry that is never selected are both errors, and +each one names the entry to add or remove. A resolver must return a `select` call on +every path, and `select.target` takes the values of the path it is given. For a +compound target, +`select.target("Checkout", { data: parentValues, states: { Review: { data: childValues } } })` constructs the explicitly selected subtree. Parallel constructors require every entered region; source-local construction can retain active sibling regions. Every compound has an initial edge, including inactive branches. Final outputs, history defaults, and choices remain part of machine readiness checking. -Use `guard: context => boolean` to decline before construction or commands. -For a resolver that can decline, explicitly add `declinable: true`; only then may -it return `context.decline()`. `reenter: true` restarts the handler's source -where that operation supports reentry. A retained source-value update does not -restart its lifecycle. +Use `guard: context => boolean` to decline before commands or construction, and +`commands: (context, enqueue) => ...` to enqueue work on any plain transition. A +resolver that can decline lists `{ decline: true }` and returns `select.decline()`. +`guard` and `decline` exist only on `on` handlers and `always`. An invocation +outcome, a completion, and a choice have no other candidate to fall through to, so +they use `{ stay: true }` or select a destination. `reenter: true` restarts the +handler's source where that operation supports reentry. A retained source-value +update does not restart its lifecycle. ### Select destinations and retain owners @@ -245,15 +243,26 @@ spelled the same way as snapshot paths. The root node is `"root"`, so a top-level state cannot use that name. Paths are checked at compile time and suggested by the editor: -| Declaration | Meaning | -| ------------------------------------------ | ------------------------------------------------------------------------- | -| `{ target: "Checkout.Review", data: ... }` | Enter a declared destination with its value. | -| `{ target: "Checkout", data: ... }` | Enter the declared initial configuration of a compound or parallel state. | -| `{ history: "Checkout.recent" }` | Restore a declared history state. | -| `{ update: "Checkout", data: ... }` | Replace a retained active owner's value. | -| `{ update: "root", data: ... }` | Replace root data and retain active descendants. | -| `{ initialize: ... }` | Reconstruct root and its initial children from fresh machine input. | -| `{ none: true }` | Accept an event without changing the configuration. | +| Plain transition | Entry in `branches` | Call in `resolve` | Meaning | +| ------------------------------------------ | -------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------- | +| `{ target: "Checkout.Review", data: ... }` | `{ target: "Checkout.Review" }` | `select.target("Checkout.Review", { data })` | Enter a declared destination with its value. | +| `{ target: "Checkout", data: ... }` | `{ target: "Checkout" }` | `select.target("Checkout", { data })` | Enter the declared initial configuration of a compound or parallel state. | +| `{ target, update: { Checkout: ... } }` | `{ target, update: "Checkout" }` | `select.target(target, { update: { Checkout: value } })` | Enter a destination and replace one retained owner's value. | +| `{ history: "Checkout.recent" }` | `{ history: "Checkout.recent" }` | `select.history("Checkout.recent")` | Restore a declared history state. | +| `{ update: { Checkout: ... } }` | `{ update: "Checkout" }` | `select.update({ Checkout: value })` | Replace a retained active owner's value. | +| `{ update: { root: ... } }` | `{ update: "root" }` | `select.update({ root: value })` | Replace root data and retain active descendants. | +| `{ initialize: ... }` | `{ initialize: true }` | `select.initialize({ input })` | Reconstruct root and its initial children from fresh machine input. | +| `{ stay: true }` | `{ stay: true }` | `select.stay()` | Accept an event without changing the configuration. | +| `{ reenter: true }` | | | Exit and reenter the source. | +| | `{ decline: true }` | `select.decline()` | Refuse the event, and let the next candidate handle it. | + +In a plain transition `update` is a record holding the new value, with exactly one +owner. A `branches` entry has no values yet, so it names the owner as a string; the +value is passed to `select`. `data` only ever means the target's value. + +A `{ stay: true }` in one parallel region accepts the event, so it blocks a handler +for that event on a parallel ancestor, while `{ decline: true }` lets that handler +run. Declarations written inline are checked in place. A declaration stored in a variable before it reaches `make` or `.handle` needs `as const` so its paths @@ -264,41 +273,34 @@ An atomic transition can enter a destination and update one retained owner: ```ts Save: { target: "Checkout.Saving", - update: "Checkout", - data: ({ event, ancestors }) => ({ - target: { request: event.request }, - update: { ...ancestors.Checkout, revision: event.revision } - }) + data: ({ event }) => ({ request: event.request }), + update: { Checkout: ({ event, ancestors }) => ({ ...ancestors.Checkout, revision: event.revision }) } } ``` The complete replacement values are validated before either change is applied. -Advanced construction declares both paths in a branch and uses -`select.saved({ data: destinationValues, update: { data: ownerValues } })`. A retained owner must be active for that source and remain active through the transition. A sibling region's value cannot be updated through this operation. -The destination can be a choice. The owner is updated first, so the choice -resolver reads the new value, and `data` constructs only the owner value: +The destination can be a choice, which has no `data` of its own. The owner is +updated first, so the choice resolver reads the new value: ```ts Bump: { target: "Route", - update: "root", - data: ({ root }) => ({ count: root.count + 1 }) + update: { root: ({ root }) => ({ count: root.count + 1 }) } } ``` Every branch the choice can reach, including through nested choices, must stay -below the owner. A declared branch selects it with -`select.route({ update: { data: ownerValues } })`. +below the owner. The runtime transition API does not replace arbitrary complete root configurations. The root is not a destination. `initialize` restarts it: it accepts fresh machine input, or `true` for a machine without input, and follows the root constructor and initial declarations in `.handle`. Add `reenter: true` to also exit and re-enter the root lifecycle. A branch declares -`{ initialize: true }` and its resolver supplies `select.branch({ input })`. +`{ initialize: true }` and its resolver supplies `select.initialize({ input })`. History defaults retain complete subtree construction for restoration. ### Protocols and ownership @@ -392,17 +394,17 @@ const invocations = [ { src: "updates", onElement: { - none: true, - resolve: ({ element }, enqueue) => { + stay: true, + commands: ({ element }, enqueue) => { enqueue.raise(Events.Changed({ value: element })) } }, - onDone: { none: true }, - onFailure: { none: true } + onDone: { stay: true }, + onFailure: { stay: true } }, { src: "timeout", onDone: { target: "Expired" } }, - { src: "worker", id: "worker", address: WorkerAddress, onSnapshot: { none: true } }, - { src: "validation", input: ({ state }) => ({ query: state.query }), onDone: { none: true } } + { src: "worker", id: "worker", address: WorkerAddress, onSnapshot: { stay: true } }, + { src: "validation", input: ({ state }) => ({ query: state.query }), onDone: { stay: true } } ] ``` @@ -697,8 +699,6 @@ pnpm install --frozen-lockfile pnpm check ``` -Use `guard: predicate` to decline before construction, or `declinable: true` -when a resolver can return `decline()`. Pull requests that change `src/` or `package.json` need a changeset and the performance checks described in `AGENTS.md`. diff --git a/packages/effect-machine/perf/runtime/effect-machine-compatibility.mjs b/packages/effect-machine/perf/runtime/effect-machine-compatibility.mjs index 86044792..7f8c0c4a 100644 --- a/packages/effect-machine/perf/runtime/effect-machine-compatibility.mjs +++ b/packages/effect-machine/perf/runtime/effect-machine-compatibility.mjs @@ -42,6 +42,15 @@ export const makeEffectMachineBenchmarkApi = (Machine) => { return false } })() + const hasStayTransitions = hasHandlerInitial && (() => { + try { + Machine.make({ root: Machine.state({ states: { Probe: {} } }), events: Machine.events({ Probe: {} }) }) + .handle({ initial: { target: "Probe" }, on: { Probe: { stay: true } } }) + return true + } catch { + return false + } + })() const rootDefinitions = new WeakMap() const transitionDefinition = Symbol("benchmark transition") const invocationDefinition = Symbol("benchmark invocation") @@ -207,7 +216,7 @@ export const makeEffectMachineBenchmarkApi = (Machine) => { : hasFluentTransitions ? fluentTransition(definition) : legacy, - targetless: hasObjectTransitions ? { none: true } : hasValueSelectors + targetless: hasObjectTransitions ? (hasStayTransitions ? { stay: true } : { none: true }) : hasValueSelectors ? (to) => to.none : hasStaticTransitions || hasFluentTransitions ? { target: Machine.targetless } diff --git a/packages/effect-machine/perf/types/composition.ts b/packages/effect-machine/perf/types/composition.ts index a49f8c1f..e05f340c 100644 --- a/packages/effect-machine/perf/types/composition.ts +++ b/packages/effect-machine/perf/types/composition.ts @@ -11,7 +11,6 @@ class CompositionService extends Context.Service()(" class CompositionFailure extends Data.TaggedError("CompositionFailure")<{}> { } const machine = Machine.make({ - branches: { enter: { app: { target: "App" } } }, root: States, events: Machine.eventsFromSchemas() }) @@ -85,9 +84,9 @@ const handled = machine.handle({ }, Route: { choice: { - branches: "enter", + branches: [{ target: "App" }], resolve: ({ select }) => - select.app({ + select.target("App", { data: App.make({}), states: { Workspace: { diff --git a/packages/effect-machine/perf/types/dynamic-invoke.ts b/packages/effect-machine/perf/types/dynamic-invoke.ts index e52c5bdb..7c1f5eaa 100644 --- a/packages/effect-machine/perf/types/dynamic-invoke.ts +++ b/packages/effect-machine/perf/types/dynamic-invoke.ts @@ -20,16 +20,16 @@ const invoked = machine.handle({ src: "load-user", input: ({ state }) => state.userId, onDone: { - none: true, - resolve: ({ output }) => { + stay: true, + commands: ({ output }) => { const user: User = output void user return undefined } }, onFailure: { - none: true, - resolve: ({ error }) => { + stay: true, + commands: ({ error }) => { const loadError: LoadError = error void loadError return undefined diff --git a/packages/effect-machine/perf/types/exact-channels.ts b/packages/effect-machine/perf/types/exact-channels.ts index dfe96f16..9ed2b0b3 100644 --- a/packages/effect-machine/perf/types/exact-channels.ts +++ b/packages/effect-machine/perf/types/exact-channels.ts @@ -7,7 +7,6 @@ type Equal = (() => Type extends Left ? 1 : 2) extends ( = Value type IsAny = 0 extends 1 & Value ? true : false const machine = Machine.make({ - branches: { finish: { done: { target: "Done" } } }, root: States, events: Machine.eventsFromSchemas(Start), internalEvents: Machine.internalEventsFromSchemas(Loaded), @@ -22,10 +21,10 @@ const complete = machine.handle({ entry: () => {}, on: { Start: { - branches: "finish", + branches: [{ target: "Done" }], resolve: ({ event, select }, enqueue) => { enqueue.emit(Notice.make({ value: event.value })) - return select.done({ data: Done.make({ value: event.value }) }) + return select.target("Done", { data: Done.make({ value: event.value }) }) } }, Loaded: { target: "Done", data: ({ event }) => ({ value: event.value }) } diff --git a/packages/effect-machine/perf/types/named-branches.ts b/packages/effect-machine/perf/types/named-branches.ts index b34cf555..801cc7cd 100644 --- a/packages/effect-machine/perf/types/named-branches.ts +++ b/packages/effect-machine/perf/types/named-branches.ts @@ -1,21 +1,6 @@ import { Machine } from "../../dist/index.js" import { Route, State, States } from "./named-branches-control.js" const machine = Machine.make({ - branches: { - route: { - length1: { target: "Text" }, - length2: { target: "Count" }, - length3: { target: "Text" }, - length4: { target: "Count" }, - length5: { target: "Text" }, - length6: { none: true }, - length7: { target: "Count" }, - length8: { target: "Text" }, - length9: { target: "Count" }, - length10: { target: "Idle" }, - unchanged: { none: true } - } - }, root: States, events: Machine.eventsFromSchemas(Route) }) @@ -25,32 +10,32 @@ const handled = machine.handle({ Idle: { on: { Route: { - branches: "route", + branches: [{ target: "Text" }, { target: "Count" }, { stay: true }, { target: "Idle" }], resolve: ({ event, select }) => { const value = event.value switch (value.length) { case 1: - return select.length1({ data: State.cases.Text.make({ value }) }) + return select.target("Text", { data: State.cases.Text.make({ value }) }) case 2: - return select.length2({ data: State.cases.Count.make({ value: value.length }) }) + return select.target("Count", { data: State.cases.Count.make({ value: value.length }) }) case 3: - return select.length3({ data: State.cases.Text.make({ value }) }) + return select.target("Text", { data: State.cases.Text.make({ value }) }) case 4: - return select.length4({ data: State.cases.Count.make({ value: value.length }) }) + return select.target("Count", { data: State.cases.Count.make({ value: value.length }) }) case 5: - return select.length5({ data: State.cases.Text.make({ value }) }) + return select.target("Text", { data: State.cases.Text.make({ value }) }) case 6: - return select.length6() + return select.stay() case 7: - return select.length7({ data: State.cases.Count.make({ value: value.length }) }) + return select.target("Count", { data: State.cases.Count.make({ value: value.length }) }) case 8: - return select.length8({ data: State.cases.Text.make({ value: value.toUpperCase() }) }) + return select.target("Text", { data: State.cases.Text.make({ value: value.toUpperCase() }) }) case 9: - return select.length9({ data: State.cases.Count.make({ value: value.length }) }) + return select.target("Count", { data: State.cases.Count.make({ value: value.length }) }) case 10: - return select.length10({ data: State.cases.Idle.make({}) }) + return select.target("Idle", { data: State.cases.Idle.make({}) }) default: - return select.unchanged() + return select.stay() } } } diff --git a/packages/effect-machine/perf/types/transition-construction.ts b/packages/effect-machine/perf/types/transition-construction.ts index 115bd791..333b6798 100644 --- a/packages/effect-machine/perf/types/transition-construction.ts +++ b/packages/effect-machine/perf/types/transition-construction.ts @@ -2,7 +2,6 @@ import { Schema } from "effect" import { Machine } from "../../dist/index.js" import { Root, Saved, States } from "./transition-construction-control.js" const machine = Machine.make({ - branches: { reset: { idle: { target: "Idle" }, same: { none: true } } }, root: States, events: Machine.events({ Save: { text: Schema.String }, Retry: {}, Reset: {} }) }) @@ -11,15 +10,15 @@ const handled = machine.handle({ target: "Idle" }, root: () => ({ count: 0 }), - on: { Reset: { update: "root", guard: ({ root }) => root.count > 0, data: () => ({ count: 0 }) } }, + on: { Reset: { update: { root: () => ({ count: 0 }) }, guard: ({ root }) => root.count > 0 } }, states: { Idle: { on: { Save: { target: "Saved", - update: "root", + update: { root: ({ root, event }) => ({ count: root.count + 1 }) }, guard: ({ event }) => event.text.length > 0, - data: ({ root, event }) => ({ target: { text: event.text }, update: { count: root.count + 1 } }) + data: ({ root, event }) => ({ text: event.text }) } } }, @@ -27,19 +26,16 @@ const handled = machine.handle({ on: { Save: { target: "Saved", - update: "root", + update: { root: { decoded: true, data: ({ root, event }) => (new Root({ count: root.count + 1 })) } }, reenter: true, decoded: true, - data: ({ root, event }) => ({ - target: new Saved({ text: event.text }), - update: new Root({ count: root.count + 1 }) - }) + data: ({ root, event }) => (new Saved({ text: event.text })) }, Retry: { target: "Saved", reenter: true, data: ({ state }) => ({ text: state.text }) }, Reset: { - branches: "reset", + branches: [{ target: "Idle" }, { stay: true }], reenter: true, - resolve: ({ state, select }) => state.text.length === 0 ? select.idle({}) : select.same() + resolve: ({ state, select }) => state.text.length === 0 ? select.target("Idle", {}) : select.stay() } } } diff --git a/packages/effect-machine/src/Machine.ts b/packages/effect-machine/src/Machine.ts index 38213b67..ee1b3871 100644 --- a/packages/effect-machine/src/Machine.ts +++ b/packages/effect-machine/src/Machine.ts @@ -112,7 +112,7 @@ type IsAny = 0 extends (1 & A) ? true : false * **Gotchas** * * Use `guard` to decline a transition before constructing its next state. - * Use `declinable: true` when a named branch resolver may return `decline()`. + * List `{ decline: true }` in `branches` when `resolve` may return `select.decline()`. * Register cancellable state-scoped delays in `make({ timers })`. * * **Example** (Declining an event with a guard) @@ -130,9 +130,8 @@ type IsAny = 0 extends (1 & A) ? true : false * root: () => ({ count: 0 }), * on: { * Add: { - * update: "root", - * guard: ({ event }) => event.by > 0, - * data: ({ root, event }) => ({ count: root.count + event.by }) + * update: { root: ({ root, event }) => ({ count: root.count + event.by }) }, + * guard: ({ event }) => event.by > 0 * } * } * }) @@ -320,8 +319,7 @@ export interface Definition< * Count: { * on: { * Increment: { - * update: "Count", - * decoded: ({ event, state }) => new Count({ value: state.value + event.by }) + * update: { Count: {} } * } * } * } @@ -3504,8 +3502,8 @@ export declare namespace Machine { } | { readonly type: "branch" + /** The entry as written: `target:Paid`, `target:Paid+update:root`, `update:root`, `history:X`, `initialize`, or `stay`. */ readonly key: string - readonly title: string readonly target: Path | undefined readonly selection: TransitionTargetSelection readonly updates: ReadonlyArray @@ -3571,7 +3569,7 @@ export declare namespace Machine { readonly reenter: boolean /** Zero-based index of the selected static branch. */ readonly branchIndex: number - /** Stable key of a named branch, or `undefined` for a direct transition. */ + /** The selected `branches` entry as written, such as `target:Paid`, or `undefined` for a direct transition. */ readonly branchKey: string | undefined /** Path returned by the handler, including a choice or history pseudo-state. */ readonly target: TargetPath | undefined @@ -4470,18 +4468,6 @@ export declare namespace Machine { } } - /** @internal */ - type StateUpdateBuilder< - States extends StateSchemas, - StateId extends ValuedStateIdentifier - > = { - readonly decoded: (value: StateByIdentifier) => StateUpdate - readonly from: FromCallable< - readonly [input: SchemaByIdentifier["~type.make.in"]], - StateUpdate - > - } - /** * Opaque result returned by an explicitly targetless transition. * @@ -4496,8 +4482,8 @@ export declare namespace Machine { } /** - * Opaque result returned when a declinable transition does not accept the - * current event or lifecycle outcome. + * Opaque result of `select.decline()`, when a transition does not accept the + * current event. * * Declining selects no transition and discards operations enqueued by that * resolver. Hierarchical event and eventless dispatch continues with the @@ -5111,9 +5097,8 @@ export declare namespace Machine { * * Handlers return snapshots for complete state replacement, target builder * results for path-safe partial transitions, state-value updates, or - * an explicit targetless result. In a machine definition, select `{ none: true }`; - * its optional resolver returns `undefined`. Raw decoded state values are - * not accepted as transition targets. + * an explicit targetless result. In a machine definition, declare `{ stay: true }`. + * Raw decoded state values are not accepted as transition targets. * * @category utility types * @since 0.4.0 @@ -5454,89 +5439,7 @@ export declare namespace Machine { Acceptance extends TransitionAcceptance = "required" > = Transition - export type SelectionBuilder = Selection extends TargetSelection ? Builder - : never - export type SelectionKind = Selection extends TargetSelection ? Kind : never - export type SelectionPath = Selection extends TargetSelection ? Path : never - export type TargetBuilderResult = - | (Builder extends (...args: any) => infer Result ? Result : never) - | (Builder extends { readonly decoded: (...args: any) => infer Result } ? Result : never) - | (Builder extends { readonly from: (...args: any) => infer Result } ? Result : never) - export type SelectedTargetResult = SelectionBuilder extends infer Builder ? - TargetBuilderResult - : never - - /** - * Destination construction returned when a transition declares one retained - * valued owner with a `{ target, update }` declaration. - * - * @category models - * @since 0.22.0 - */ - export interface UpdatingStateConstruction< - Result, - States extends StateSchemas, - Owner extends ValuedStateIdentifier - > { - /** Combines the selected topology with the required owner replacement. */ - readonly update: ( - update: StateUpdate - ) => CombinedTarget, States, Owner> - } - - type UpdatingCallable< - Callable, - States extends StateSchemas, - Owner extends ValuedStateIdentifier - > = Callable extends { - (...args: infer Arguments1): infer Result1 - (...args: infer Arguments2): infer Result2 - } ? { - (...args: Arguments1): UpdatingStateConstruction - (...args: Arguments2): UpdatingStateConstruction - } - : Callable extends (...args: infer Arguments) => infer Result ? - (...args: Arguments) => UpdatingStateConstruction - : never - - type UpdatingTargetBuilder< - Builder, - States extends StateSchemas, - Owner extends ValuedStateIdentifier - > = { - readonly [Key in keyof Builder]: Key extends "from" | "decoded" ? UpdatingCallable - : Builder[Key] - } - - type SelectionSupportsDefaultConstruction = SelectionKind extends "none" ? true - : SelectionBuilder extends { readonly from: (...args: infer Args) => any } ? [] extends Args ? true - : false - : false - - export type TransitionResolveContext< - Context, - Selection - > = - & Omit - & (SelectionKind extends "none" ? {} : { readonly target: SelectionBuilder }) - - /** Context capability available only to explicitly declinable resolvers. */ - export interface DeclineCapability { - /** Declines this candidate and continues hierarchical transition selection. */ - readonly decline: () => Declined - } - - /** One named destination declared by a branching transition. */ - export interface TransitionBranchInput< - Selection extends TargetSelection = TargetSelection - > { - /** Exact topology destination available to the branching resolver. */ - readonly target: Selection - /** Optional human-readable branch label used by visualization tooling. */ - readonly title?: string - } - - /** Opaque evidence that a branching resolver selected one declared branch. */ + /** Opaque evidence that `resolve` selected one declared branch. */ export interface SelectedBranch { readonly [Topology.SelectedBranchTypeId]: { readonly key: Types.Covariant @@ -5544,65 +5447,6 @@ export declare namespace Machine { } } - type CompletedBranchResult = R extends { readonly update: unknown } - ? R extends UpdatingStateConstruction - ? CombinedTarget, S, Owner> - : R - : R - type SelectedConstruction = R extends { readonly update: unknown } ? - R extends UpdatingStateConstruction ? { - readonly update: SelectedBranchBuilder, K> extends infer B ? { - readonly [M in keyof B]: - StateUpdateBuilder[Extract>] extends - (...args: infer A) => unknown - ? (...args: A) => SelectedBranch, S, Owner>> - : never - } : - never - } - : SelectedBranch : - SelectedBranch - type SelectedBranchCallable = Callable extends { - (...args: infer Arguments1): infer Result1 - (...args: infer Arguments2): infer Result2 - } ? { - (...args: Arguments1): SelectedConstruction - (...args: Arguments2): SelectedConstruction - } - : Callable extends (...args: infer Arguments) => infer Result ? - (...args: Arguments) => SelectedConstruction - : {} - - type SelectedBranchBuilderProperties = { - readonly [Property in keyof Builder]: Builder[Property] extends (...args: any) => any ? - SelectedBranchCallable - : Builder[Property] - } - - /** Target-specific builder that brands every construction with its branch key. */ - export type SelectedBranchBuilder = - & SelectedBranchCallable - & SelectedBranchBuilderProperties - - export type BranchSelectors>> = { - readonly [Key in Extract]: SelectedBranchBuilder< - SelectionBuilder, - Key - > - } - - export type BranchSelectionResult>> = { - readonly [Key in Extract]: SelectedBranch< - Key, - CompletedBranchResult> - > - }[Extract] - - export type TransitionBranchesResolveContext< - Context, - Branches extends Readonly> - > = Omit & { readonly select: BranchSelectors } - export type InvokeTransition< States extends StateSchemas, Events extends ReadonlyArray, @@ -6474,92 +6318,6 @@ export declare namespace Machine { Paths > type DestinationPath = Exclude, ""> - type RootSchemas = { readonly "": Root } - - /** A topology declaration; its state values are constructed by a resolver. */ - export type BranchDeclaration> = - & { - /** Optional presentation label for this named branch. */ - readonly title?: string - } - & ( - | { - /** Selects a declared descendant; its transition or bound branch constructor supplies required values. */ - readonly target: ReferenceUnion> - /** - * Replaces the complete value of a retained ancestor of the source and target without replacing its active - * children. A choice target resolves after the update, and every branch it can reach must stay below the owner. - */ - readonly update?: ReferenceUnion> - /** Enters a compound or parallel subtree through its declared initialization. */ - readonly initial?: never - /** Restores the declared history reference, using its fallback on first entry. */ - readonly history?: never - /** Accepts the event without selecting a new destination. */ - readonly none?: never - /** Reconstructs root and its initial children from fresh machine input. */ - readonly initialize?: never - } - | { - /** Reconstructs root and its initial children from fresh machine input supplied by the resolver. */ - readonly initialize: true - /** Selects a declared descendant; its transition or bound branch constructor supplies required values. */ - readonly target?: never - /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ - readonly update?: never - /** Enters a compound or parallel subtree through its declared initialization. */ - readonly initial?: never - /** Restores the declared history reference, using its fallback on first entry. */ - readonly history?: never - /** Accepts the event without selecting a new destination. */ - readonly none?: never - } - | { - /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ - readonly update: ReferenceUnion> - /** Selects a declared descendant; its transition or bound branch constructor supplies required values. */ - readonly target?: never - /** Enters a compound or parallel subtree through its declared initialization. */ - readonly initial?: never - /** Restores the declared history reference, using its fallback on first entry. */ - readonly history?: never - /** Accepts the event without selecting a new destination. */ - readonly none?: never - /** Reconstructs root and its initial children from fresh machine input. */ - readonly initialize?: never - } - | { - /** Restores the declared history reference, using its fallback on first entry. */ - readonly history: ReferenceUnion> - /** Selects a declared descendant; its transition or bound branch constructor supplies required values. */ - readonly target?: never - /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ - readonly update?: never - /** Enters a compound or parallel subtree through its declared initialization. */ - readonly initial?: never - /** Accepts the event without selecting a new destination. */ - readonly none?: never - /** Reconstructs root and its initial children from fresh machine input. */ - readonly initialize?: never - } - | { - /** Accepts the event without selecting a new destination. */ - readonly none: true - /** Selects a declared descendant; its transition or bound branch constructor supplies required values. */ - readonly target?: never - /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ - readonly update?: never - /** Enters a compound or parallel subtree through its declared initialization. */ - readonly initial?: never - /** Restores the declared history reference, using its fallback on first entry. */ - readonly history?: never - /** Reconstructs root and its initial children from fresh machine input. */ - readonly initialize?: never - } - ) - export type BranchDefinitions = Readonly< - Record>>> - > /** Source programs and named topology declarations captured by a machine definition. */ export interface Registrations { /** Lazy Effects or functions with one required input, invoked by registered name. */ @@ -6572,8 +6330,6 @@ export declare namespace Machine { readonly logic?: LogicSources /** Child machine descriptors registered for state-owned invocation. */ readonly children?: ChildSources - /** Named groups of inspectable destinations used by transition resolvers. */ - readonly branches?: Readonly>>> } type RegisteredInput = R extends { readonly "~input": infer I extends Schema.Top } ? I : typeof Schema.Void type Registered = Kind extends keyof R ? NonNullable : {} @@ -6626,86 +6382,68 @@ export declare namespace Machine { /** Fresh machine input used by root construction and root initial callbacks. */ readonly input: RegisteredInput["Type"] }) - type ObjectSelection< - S extends StateSchemas, - Src extends StateNodeIdentifier, - P extends StateNodeIdentifier, - R - > = P extends "" ? ConstructionCall, InitialTarget

> - : P extends ChoiceIdentifier ? () => ChoiceTarget - : P extends StateIdentifier ? ConstructionCall< - ConstructionObject< - NodeByIdentifier, - Src extends `${P}.${infer Rest}` ? Rest : Src extends P ? "" : never - > & { readonly update?: never }, - Target - > - : never - type SelectionOf, D, R = {}> = D extends { - readonly initialize: true - } ? TargetSelection>, R>, "", "state", "branch"> - : D extends { - readonly target: infer Ref - } ? - RefPath extends infer P extends StateNodeIdentifier ? - D extends { readonly update: infer Update } - ? RefPath extends - infer Owner extends Extract, ParentStateIdentifier

= Types.IsUnion

extends true + ? { readonly [K in `select.target takes one path, got: ${JoinUnion

}. Call it once per path`]: never } + : unknown + type OneOwner = Types.IsUnion extends true ? `update takes one owner, got: ${JoinUnion}` + : unknown + /** + * Selects one entry of a transition's `branches` list. The compiler checks that `resolve` selects exactly the + * declared entries, so each call here must match one of them. + * + * @category models + * @since 0.47.0 + */ + export interface BranchSelect, R> { + /** Selects `{ target }`, or `{ target, update }` when the construction has an `update` record. */ + readonly target: { +

, O extends ReferenceUnion>>( + path: P & OnePath

, + construction: TargetConstruction & { + /** The new value of the retained owner named by this branch. */ + readonly update: + & { readonly [Owner in O]: UpdateValue, StateIdentifier>>> } + & OneOwner + } + ): SelectedBranch< + `target:${P}+update:${O}`, + CombinedTarget, S, Extract, ValuedStateIdentifier>> + > +

>( + path: P & OnePath

, + ...construction: TargetArguments> + ): SelectedBranch<`target:${P}`, TargetResult> + } + /** Selects `{ update }`, replacing the value of one retained source or ancestor. */ + readonly update: >>( + updates: + & { readonly [Owner in O]: UpdateValue, StateIdentifier>>> } + & OneOwner + ) => SelectedBranch<`update:${O}`, StateUpdate, ValuedStateIdentifier>>> + /** Selects `{ history }`. */ + readonly history:

>>( + path: P + ) => SelectedBranch<`history:${P}`, HistoryTarget, HistoryIdentifier>>> + /** Selects `{ initialize: true }`. */ + readonly initialize: ConstructionCall, SelectedBranch<"initialize", InitialTarget<"">>> + /** Selects `{ stay: true }`. */ + readonly stay: () => SelectedBranch<"stay", NoTarget> + /** Selects `{ decline: true }`, passing the event on to the next candidate. */ + readonly decline: () => SelectedBranch<"decline", Declined> + } type ObjectPolicy = { /** Set to true to exit and reenter the handler source. */ readonly reenter?: Reenter extends true ? boolean : never @@ -6736,147 +6474,110 @@ export declare namespace Machine { */ readonly initialize: RegisteredInput extends typeof Schema.Void ? true : DataValue["Type"]> + /** Selects a declared descendant, constructing the values it requires. */ readonly target?: never + /** Replaces the value of one retained state without replacing its active children. */ readonly update?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Accepts the event and stays in the current states. */ + readonly stay?: never + /** The destinations this transition can select, each one checked against `resolve`. */ readonly branches?: never + /** Selects one declared branch and may enqueue synchronous commands. */ readonly resolve?: never - readonly history?: never - readonly none?: never } + type OwnerUpdates, C> = { + readonly [O in Owners as PathName]?: ShortConstruction> + } + type TargetOwner, P extends string> = Src extends + ChoiceIdentifier ? never + : Extract, ParentStateIdentifier

& ValuedStateIdentifier> + type UpdatableOwner> = Src extends ChoiceIdentifier ? + never + : Extract, ValuedStateIdentifier> type InlineDestination, C> = { readonly [P in DestinationPath]: & { - /** Selects a declared descendant; its transition or bound branch constructor supplies required values. */ + /** Selects a declared descendant, constructing the values it requires. */ readonly target: ReferenceAt - /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ - readonly update?: never - /** Enters a compound or parallel subtree through its declared initialization. */ - readonly initial?: never + /** + * Replaces the value of one retained ancestor of the source and target without replacing its active children. + * A choice target resolves after the update, and every branch it can reach must stay below the owner. + */ + readonly update?: OwnerUpdates, C> + /** Reconstructs root and its initial children from fresh machine input. */ + readonly initialize?: never /** Restores the declared history reference, using its fallback on first entry. */ readonly history?: never - /** Accepts the event without selecting a new destination. */ - readonly none?: never - /** Named groups of inspectable destinations used by transition resolvers. */ + /** Accepts the event and stays in the current states. */ + readonly stay?: never + /** The destinations this transition can select, each one checked against `resolve`. */ readonly branches?: never /** Selects one declared branch and may enqueue synchronous commands. */ readonly resolve?: never - /** Set to true when the resolver can explicitly return decline(). */ - readonly declinable?: never } & InlineDestinationConstruction }[DestinationPath] - type InlinePairConstruction< + type InlineUpdate, C> = [UpdatableOwner] extends + [never] ? never : { + /** Replaces the value of one retained source or ancestor without replacing its active children. */ + readonly update: OwnerUpdates, C> + /** Selects a declared descendant, constructing the values it requires. */ + readonly target?: never + /** Reconstructs root and its initial children from fresh machine input. */ + readonly initialize?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Accepts the event and stays in the current states. */ + readonly stay?: never + /** The destinations this transition can select, each one checked against `resolve`. */ + readonly branches?: never + /** Selects one declared branch and may enqueue synchronous commands. */ + readonly resolve?: never + } + /** + * One destination a `resolve` function can select. Here `update` names the owner as a string, because the value is + * passed to the `select` call. + * + * @category models + * @since 0.47.0 + */ + export type BranchEntry< S extends StateSchemas, Src extends StateNodeIdentifier, - P extends string, - Owner extends ValuedStateIdentifier, - C - > = P extends StateIdentifier ? - | { - readonly from: ( - context: C - ) => { - readonly target: [NodeSchema>] extends [never] ? undefined - : NodeMakeInput> - readonly update: SchemaByIdentifier["~type.make.in"] - } - readonly decoded?: never - } - | ([NodeSchema>] extends [never] ? never - : { - readonly decoded: ( - context: C - ) => { readonly target: NodeValue>; readonly update: StateByIdentifier } - readonly from?: never - }) - : P extends ChoiceIdentifier ? ObjectConstruction> - : never - type InlineCombined, C> = Src extends ChoiceIdentifier ? - never : - [Extract, ValuedStateIdentifier>] extends [never] ? never : - { + Acceptance extends TransitionAcceptance + > = + | { readonly [P in DestinationPath]: { - readonly [Owner in Extract, ParentStateIdentifier

& ValuedStateIdentifier>]: - & { - /** Selects a declared descendant; its transition or bound branch constructor supplies required values. */ - readonly target: ReferenceAt - /** - * Replaces the complete value of a retained ancestor of the source and target without replacing its active - * children. A choice target resolves after the update, and every branch it can reach must stay below the - * owner; `data` then constructs only the owner value. - */ - readonly update: ReferenceAt - /** Enters a compound or parallel subtree through its declared initialization. */ - readonly initial?: never - /** Restores the declared history reference, using its fallback on first entry. */ - readonly history?: never - /** Accepts the event without selecting a new destination. */ - readonly none?: never - /** Named groups of inspectable destinations used by transition resolvers. */ - readonly branches?: never - /** Selects one declared branch and may enqueue synchronous commands. */ - readonly resolve?: never - /** Set to true when the resolver can explicitly return decline(). */ - readonly declinable?: never - } - & InlinePairConstruction - }[Extract, ParentStateIdentifier

& ValuedStateIdentifier>] + /** Selects a declared descendant through `select.target`. */ + readonly target: ReferenceAt + /** The retained ancestor that this branch's `select.target` call updates. */ + readonly update?: ReferenceUnion> + } }[DestinationPath] - type InlineUpdate, C> = Src extends ChoiceIdentifier ? - never : - { - readonly [P in Extract, ValuedStateIdentifier>]: - & { - /** Set to true to exit and reenter the handler source. */ - readonly reenter?: P extends Src ? never : boolean - /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ - readonly update: ReferenceAt - /** Selects a declared descendant; its transition or bound branch constructor supplies required values. */ - readonly target?: never - /** Enters a compound or parallel subtree through its declared initialization. */ - readonly initial?: never - /** Restores the declared history reference, using its fallback on first entry. */ - readonly history?: never - /** Accepts the event without selecting a new destination. */ - readonly none?: never - /** Named groups of inspectable destinations used by transition resolvers. */ - readonly branches?: never - /** Selects one declared branch and may enqueue synchronous commands. */ - readonly resolve?: never - /** Set to true when the resolver can explicitly return decline(). */ - readonly declinable?: never - } - & ObjectConstruction> - }[Extract, ValuedStateIdentifier>] - type BranchSelections, Group, R> = { - readonly [K in Extract]: { - /** Selects a declared descendant; its transition or bound branch constructor supplies required values. */ - readonly target: SelectionOf + | ([UpdatableOwner] extends [never] ? never : { + /** The retained state updated through `select.update`. */ + readonly update: ReferenceUnion> + readonly target?: never + }) + | { + /** Restores this history reference through `select.history`. */ + readonly history: ReferenceUnion> } - } - type ObjectBranchResolver< - Ev extends ReadonlyArray, - Em extends ReadonlyArray, - C, - B extends Readonly>, - Declinable extends boolean - > = ( - context: C & { readonly select: BranchSelectors } & DeclineCapability, - enqueue: Enqueue, EmittedEventOf> - ) => BranchSelectionResult | (Declinable extends true ? Declined : never) - type InvalidBranchSelection, Group, R> = { - readonly [K in keyof Group]: Src extends ChoiceIdentifier ? Group[K] extends { - /** Accepts the event without selecting a new destination. */ - readonly none: true - } | { - /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ - readonly update: unknown - } ? K - : [SelectionBuilder>] extends [never] ? K - : never - : [SelectionBuilder>] extends [never] ? K - : never - }[keyof Group] + | { + /** Reconstructs root from fresh machine input through `select.initialize`. */ + readonly initialize: true + } + | (Src extends ChoiceIdentifier ? never : { + /** Accepts the event and stays in the current states through `select.stay`. */ + readonly stay: true + }) + | (Acceptance extends "declinable" ? { + /** Passes the event on to the next candidate through `select.decline`. */ + readonly decline: true + } : + never) type ObjectBranches< S extends StateSchemas, Src extends StateNodeIdentifier, @@ -6884,41 +6585,40 @@ export declare namespace Machine { Ev extends ReadonlyArray, Em extends ReadonlyArray, R, + Reenter extends boolean, Acceptance extends TransitionAcceptance > = { - readonly [K in Extract, string>]: - & ([InvalidBranchSelection[K], R>] extends [never] ? unknown : never) - & { - /** Named groups of inspectable destinations used by transition resolvers. */ - readonly branches: K - /** Selects a declared descendant; its transition or bound branch constructor supplies required values. */ - readonly target?: never - /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ - readonly update?: never - /** Enters a compound or parallel subtree through its declared initialization. */ - readonly initial?: never - /** Restores the declared history reference, using its fallback on first entry. */ - readonly history?: never - /** Accepts the event without selecting a new destination. */ - readonly none?: never - /** Constructs schema make input from the typed source context. */ - readonly from?: never - /** Supplies an already decoded schema value from the typed source context. */ - readonly decoded?: never - } - & { - /** Set to true when the resolver can explicitly return decline(). */ - readonly declinable?: "declinable" extends Acceptance ? boolean : false - /** Selects one declared branch and may enqueue synchronous commands. */ - readonly resolve: ObjectBranchResolver< - Ev, - Em, - C, - BranchSelections[K], R>, - true - > - } - }[Extract, string>] + /** The destinations this transition can select, each one checked against `resolve`. */ + readonly branches?: ReadonlyArray> + /** Selects one declared branch and may enqueue synchronous commands. */ + readonly resolve?: ( + context: C & { + /** Selects one of the declared `branches`. */ + readonly select: BranchSelect + }, + enqueue: Enqueue, EmittedEventOf> + ) => unknown + /** Set to true to exit and reenter the handler source. */ + readonly reenter?: Reenter extends true ? boolean : never + /** Branch on the condition inside `resolve`, returning `select.decline()`. */ + readonly guard?: never + /** Enqueue commands inside `resolve`. */ + readonly commands?: never + /** Constructs values through the `select` calls inside `resolve`. */ + readonly data?: never + /** Constructs values through the `select` calls inside `resolve`. */ + readonly decoded?: never + /** Selects a declared descendant, constructing the values it requires. */ + readonly target?: never + /** Replaces the value of one retained state without replacing its active children. */ + readonly update?: never + /** Reconstructs root and its initial children from fresh machine input. */ + readonly initialize?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Accepts the event and stays in the current states. */ + readonly stay?: never + } type DataValue = A | ((context: C) => A) type DeclarationData = T extends { readonly from: (context: infer C) => infer A } ? Omit & { readonly data: DataValue; readonly decoded?: false } @@ -6942,63 +6642,70 @@ export declare namespace Machine { Reenter extends boolean = false, Acceptance extends TransitionAcceptance = "required" > = - & ObjectPolicy - & DeclarationData< - ( + | ( + & ObjectPolicy + & { + /** Enqueues synchronous commands when this transition is selected. */ + readonly commands?: (context: C, enqueue: Enqueue, EmittedEventOf>) => void + } + & DeclarationData< | InlineDestination | InlineUpdate - | InlineCombined | InlineRoot | { /** Restores the declared history reference, using its fallback on first entry. */ readonly history: ReferenceUnion> - /** Selects a declared descendant; its transition or bound branch constructor supplies required values. */ + /** Selects a declared descendant, constructing the values it requires. */ readonly target?: never - /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + /** Replaces the value of one retained state without replacing its active children. */ readonly update?: never - /** Enters a compound or parallel subtree through its declared initialization. */ - readonly initial?: never - /** Accepts the event without selecting a new destination. */ - readonly none?: never - /** Named groups of inspectable destinations used by transition resolvers. */ + /** Reconstructs root and its initial children from fresh machine input. */ + readonly initialize?: never + /** Accepts the event and stays in the current states. */ + readonly stay?: never + /** The destinations this transition can select, each one checked against `resolve`. */ readonly branches?: never /** Selects one declared branch and may enqueue synchronous commands. */ readonly resolve?: never - /** Constructs schema make input from the typed source context. */ - readonly from?: never - /** Supplies an already decoded schema value from the typed source context. */ - readonly decoded?: never - /** Set to true when the resolver can explicitly return decline(). */ - readonly declinable?: never } | (Src extends ChoiceIdentifier ? never : { - /** Accepts the event without selecting a new destination. */ - readonly none: true - /** Selects a declared descendant; its transition or bound branch constructor supplies required values. */ + /** Accepts the event and stays in the current states. */ + readonly stay: true + /** Selects a declared descendant, constructing the values it requires. */ readonly target?: never - /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + /** Replaces the value of one retained state without replacing its active children. */ readonly update?: never - /** Enters a compound or parallel subtree through its declared initialization. */ - readonly initial?: never + /** Reconstructs root and its initial children from fresh machine input. */ + readonly initialize?: never /** Restores the declared history reference, using its fallback on first entry. */ readonly history?: never - /** Named groups of inspectable destinations used by transition resolvers. */ + /** The destinations this transition can select, each one checked against `resolve`. */ readonly branches?: never - /** Constructs schema make input from the typed source context. */ - readonly from?: never - /** Supplies an already decoded schema value from the typed source context. */ - readonly decoded?: never - /** Set to true when the resolver can explicitly return decline(). */ - readonly declinable?: "declinable" extends Acceptance ? boolean : false /** Selects one declared branch and may enqueue synchronous commands. */ - readonly resolve?: ( - context: C & DeclineCapability, - enqueue: Enqueue, EmittedEventOf> - ) => undefined | Declined + readonly resolve?: never }) - | ObjectBranches - ) - > + | (Src extends ChoiceIdentifier ? never : Reenter extends true ? { + /** Exits and reenters the source. */ + readonly reenter: true + /** Selects a declared descendant, constructing the values it requires. */ + readonly target?: never + /** Replaces the value of one retained state without replacing its active children. */ + readonly update?: never + /** Reconstructs root and its initial children from fresh machine input. */ + readonly initialize?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Accepts the event and stays in the current states. */ + readonly stay?: never + /** The destinations this transition can select, each one checked against `resolve`. */ + readonly branches?: never + /** Selects one declared branch and may enqueue synchronous commands. */ + readonly resolve?: never + } : + never) + > + ) + | ObjectBranches type SourceValue = { readonly [Kind in "effects" | "streams" | "timers" | "logic" | "children"]: K extends keyof Registered ? Registered[K] @@ -7072,7 +6779,7 @@ export declare namespace Machine { Em, R, true, - TransitionAcceptance + "required" > } : Channel extends "onFailure" ? { @@ -7085,7 +6792,7 @@ export declare namespace Machine { Em, R, true, - TransitionAcceptance + "required" > } : Channel extends "onElement" ? { @@ -7098,7 +6805,7 @@ export declare namespace Machine { Em, R, true, - TransitionAcceptance + "required" > } : { @@ -7111,7 +6818,7 @@ export declare namespace Machine { Em, R, true, - TransitionAcceptance + "required" > } type RegisteredInvokeError = SourceKind extends "children" @@ -7411,7 +7118,7 @@ export declare namespace Machine { Em, R, true, - TransitionAcceptance + "declinable" > } /** Eventless transition evaluated during stabilization. */ @@ -7423,7 +7130,7 @@ export declare namespace Machine { Em, R, false, - TransitionAcceptance + "declinable" > /** Handles successful invocation or state completion with its exact output type. */ readonly onDone?: Transition< @@ -7434,7 +7141,7 @@ export declare namespace Machine { Em, R, false, - TransitionAcceptance + "required" > /** Required total transition for a transient choice state. */ readonly choice?: never @@ -7460,9 +7167,102 @@ export declare namespace Machine { readonly history?: never }) : never) - type ValidateObjectTransition = + type LastOfUnion = Types.UnionToIntersection U : never> extends () => infer Last ? Last + : never + type JoinUnion = [U] extends [never] ? "" + : LastOfUnion extends infer Last extends string + ? [Exclude] extends [never] ? Last : `${JoinUnion>}, ${Last}` + : never + type BranchEntryKey = Entry extends + { readonly target: infer P extends string; readonly update: infer O extends string } ? `target:${P}+update:${O}` + : Entry extends { readonly target: infer P extends string } ? `target:${P}` + : Entry extends { readonly update: infer O extends string } ? `update:${O}` + : Entry extends { readonly history: infer P extends string } ? `history:${P}` + : Entry extends { readonly initialize: true } ? "initialize" + : Entry extends { readonly stay: true } ? "stay" + : Entry extends { readonly decline: true } ? "decline" + : never + type BranchLabel = Key extends `target:${infer P}+update:${infer O}` + ? `{ target: "${P}", update: "${O}" }` + : Key extends `target:${infer P}` ? `{ target: "${P}" }` + : Key extends `update:${infer O}` ? `{ update: "${O}" }` + : Key extends `history:${infer P}` ? `{ history: "${P}" }` + : Key extends "initialize" | "stay" | "decline" ? `{ ${Key}: true }` + : never + type BranchEntryKeyAt, K> = K extends keyof B ? BranchEntryKey : never + type DuplicateBranchKey, Indices extends keyof B = Extract> = { + readonly [K in Indices]: BranchEntryKeyAt extends BranchEntryKeyAt> + ? BranchEntryKeyAt + : never + }[Indices] + type SelectedBranchKey = Result extends SelectedBranch ? Key : never + type ValidateBranchSelection, Result> = number extends B["length"] ? + { readonly branches: "branches must list its entries inline" } + : B["length"] extends 0 ? { readonly branches: "branches needs at least one entry" } + : [DuplicateBranchKey] extends [never] ? + 0 extends 1 & Result ? { readonly resolve: "resolve must return a select call, got any" } + : unknown extends Result ? { readonly resolve: "resolve must return a select call, got unknown" } + : [Exclude>] extends [never] ? + [Exclude, BranchEntryKey>] extends [never] ? + [Exclude, SelectedBranchKey>] extends [never] ? unknown : { + readonly branches: `Declared branch ${BranchLabel< + Exclude, SelectedBranchKey> + > extends infer Label extends string ? JoinUnion

& ValuedStateIdentifier> - ? P extends StateIdentifier ? TargetSelection< - ConstructionCall< - ConstructionObject< - NodeByIdentifier, - Src extends `${P}.${infer Rest}` ? Rest : Src extends P ? "" : never - > & { - readonly update: ConstructionData> - }, - CombinedTarget, S, Owner> - >, - P, - "state", - "branch" - > - : P extends ChoiceIdentifier ? TargetSelection< - (construction: { - readonly update: ConstructionData> - }) => CombinedTarget, S, Owner>, - P, - "choice", - "branch" - > : - never - : never - : TargetSelection< - ObjectSelection, - P, - P extends ChoiceIdentifier ? "choice" : "state", - "branch" - > - : never - : D extends { readonly history: infer Ref } - ? RefPath extends infer P extends HistoryIdentifier - ? TargetSelection<() => HistoryTarget, P, "history", "full"> : - never - : D extends { readonly update: infer Ref } - ? RefPath extends infer P extends Extract, ValuedStateIdentifier> - ? TargetSelection< - ConstructionCall>, StateUpdate>, - P, - "update", - "branch" - > : - never - : D extends { readonly none: true } ? TargetSelection<() => NoTarget, never, "none", "local"> + type UpdateValue = OrdinaryData> | { + readonly decoded: true + readonly data: NodeValue + } + type TargetConstruction, P extends DestinationPath> = + P extends StateIdentifier ? + ConstructionObject, Src extends `${P}.${infer Rest}` ? Rest : Src extends P ? "" : never> + : {} + type TargetArguments = {} extends A ? [construction?: A & { readonly update?: never }] + : [construction: A & { readonly update?: never }] + type TargetResult = P extends ChoiceIdentifier ? ChoiceTarget + : P extends StateIdentifier ? Target : never - type BuilderArgs = K extends keyof B - ? B[K] extends (...args: infer A) => unknown ? A : never - : never - type ObjectConstruction = { - readonly [M in Extract]: BuilderArgs extends readonly [unknown?] ? - & { readonly [K in M]: (context: C) => BuilderArgs[0] } - & { readonly [K in Exclude<"from" | "decoded", M>]?: never } - : never - }[Extract] + type OnePath