@typeonce/effect-machine 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,6 +48,7 @@ export const inspection = internal.inspection;
48
48
  export const childEmissions = internal.childEmissions;
49
49
  const InvalidSelectorPathTypeId = "~effect/reactivity/AtomMachine/InvalidSelectorPath";
50
50
  const SelectorProjectionTypeId = "~effect/reactivity/AtomMachine/SelectorProjection";
51
+ const InvalidCanEventTypeId = "~effect/reactivity/AtomMachine/InvalidCanEvent";
51
52
  /**
52
53
  * Selects the typed value for an active state path.
53
54
  *
@@ -159,6 +160,33 @@ export const selectSnapshotChild = dual(2, internal.selectSnapshotChild);
159
160
  * @since 0.4.0
160
161
  */
161
162
  export const matches = dual(2, internal.matches);
163
+ /**
164
+ * Reactively tests whether a concrete event would be accepted by a running
165
+ * machine.
166
+ *
167
+ * Declare the returned projection once, then apply it to compatible machine
168
+ * bridges. Repeated applications to the same bridge return the same atom. An
169
+ * event atom is read reactively when acceptance depends on a changing payload.
170
+ *
171
+ * Startup remains in the source `AsyncResult`. Active snapshots use
172
+ * {@link Machine.can}; done and stopped snapshots produce `false`, while
173
+ * runtime and schema failures remain in the typed failure channel.
174
+ *
175
+ * **Example**
176
+ *
177
+ * ```ts
178
+ * const submitAllowed = AtomMachine.can(AuthEvents.Submitted())
179
+ * const canSubmitAtom = submitAllowed(authMachineAtom)
180
+ *
181
+ * const submitEvent = Atom.map(draftAtom, (draft) =>
182
+ * AuthEvents.Submitted({ draft }))
183
+ * const reactiveSubmitAllowed = AtomMachine.can(submitEvent)
184
+ * ```
185
+ *
186
+ * @category combinators
187
+ * @since 0.31.0
188
+ */
189
+ export const can = internal.can;
162
190
  /**
163
191
  * Returns whether a state path is active in a directly owned child.
164
192
  *
@@ -257,6 +285,26 @@ export const familyChild = internal.familyChild;
257
285
  * @since 0.4.0
258
286
  */
259
287
  export const make = internal.make;
288
+ /**
289
+ * Specializes a machine definition into a reusable bridge constructor.
290
+ *
291
+ * The returned function preserves the machine's startup input arity and exact
292
+ * bridge type. Every call creates a fresh `MachineAtom`; it does not cache by
293
+ * input or start the machine before an `AtomRegistry` reads or mounts it.
294
+ *
295
+ * **Example**
296
+ *
297
+ * ```ts
298
+ * const makeSearchMachine = AtomMachine.factory(searchMachine)
299
+ * const search = makeSearchMachine({ query: "effect" })
300
+ *
301
+ * type SearchMachineAtom = ReturnType<typeof makeSearchMachine>
302
+ * ```
303
+ *
304
+ * @category constructors
305
+ * @since 0.30.0
306
+ */
307
+ export const factory = internal.factory;
260
308
  /**
261
309
  * Creates a lazy atom bridge from a decoded logical snapshot.
262
310
  *
@@ -269,11 +317,12 @@ export const make = internal.make;
269
317
  */
270
318
  export const resume = internal.resume;
271
319
  /**
272
- * Creates an `AtomMachine` factory that owns a shared Effect runtime.
320
+ * Binds `AtomMachine` constructors to a shared Effect runtime.
273
321
  *
274
322
  * Use this when an application runs many machines from the same service layer.
275
- * The returned factory keeps runtime provisioning at the composition boundary,
276
- * while every call to `make` still creates an independent machine bridge.
323
+ * The returned interface keeps runtime provisioning at the composition seam,
324
+ * while every call to `make` or a specialized `factory` still creates an
325
+ * independent machine bridge.
277
326
  *
278
327
  * @category constructors
279
328
  * @since 0.4.0
@@ -1 +1 @@
1
- {"version":3,"file":"AtomMachine.js","sourceRoot":"","sources":["../../../src/unstable/reactivity/AtomMachine.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAMtC,OAAO,KAAK,QAAQ,MAAM,gCAAgC,CAAA;AAK1D;;;;;GAKG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAA;AAE9D;;;;;GAKG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AAMpE,MAAM,0BAA0B,GAAG,qDAAqD,CAAA;AAoIxF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAE+C,QAAQ,CAAC,SAAS,CAAA;AAEvF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAE+D,QAAQ,CAAC,UAAU,CAAA;AAEzG;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAMvB,QAAQ,CAAC,cAAc,CAAA;AAwJ3B,MAAM,yBAAyB,GAAG,oDAAoD,CAAA;AACtF,MAAM,wBAAwB,GAAG,mDAAmD,CAAA;AA0BpF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,MAAM,MAAM,GA0Cf,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;AAE5B;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GA0CvB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAA;AAEpC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,WAAW,GAsCpB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAA;AAEjC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAoC5B,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,MAAM,OAAO,GAsChB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;AAE7B;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GA2BrB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAA;AAElC,MAAM,uBAAuB,GAAG,kDAAkD,CAAA;AAkJlF,MAAM,yBAAyB,GAAG,oDAAoD,CAAA;AAyFtF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,MAAM,MAAM,GAkBgE,QAAQ,CAAC,MAAa,CAAA;AAEzG;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,WAAW,GAYqC,QAAQ,CAAC,WAAkB,CAAA;AAExF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,IAAI,GAqDb,QAAQ,CAAC,IAAI,CAAA;AAEjB;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,MAAM,GASf,QAAQ,CAAC,MAAM,CAAA;AAEnB;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,IAAI,GAEoB,QAAQ,CAAC,IAAI,CAAA"}
1
+ {"version":3,"file":"AtomMachine.js","sourceRoot":"","sources":["../../../src/unstable/reactivity/AtomMachine.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAMtC,OAAO,KAAK,QAAQ,MAAM,gCAAgC,CAAA;AAK1D;;;;;GAKG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAA;AAE9D;;;;;GAKG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAA;AAMpE,MAAM,0BAA0B,GAAG,qDAAqD,CAAA;AAoIxF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAE+C,QAAQ,CAAC,SAAS,CAAA;AAEvF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAE+D,QAAQ,CAAC,UAAU,CAAA;AAEzG;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAMvB,QAAQ,CAAC,cAAc,CAAA;AAwJ3B,MAAM,yBAAyB,GAAG,oDAAoD,CAAA;AACtF,MAAM,wBAAwB,GAAG,mDAAmD,CAAA;AACpF,MAAM,qBAAqB,GAAG,gDAAgD,CAAA;AAkD9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,MAAM,MAAM,GA0Cf,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;AAE5B;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GA0CvB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAA;AAEpC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,WAAW,GAsCpB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAA;AAEjC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAoC5B,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,MAAM,OAAO,GAsChB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,GAAG,GAGZ,QAAQ,CAAC,GAAG,CAAA;AAEhB;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GA2BrB,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAA;AAElC,MAAM,uBAAuB,GAAG,kDAAkD,CAAA;AAkJlF,MAAM,yBAAyB,GAAG,oDAAoD,CAAA;AAyGtF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,MAAM,MAAM,GAkBgE,QAAQ,CAAC,MAAa,CAAA;AAEzG;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,WAAW,GAYqC,QAAQ,CAAC,WAAkB,CAAA;AAExF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,IAAI,GAqDb,QAAQ,CAAC,IAAI,CAAA;AAEjB;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,OAAO,GAM+C,QAAQ,CAAC,OAAO,CAAA;AAEnF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,MAAM,GASf,QAAQ,CAAC,MAAM,CAAA;AAEnB;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,IAAI,GAEoB,QAAQ,CAAC,IAAI,CAAA"}
@@ -48,7 +48,7 @@ import { createContext, type ReactNode, useContext } from "react"
48
48
  import { AuthMachine, type AuthMachineInput } from "../machines/auth-machine"
49
49
  import { MachineAtoms } from "../lib/atom-runtime"
50
50
 
51
- const makeAuthMachine = (input: AuthMachineInput) => MachineAtoms.make(AuthMachine, input)
51
+ const makeAuthMachine = MachineAtoms.factory(AuthMachine)
52
52
  type AuthMachineAtom = ReturnType<typeof makeAuthMachine>
53
53
 
54
54
  const AuthMachineContext = createContext<AuthMachineAtom | null>(null)
@@ -183,6 +183,53 @@ function SubmitButton() {
183
183
  `useAtomSet` mounts the writable atom and does not subscribe the component to
184
184
  its value.
185
185
 
186
+ ## Query concrete event acceptance
187
+
188
+ `AtomMachine.can` turns one concrete event input into a reusable machine
189
+ projection. Declare the projection once, then apply it to the React-owned
190
+ machine. Repeated applications to the same machine return the same atom:
191
+
192
+ ```tsx
193
+ import { AtomMachine } from "@typeonce/effect-machine/reactivity"
194
+ import { useAtomSet, useAtomSuspense } from "@effect/atom-react"
195
+ import { AuthEvents } from "../machines/auth-machine"
196
+
197
+ const submitAllowed = AtomMachine.can(AuthEvents.Submitted())
198
+
199
+ function SubmitButton() {
200
+ const machine = useAuthMachine()
201
+ const canSubmit = useAtomSuspense(submitAllowed(machine)).value
202
+ const send = useAtomSet(machine.send)
203
+
204
+ return (
205
+ <button
206
+ disabled={!canSubmit}
207
+ onClick={() => send(AuthEvents.Submitted())}
208
+ >
209
+ Continue
210
+ </button>
211
+ )
212
+ }
213
+ ```
214
+
215
+ Startup still suspends, startup and runtime failures reach the error boundary,
216
+ and invalid event input for an active machine remains a
217
+ `MachineSchemaDecodeError`. Done and stopped machines return `false`.
218
+
219
+ When acceptance depends on a changing payload, project an event atom instead:
220
+
221
+ ```ts
222
+ import { Atom } from "effect/unstable/reactivity"
223
+
224
+ const submitEvent = Atom.map(draftAtom, (draft) =>
225
+ AuthEvents.Submitted({ draft }))
226
+
227
+ const submitAllowed = AtomMachine.can(submitEvent)
228
+ ```
229
+
230
+ Changes to `draftAtom` recompute acceptance. The event atom contains the event
231
+ input itself rather than an `AsyncResult`.
232
+
186
233
  ## Whole-result and custom selections
187
234
 
188
235
  Reading the full result is correct when a component renders the complete
@@ -71,7 +71,8 @@ Use `useMachineAtom` when one React subtree owns the workflow, including a
71
71
  machine with startup input:
72
72
 
73
73
  ```tsx
74
- const machine = useMachineAtom(() => machineAtoms.make(processMachine, input))
74
+ const makeProcessMachine = machineAtoms.factory(processMachine)
75
+ const machine = useMachineAtom(() => makeProcessMachine(input))
75
76
  ```
76
77
 
77
78
  Pass the stable machine through props or Context. Startup input is captured
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeonce/effect-machine",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "Schema-first state machines and statecharts for Effect",
5
5
  "author": "Sandro Maglione",
6
6
  "repository": {
package/src/Machine.ts CHANGED
@@ -9301,6 +9301,120 @@ export const enabled: <
9301
9301
  state: Machine.Snapshot<States>
9302
9302
  ) => ReadonlyArray<Machine.TagOf<Events[number]>> = internal.enabled as any
9303
9303
 
9304
+ /**
9305
+ * Tests whether a concrete event would select at least one transition from a
9306
+ * decoded snapshot.
9307
+ *
9308
+ * **Details**
9309
+ *
9310
+ * Required handlers are accepted from their structural eligibility.
9311
+ * Declinable handlers run their resolver only far enough to decide whether
9312
+ * they accept the event. Any commands, emissions, or raised events collected
9313
+ * during that check are discarded.
9314
+ *
9315
+ * Event input is decoded through the machine's public event protocol. Invalid
9316
+ * input fails with `MachineSchemaDecodeError`. Final snapshots and valid events
9317
+ * with no accepting handler return `false`.
9318
+ *
9319
+ * **Gotchas**
9320
+ *
9321
+ * This query does not execute transitions or stabilize the resulting machine.
9322
+ * It does not run entry, exit, always, completion, child lifecycle, or command
9323
+ * effects. A `true` result therefore describes event acceptance only.
9324
+ *
9325
+ * **Example**
9326
+ *
9327
+ * ```ts
9328
+ * const canCheckout = Machine.can(checkoutMachine)
9329
+ *
9330
+ * const canSubmit = yield* canCheckout(snapshot, {
9331
+ * _tag: "SubmitOrder"
9332
+ * })
9333
+ * ```
9334
+ *
9335
+ * @category getters
9336
+ * @since 0.30.0
9337
+ */
9338
+ export const can: {
9339
+ <
9340
+ const States extends Machine.StateSchemas,
9341
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
9342
+ const Emits extends ReadonlyArray<Machine.TaggedSchema>,
9343
+ const Input extends Schema.Top = typeof Schema.Void,
9344
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
9345
+ E = never,
9346
+ R = never,
9347
+ InitialE = never,
9348
+ InitialR = never,
9349
+ FinalStates extends Machine.StateIdentifier<States> = never,
9350
+ Output = never,
9351
+ OutputStates extends Machine.StateIdentifier<States> = never,
9352
+ InputEvents extends ReadonlyArray<Machine.TaggedSchema> = Events,
9353
+ ParentEvents extends ReadonlyArray<Machine.TaggedSchema> = readonly []
9354
+ >(
9355
+ machine:
9356
+ & Machine<
9357
+ States,
9358
+ Events,
9359
+ Input,
9360
+ UnhandledStates,
9361
+ E,
9362
+ R,
9363
+ InitialE,
9364
+ InitialR,
9365
+ FinalStates,
9366
+ Output,
9367
+ Emits,
9368
+ OutputStates,
9369
+ InputEvents,
9370
+ ParentEvents
9371
+ >
9372
+ & EnsureExecutable<States, UnhandledStates, OutputStates>
9373
+ & Machine.RootCompatible<ParentEvents>
9374
+ ): (
9375
+ state: Machine.Snapshot<States>,
9376
+ event: Machine.EventInputOf<InputEvents>
9377
+ ) => Effect.Effect<boolean, MachineSchemaDecodeError>
9378
+ <
9379
+ const States extends Machine.StateSchemas,
9380
+ const Events extends ReadonlyArray<Machine.TaggedSchema>,
9381
+ const Emits extends ReadonlyArray<Machine.TaggedSchema>,
9382
+ const Input extends Schema.Top = typeof Schema.Void,
9383
+ UnhandledStates extends Machine.StateIdentifier<States> = Machine.StateIdentifier<States>,
9384
+ E = never,
9385
+ R = never,
9386
+ InitialE = never,
9387
+ InitialR = never,
9388
+ FinalStates extends Machine.StateIdentifier<States> = never,
9389
+ Output = never,
9390
+ OutputStates extends Machine.StateIdentifier<States> = never,
9391
+ InputEvents extends ReadonlyArray<Machine.TaggedSchema> = Events,
9392
+ ParentEvents extends ReadonlyArray<Machine.TaggedSchema> = readonly []
9393
+ >(
9394
+ machine:
9395
+ & Machine<
9396
+ States,
9397
+ Events,
9398
+ Input,
9399
+ UnhandledStates,
9400
+ E,
9401
+ R,
9402
+ InitialE,
9403
+ InitialR,
9404
+ FinalStates,
9405
+ Output,
9406
+ Emits,
9407
+ OutputStates,
9408
+ InputEvents,
9409
+ ParentEvents
9410
+ >
9411
+ & EnsureExecutable<States, UnhandledStates, OutputStates>
9412
+ & Machine.RootCompatible<ParentEvents>,
9413
+ state: Machine.Snapshot<States>,
9414
+ event: Machine.EventInputOf<InputEvents>
9415
+ ): Effect.Effect<boolean, MachineSchemaDecodeError>
9416
+ } = internal.can as any
9417
+
9304
9418
  /**
9305
9419
  * Plans the next state snapshot synchronously.
9306
9420
  *
@@ -79,6 +79,8 @@ const preparedByMachineAtom = new WeakMap<
79
79
  Atom.Atom<AsyncResult.AsyncResult<Machine.Prepared<any, any, any, any, any, any, any>, any>>
80
80
  >()
81
81
 
82
+ const machineByMachineAtom = new WeakMap<object, Machine.Machine.Any>()
83
+
82
84
  type WeakFamilyEntry<Value extends object> = {
83
85
  readonly ref: WeakRef<Value>
84
86
  }
@@ -422,7 +424,8 @@ const makeChildSelector = <StartError>(
422
424
  }
423
425
 
424
426
  const makeFromRefAtom = <State, Event, Error, Output, StartError, Emitted>(
425
- ref: Atom.Atom<AsyncResult.AsyncResult<Machine.MachineRef<State, Event, Error, Output, Emitted>, StartError>>
427
+ ref: Atom.Atom<AsyncResult.AsyncResult<Machine.MachineRef<State, Event, Error, Output, Emitted>, StartError>>,
428
+ machine: Machine.Machine.Any
426
429
  ): MachineAtom<State, Event, Error, Output, StartError, Emitted> => {
427
430
  const snapshot = Atom.readable((
428
431
  get
@@ -498,7 +501,7 @@ const makeFromRefAtom = <State, Event, Error, Output, StartError, Emitted>(
498
501
  const optionalRef = Atom.mapResult(ref, Option.some)
499
502
  const child = makeChildSelector<StartError>(optionalRef as any)
500
503
 
501
- return {
504
+ const result = {
502
505
  ref,
503
506
  snapshot,
504
507
  state: Atom.mapResult(snapshot, (snapshot) => snapshot.state),
@@ -507,6 +510,8 @@ const makeFromRefAtom = <State, Event, Error, Output, StartError, Emitted>(
507
510
  stop,
508
511
  child
509
512
  }
513
+ machineByMachineAtom.set(result, machine)
514
+ return result
510
515
  }
511
516
 
512
517
  type SnapshotNode<State> = State extends Machine.Machine.AtomicSnapshot<string, unknown> ?
@@ -716,6 +721,57 @@ export const matchesChild = <
716
721
  Option.exists((snapshot) => Option.isSome(Topology.getSnapshotByPath(snapshot, path)))
717
722
  ).pipe(Atom.withEquality(Equal.equals)))
718
723
 
724
+ export const can = (event: unknown) => {
725
+ const byBridge = new WeakMap<object, Atom.Atom<AsyncResult.AsyncResult<boolean, any>>>()
726
+ return (self: MachineAtom<any, any, any, any, any, any>): Atom.Atom<AsyncResult.AsyncResult<boolean, any>> => {
727
+ const cached = byBridge.get(self)
728
+ if (cached !== undefined) return cached
729
+
730
+ const machine = machineByMachineAtom.get(self)
731
+ const query: (
732
+ state: Machine.Machine.Snapshot<any>,
733
+ event: unknown
734
+ ) => Effect.Effect<boolean, Machine.MachineSchemaDecodeError> = machine === undefined
735
+ ? () => Effect.die(new Error("AtomMachine.can requires a machine atom created by AtomMachine"))
736
+ : internalMachine.can(machine) as (
737
+ state: Machine.Machine.Snapshot<any>,
738
+ event: unknown
739
+ ) => Effect.Effect<boolean, Machine.MachineSchemaDecodeError>
740
+
741
+ const result = Atom.readable((get): AsyncResult.AsyncResult<boolean, any> => {
742
+ const current = get(self.snapshot)
743
+ const previous = get.self<AsyncResult.AsyncResult<boolean, any>>()
744
+ if (AsyncResult.isInitial(current)) {
745
+ return AsyncResult.initial(current.waiting)
746
+ } else if (AsyncResult.isFailure(current)) {
747
+ return AsyncResult.failureWithPrevious(current.cause, {
748
+ previous,
749
+ waiting: current.waiting
750
+ })
751
+ } else if (current.value.status === "error") {
752
+ return AsyncResult.failureWithPrevious(current.value.cause, {
753
+ previous,
754
+ waiting: current.waiting
755
+ })
756
+ } else if (current.value.status !== "active") {
757
+ return AsyncResult.success(false, { waiting: current.waiting })
758
+ }
759
+
760
+ const input = Atom.isAtom(event) ? get(event) : event
761
+ const exit = Effect.runSyncExit(query(current.value.state, input))
762
+ return exit._tag === "Success"
763
+ ? AsyncResult.success(exit.value, { waiting: current.waiting })
764
+ : AsyncResult.failureWithPrevious(exit.cause, {
765
+ previous,
766
+ waiting: current.waiting
767
+ })
768
+ }).pipe(Atom.withEquality(Equal.equals))
769
+
770
+ byBridge.set(self, result)
771
+ return result
772
+ }
773
+ }
774
+
719
775
  type MachineResumeRequirementsOf<M extends Machine.Machine.Any> = MachineResumeRequirements<
720
776
  Machine.Machine.Services<M>,
721
777
  Machine.Machine.Event<M>,
@@ -796,11 +852,14 @@ export const make: {
796
852
  } = ((machine: Machine.Machine.Any, ...args: ReadonlyArray<unknown>) => {
797
853
  const prepared = Atom.make(() => internalMachine.prepare(machine as any, ...(args as [])))
798
854
  const ref = Atom.make((get) => startPreparedMachineAtomEffect(get, prepared as any))
799
- const result = makeFromRefAtom(ref as any)
855
+ const result = makeFromRefAtom(ref as any, machine)
800
856
  preparedByMachineAtom.set(result, prepared as any)
801
857
  return result
802
858
  }) as any
803
859
 
860
+ export const factory =
861
+ ((machine: Machine.Machine.Any) => (...args: ReadonlyArray<unknown>) => (make as any)(machine, ...args)) as any
862
+
804
863
  export const resume: {
805
864
  <M extends Machine.Machine.Any>(
806
865
  machine:
@@ -812,7 +871,7 @@ export const resume: {
812
871
  ): ResumedMachineAtomOf<M, never>
813
872
  } = ((machine: Machine.Machine.Any, snapshot: Machine.Machine.Snapshot<any>) => {
814
873
  const ref = Atom.make((get) => resumeMachineAtomEffect(get, machine, snapshot))
815
- return makeFromRefAtom(ref as any)
874
+ return makeFromRefAtom(ref as any, machine)
816
875
  }) as any
817
876
 
818
877
  const makeWithRuntime = (
@@ -822,7 +881,7 @@ const makeWithRuntime = (
822
881
  ): MachineAtom<any, any, any, any, any, any> => {
823
882
  const prepared = runtime.atom(() => internalMachine.prepare(machine as any, ...(args as [])))
824
883
  const ref = runtime.atom((get) => startPreparedMachineAtomEffect(get, prepared as any))
825
- const result = makeFromRefAtom(ref as any)
884
+ const result = makeFromRefAtom(ref as any, machine)
826
885
  preparedByMachineAtom.set(result, prepared as any)
827
886
  return result
828
887
  }
@@ -833,7 +892,7 @@ const resumeWithRuntime = (
833
892
  snapshot: Machine.Machine.Snapshot<any>
834
893
  ): MachineAtom<any, any, any, any, any, any> => {
835
894
  const ref = runtime.atom((get) => resumeMachineAtomEffect(get, machine, snapshot))
836
- return makeFromRefAtom(ref as any)
895
+ return makeFromRefAtom(ref as any, machine)
837
896
  }
838
897
 
839
898
  type FamilyBridge = MachineAtom<any, never, any, any, any, any> | ChildMachineAtom<any, any>
@@ -901,19 +960,25 @@ export const familyChild = (
901
960
 
902
961
  export const bind = <Services, RuntimeError>(
903
962
  runtime: Atom.AtomRuntime<Services, RuntimeError>
904
- ): Bound<Services, RuntimeError> => ({
905
- make:
963
+ ): Bound<Services, RuntimeError> => {
964
+ const makeBound =
906
965
  ((machine: Machine.Machine.Any, ...args: ReadonlyArray<unknown>) =>
907
966
  makeWithRuntime(runtime, machine, args)) as Bound<
908
967
  Services,
909
968
  RuntimeError
910
- >["make"],
911
- resume:
912
- ((machine: Machine.Machine.Any, snapshot: Machine.Machine.Snapshot<any>) =>
913
- resumeWithRuntime(runtime, machine, snapshot)) as Bound<Services, RuntimeError>["resume"],
914
- family: ((machine: Machine.Machine.Any, options: FamilyOptions) =>
915
- makeFamily(
916
- (input) => makeWithRuntime(runtime, machine, [input]),
917
- options
918
- )) as Bound<Services, RuntimeError>["family"]
919
- })
969
+ >["make"]
970
+ return {
971
+ make: makeBound,
972
+ factory:
973
+ ((machine: Machine.Machine.Any) => (...args: ReadonlyArray<unknown>) =>
974
+ (makeBound as any)(machine, ...args)) as Bound<Services, RuntimeError>["factory"],
975
+ resume:
976
+ ((machine: Machine.Machine.Any, snapshot: Machine.Machine.Snapshot<any>) =>
977
+ resumeWithRuntime(runtime, machine, snapshot)) as Bound<Services, RuntimeError>["resume"],
978
+ family: ((machine: Machine.Machine.Any, options: FamilyOptions) =>
979
+ makeFamily(
980
+ (input) => makeWithRuntime(runtime, machine, [input]),
981
+ options
982
+ )) as Bound<Services, RuntimeError>["family"]
983
+ }
984
+ }
@@ -2176,6 +2176,8 @@ export const enabled = <
2176
2176
  state: Machine.Snapshot<States>
2177
2177
  ): ReadonlyArray<Machine.TagOf<Events[number]>> => internalPlanner.enabled(machine as any, state)
2178
2178
 
2179
+ export const can = internalPlanner.can
2180
+
2179
2181
  export const plan: <
2180
2182
  const States extends Machine.StateSchemas,
2181
2183
  const Events extends ReadonlyArray<Machine.TaggedSchema>,
@@ -1876,6 +1876,17 @@ export const enabled = <
1876
1876
  return tags
1877
1877
  }
1878
1878
 
1879
+ const canSync = (
1880
+ machine: Machine.Any,
1881
+ state: Machine.Snapshot<any>,
1882
+ event: unknown
1883
+ ): boolean => {
1884
+ const decodedEvent = decodeEventSync(machine, event)
1885
+ if (isFinalState(machine, state)) return false
1886
+ const configuration = normalizeConfigurationSync(machine, state)
1887
+ return selectEventTransitions(machine, configuration, decodedEvent as any).length > 0
1888
+ }
1889
+
1879
1890
  const microstep = <
1880
1891
  const States extends Machine.StateSchemas,
1881
1892
  const Events extends ReadonlyArray<Machine.TaggedSchema>,
@@ -2300,6 +2311,20 @@ const planningEffect = <A>(thunk: () => A): Effect.Effect<A, InfiniteTransitionE
2300
2311
  }
2301
2312
  })
2302
2313
 
2314
+ const schemaEffect = <A>(thunk: () => A): Effect.Effect<A, MachineSchemaDecodeError> =>
2315
+ Effect.suspend(() => {
2316
+ try {
2317
+ return Effect.succeed(thunk())
2318
+ } catch (error) {
2319
+ return error instanceof MachineSchemaDecodeError ? Effect.fail(error) : Effect.die(error)
2320
+ }
2321
+ })
2322
+
2323
+ export const can = (...args: readonly [Machine.Any] | readonly [Machine.Any, Machine.Snapshot<any>, unknown]) => {
2324
+ const query = (state: Machine.Snapshot<any>, event: unknown) => schemaEffect(() => canSync(args[0], state, event))
2325
+ return args.length === 1 ? query : query(args[1], args[2])
2326
+ }
2327
+
2303
2328
  export const plan = (machine: Machine.Any, state: Machine.Snapshot<any>, event: unknown) =>
2304
2329
  planningEffect(() => planSync(machine as any, state, event as any))
2305
2330
 
@@ -358,6 +358,7 @@ type ChildSnapshot<Child extends Machine.ChildMachine.Any> = Machine.Machine.Sna
358
358
 
359
359
  const InvalidSelectorPathTypeId = "~effect/reactivity/AtomMachine/InvalidSelectorPath"
360
360
  const SelectorProjectionTypeId = "~effect/reactivity/AtomMachine/SelectorProjection"
361
+ const InvalidCanEventTypeId = "~effect/reactivity/AtomMachine/InvalidCanEvent"
361
362
 
362
363
  type SelectorProjectionKind =
363
364
  | "select"
@@ -374,6 +375,30 @@ interface SelectorProjection<Kind extends SelectorProjectionKind, Path extends s
374
375
  }
375
376
  }
376
377
 
378
+ type EnsureCanEvent<AcceptedEvent, Input> = [Input] extends [AcceptedEvent] ? unknown : {
379
+ readonly [InvalidCanEventTypeId]: {
380
+ readonly input: Input
381
+ readonly accepted: AcceptedEvent
382
+ }
383
+ }
384
+
385
+ interface CanProjection<Input> {
386
+ <
387
+ State extends Machine.Machine.AtomicSnapshot<string, unknown>,
388
+ AcceptedEvent,
389
+ Error,
390
+ Output,
391
+ StartError,
392
+ Emitted
393
+ >(
394
+ self:
395
+ & MachineAtom<State, AcceptedEvent, Error, Output, StartError, Emitted>
396
+ & EnsureCanEvent<AcceptedEvent, Input>
397
+ ): Atom.Atom<
398
+ AsyncResult.AsyncResult<boolean, StartError | Error | Machine.MachineSchemaDecodeError>
399
+ >
400
+ }
401
+
377
402
  type EnsureSelectorPath<State, Path extends string> = [Path] extends [SnapshotIdentifier<State>] ? unknown : {
378
403
  readonly [InvalidSelectorPathTypeId]: Path
379
404
  }
@@ -695,6 +720,37 @@ export const matches: {
695
720
  ): Atom.Atom<AsyncResult.AsyncResult<boolean, StartError | Error>>
696
721
  } = dual(2, internal.matches)
697
722
 
723
+ /**
724
+ * Reactively tests whether a concrete event would be accepted by a running
725
+ * machine.
726
+ *
727
+ * Declare the returned projection once, then apply it to compatible machine
728
+ * bridges. Repeated applications to the same bridge return the same atom. An
729
+ * event atom is read reactively when acceptance depends on a changing payload.
730
+ *
731
+ * Startup remains in the source `AsyncResult`. Active snapshots use
732
+ * {@link Machine.can}; done and stopped snapshots produce `false`, while
733
+ * runtime and schema failures remain in the typed failure channel.
734
+ *
735
+ * **Example**
736
+ *
737
+ * ```ts
738
+ * const submitAllowed = AtomMachine.can(AuthEvents.Submitted())
739
+ * const canSubmitAtom = submitAllowed(authMachineAtom)
740
+ *
741
+ * const submitEvent = Atom.map(draftAtom, (draft) =>
742
+ * AuthEvents.Submitted({ draft }))
743
+ * const reactiveSubmitAllowed = AtomMachine.can(submitEvent)
744
+ * ```
745
+ *
746
+ * @category combinators
747
+ * @since 0.31.0
748
+ */
749
+ export const can: {
750
+ <Input>(event: Atom.Atom<Input>): CanProjection<Input>
751
+ <const Input>(event: Input): CanProjection<Input>
752
+ } = internal.can
753
+
698
754
  /**
699
755
  * Returns whether a state path is active in a directly owned child.
700
756
  *
@@ -906,7 +962,7 @@ type ResumedMachineAtomOf<M extends Machine.Machine.Any, RuntimeError> = Machine
906
962
  >
907
963
 
908
964
  /**
909
- * An `AtomMachine` factory with one owned Effect runtime.
965
+ * `AtomMachine` constructors bound to one owned Effect runtime.
910
966
  *
911
967
  * @category models
912
968
  * @since 0.4.0
@@ -929,6 +985,22 @@ export interface Bound<Services, RuntimeError = never> {
929
985
  ...args: MachineInputArgsOf<M>
930
986
  ) => MachineAtomOf<M, RuntimeError>
931
987
 
988
+ /**
989
+ * Specializes a machine definition into a reusable bridge constructor.
990
+ *
991
+ * Every call creates an independent machine bridge. Startup remains lazy
992
+ * and begins only when an `AtomRegistry` reads or mounts the bridge.
993
+ *
994
+ * @since 0.30.0
995
+ */
996
+ readonly factory: <M extends Machine.Machine.Any>(
997
+ machine:
998
+ & M
999
+ & EnsureBoundRequirements<Services, NoInfer<M>>
1000
+ & EnsureMachineExecutable<NoInfer<M>>
1001
+ & Machine.Machine.RootCompatible<Machine.Machine.ParentEvents<NoInfer<M>>>
1002
+ ) => (...args: MachineInputArgsOf<M>) => MachineAtomOf<M, RuntimeError>
1003
+
932
1004
  /** Creates a lazy bridge from a decoded logical snapshot. */
933
1005
  readonly resume: <M extends Machine.Machine.Any>(
934
1006
  machine:
@@ -1140,6 +1212,33 @@ export const make: {
1140
1212
  >
1141
1213
  } = internal.make
1142
1214
 
1215
+ /**
1216
+ * Specializes a machine definition into a reusable bridge constructor.
1217
+ *
1218
+ * The returned function preserves the machine's startup input arity and exact
1219
+ * bridge type. Every call creates a fresh `MachineAtom`; it does not cache by
1220
+ * input or start the machine before an `AtomRegistry` reads or mounts it.
1221
+ *
1222
+ * **Example**
1223
+ *
1224
+ * ```ts
1225
+ * const makeSearchMachine = AtomMachine.factory(searchMachine)
1226
+ * const search = makeSearchMachine({ query: "effect" })
1227
+ *
1228
+ * type SearchMachineAtom = ReturnType<typeof makeSearchMachine>
1229
+ * ```
1230
+ *
1231
+ * @category constructors
1232
+ * @since 0.30.0
1233
+ */
1234
+ export const factory: <M extends Machine.Machine.Any>(
1235
+ machine:
1236
+ & M
1237
+ & EnsureNoExternalRequirements<MachineRequirementsOf<NoInfer<M>>>
1238
+ & EnsureMachineExecutable<NoInfer<M>>
1239
+ & Machine.Machine.RootCompatible<Machine.Machine.ParentEvents<NoInfer<M>>>
1240
+ ) => (...args: MachineInputArgsOf<M>) => MachineAtomOf<M, never> = internal.factory
1241
+
1143
1242
  /**
1144
1243
  * Creates a lazy atom bridge from a decoded logical snapshot.
1145
1244
  *
@@ -1162,11 +1261,12 @@ export const resume: {
1162
1261
  } = internal.resume
1163
1262
 
1164
1263
  /**
1165
- * Creates an `AtomMachine` factory that owns a shared Effect runtime.
1264
+ * Binds `AtomMachine` constructors to a shared Effect runtime.
1166
1265
  *
1167
1266
  * Use this when an application runs many machines from the same service layer.
1168
- * The returned factory keeps runtime provisioning at the composition boundary,
1169
- * while every call to `make` still creates an independent machine bridge.
1267
+ * The returned interface keeps runtime provisioning at the composition seam,
1268
+ * while every call to `make` or a specialized `factory` still creates an
1269
+ * independent machine bridge.
1170
1270
  *
1171
1271
  * @category constructors
1172
1272
  * @since 0.4.0