@typeonce/effect-machine 0.28.0 → 0.30.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeonce/effect-machine",
3
- "version": "0.28.0",
3
+ "version": "0.30.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
  *
@@ -557,6 +557,41 @@ const selectSnapshotByPath = <
557
557
  ): Option.Option<SnapshotByIdentifier<State, Path>> =>
558
558
  Topology.getSnapshotByPath(snapshot, path) as Option.Option<SnapshotByIdentifier<State, Path>>
559
559
 
560
+ type SelectorKind =
561
+ | "matches"
562
+ | "matchesChild"
563
+ | "select"
564
+ | "selectChild"
565
+ | "selectSnapshot"
566
+ | "selectSnapshotChild"
567
+
568
+ const selectorsByBridge = new WeakMap<object, Map<SelectorKind, Map<string, Atom.Atom<any>>>>()
569
+
570
+ const cachedSelector = <A>(
571
+ bridge: object,
572
+ kind: SelectorKind,
573
+ path: string,
574
+ make: () => Atom.Atom<A>
575
+ ): Atom.Atom<A> => {
576
+ let selectorsByKind = selectorsByBridge.get(bridge)
577
+ if (selectorsByKind === undefined) {
578
+ selectorsByKind = new Map()
579
+ selectorsByBridge.set(bridge, selectorsByKind)
580
+ }
581
+ let selectorsByPath = selectorsByKind.get(kind)
582
+ if (selectorsByPath === undefined) {
583
+ selectorsByPath = new Map()
584
+ selectorsByKind.set(kind, selectorsByPath)
585
+ }
586
+ const cached = selectorsByPath.get(path)
587
+ if (cached !== undefined) {
588
+ return cached as Atom.Atom<A>
589
+ }
590
+ const selector = make()
591
+ selectorsByPath.set(path, selector)
592
+ return selector
593
+ }
594
+
560
595
  export const select = <
561
596
  State extends Machine.Machine.AtomicSnapshot<string, unknown>,
562
597
  Event,
@@ -571,8 +606,14 @@ export const select = <
571
606
  ): Atom.Atom<
572
607
  AsyncResult.AsyncResult<Option.Option<SnapshotValueByIdentifier<State, Path>>, StartError | Error>
573
608
  > =>
574
- Atom.mapResult(self.result, (snapshot) => selectValueByPath(snapshot, path)).pipe(
575
- Atom.withEquality(Equal.equals)
609
+ cachedSelector(
610
+ self,
611
+ "select",
612
+ path,
613
+ () =>
614
+ Atom.mapResult(self.result, (snapshot) => selectValueByPath(snapshot, path)).pipe(
615
+ Atom.withEquality(Equal.equals)
616
+ )
576
617
  )
577
618
 
578
619
  export const selectSnapshot = <
@@ -589,8 +630,14 @@ export const selectSnapshot = <
589
630
  ): Atom.Atom<
590
631
  AsyncResult.AsyncResult<Option.Option<SnapshotByIdentifier<State, Path>>, StartError | Error>
591
632
  > =>
592
- Atom.mapResult(self.result, (snapshot) => selectSnapshotByPath(snapshot, path)).pipe(
593
- Atom.withEquality(Equal.equals)
633
+ cachedSelector(
634
+ self,
635
+ "selectSnapshot",
636
+ path,
637
+ () =>
638
+ Atom.mapResult(self.result, (snapshot) => selectSnapshotByPath(snapshot, path)).pipe(
639
+ Atom.withEquality(Equal.equals)
640
+ )
594
641
  )
595
642
 
596
643
  export const selectChild = <
@@ -606,10 +653,11 @@ export const selectChild = <
606
653
  StartError | RefError<Machine.ChildMachine.Ref<Child>>
607
654
  >
608
655
  > =>
609
- Atom.mapResult(
610
- self.result,
611
- Option.flatMap((snapshot) => selectValueByPath(snapshot, path))
612
- ).pipe(Atom.withEquality(Equal.equals))
656
+ cachedSelector(self, "selectChild", path, () =>
657
+ Atom.mapResult(
658
+ self.result,
659
+ Option.flatMap((snapshot) => selectValueByPath(snapshot, path))
660
+ ).pipe(Atom.withEquality(Equal.equals)))
613
661
 
614
662
  export const selectSnapshotChild = <
615
663
  Child extends Machine.ChildMachine.Any,
@@ -624,10 +672,11 @@ export const selectSnapshotChild = <
624
672
  StartError | RefError<Machine.ChildMachine.Ref<Child>>
625
673
  >
626
674
  > =>
627
- Atom.mapResult(
628
- self.result,
629
- Option.flatMap((snapshot) => selectSnapshotByPath(snapshot, path))
630
- ).pipe(Atom.withEquality(Equal.equals))
675
+ cachedSelector(self, "selectSnapshotChild", path, () =>
676
+ Atom.mapResult(
677
+ self.result,
678
+ Option.flatMap((snapshot) => selectSnapshotByPath(snapshot, path))
679
+ ).pipe(Atom.withEquality(Equal.equals)))
631
680
 
632
681
  export const matches = <
633
682
  State extends Machine.Machine.AtomicSnapshot<string, unknown>,
@@ -641,8 +690,14 @@ export const matches = <
641
690
  self: MachineAtom<State, Event, Error, Output, StartError, Emitted>,
642
691
  path: Path
643
692
  ): Atom.Atom<AsyncResult.AsyncResult<boolean, StartError | Error>> =>
644
- Atom.mapResult(self.result, (snapshot) => Option.isSome(Topology.getSnapshotByPath(snapshot, path))).pipe(
645
- Atom.withEquality(Equal.equals)
693
+ cachedSelector(
694
+ self,
695
+ "matches",
696
+ path,
697
+ () =>
698
+ Atom.mapResult(self.result, (snapshot) => Option.isSome(Topology.getSnapshotByPath(snapshot, path))).pipe(
699
+ Atom.withEquality(Equal.equals)
700
+ )
646
701
  )
647
702
 
648
703
  export const matchesChild = <
@@ -655,10 +710,11 @@ export const matchesChild = <
655
710
  ): Atom.Atom<
656
711
  AsyncResult.AsyncResult<boolean, StartError | RefError<Machine.ChildMachine.Ref<Child>>>
657
712
  > =>
658
- Atom.mapResult(
659
- self.result,
660
- Option.exists((snapshot) => Option.isSome(Topology.getSnapshotByPath(snapshot, path)))
661
- ).pipe(Atom.withEquality(Equal.equals))
713
+ cachedSelector(self, "matchesChild", path, () =>
714
+ Atom.mapResult(
715
+ self.result,
716
+ Option.exists((snapshot) => Option.isSome(Topology.getSnapshotByPath(snapshot, path)))
717
+ ).pipe(Atom.withEquality(Equal.equals)))
662
718
 
663
719
  type MachineResumeRequirementsOf<M extends Machine.Machine.Any> = MachineResumeRequirements<
664
720
  Machine.Machine.Services<M>,
@@ -745,6 +801,9 @@ export const make: {
745
801
  return result
746
802
  }) as any
747
803
 
804
+ export const factory =
805
+ ((machine: Machine.Machine.Any) => (...args: ReadonlyArray<unknown>) => (make as any)(machine, ...args)) as any
806
+
748
807
  export const resume: {
749
808
  <M extends Machine.Machine.Any>(
750
809
  machine:
@@ -845,19 +904,25 @@ export const familyChild = (
845
904
 
846
905
  export const bind = <Services, RuntimeError>(
847
906
  runtime: Atom.AtomRuntime<Services, RuntimeError>
848
- ): Bound<Services, RuntimeError> => ({
849
- make:
907
+ ): Bound<Services, RuntimeError> => {
908
+ const makeBound =
850
909
  ((machine: Machine.Machine.Any, ...args: ReadonlyArray<unknown>) =>
851
910
  makeWithRuntime(runtime, machine, args)) as Bound<
852
911
  Services,
853
912
  RuntimeError
854
- >["make"],
855
- resume:
856
- ((machine: Machine.Machine.Any, snapshot: Machine.Machine.Snapshot<any>) =>
857
- resumeWithRuntime(runtime, machine, snapshot)) as Bound<Services, RuntimeError>["resume"],
858
- family: ((machine: Machine.Machine.Any, options: FamilyOptions) =>
859
- makeFamily(
860
- (input) => makeWithRuntime(runtime, machine, [input]),
861
- options
862
- )) as Bound<Services, RuntimeError>["family"]
863
- })
913
+ >["make"]
914
+ return {
915
+ make: makeBound,
916
+ factory:
917
+ ((machine: Machine.Machine.Any) => (...args: ReadonlyArray<unknown>) =>
918
+ (makeBound as any)(machine, ...args)) as Bound<Services, RuntimeError>["factory"],
919
+ resume:
920
+ ((machine: Machine.Machine.Any, snapshot: Machine.Machine.Snapshot<any>) =>
921
+ resumeWithRuntime(runtime, machine, snapshot)) as Bound<Services, RuntimeError>["resume"],
922
+ family: ((machine: Machine.Machine.Any, options: FamilyOptions) =>
923
+ makeFamily(
924
+ (input) => makeWithRuntime(runtime, machine, [input]),
925
+ options
926
+ )) as Bound<Services, RuntimeError>["family"]
927
+ }
928
+ }
@@ -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
 
@@ -387,8 +387,8 @@ type EnsureValuedSelectorPath<State, Path extends string> = [Path] extends [Valu
387
387
  * Selects the typed value for an active state path.
388
388
  *
389
389
  * Valid paths and their selected value types are inferred from the bridge.
390
- * The derived atom suppresses structurally equal updates. Keep the returned
391
- * atom stable when constructing it inside a component.
390
+ * The derived atom suppresses structurally equal updates. Repeated calls with
391
+ * the same bridge and path return the same atom.
392
392
  *
393
393
  * **Example**
394
394
  *
@@ -465,8 +465,8 @@ export const select: {
465
465
  * Selects the typed logical snapshot for an active state path.
466
466
  *
467
467
  * Unlike {@link select}, the selected value retains its child snapshot
468
- * topology. The derived atom suppresses structurally equal updates. Keep the
469
- * returned atom stable when constructing it inside a component.
468
+ * topology. The derived atom suppresses structurally equal updates. Repeated
469
+ * calls with the same bridge and path return the same atom.
470
470
  *
471
471
  * @category combinators
472
472
  * @since 0.7.0
@@ -519,8 +519,8 @@ export const selectSnapshot: {
519
519
  * Selects the typed value for an active state path in a directly owned child.
520
520
  *
521
521
  * Valid paths and their selected value types are inferred from the child
522
- * bridge. An inactive child produces `Option.none()`. Keep the returned atom
523
- * stable when constructing it inside a component.
522
+ * bridge. An inactive child produces `Option.none()`. Repeated calls with the
523
+ * same child bridge and path return the same atom.
524
524
  *
525
525
  * **Example**
526
526
  *
@@ -578,7 +578,8 @@ export const selectChild: {
578
578
  *
579
579
  * An inactive child or state path produces `Option.none()`. Unlike
580
580
  * {@link selectChild}, the selected value retains its child snapshot topology.
581
- * The derived atom suppresses structurally equal updates.
581
+ * The derived atom suppresses structurally equal updates. Repeated calls with
582
+ * the same child bridge and path return the same atom.
582
583
  *
583
584
  * @category combinators
584
585
  * @since 0.7.0
@@ -625,8 +626,9 @@ export const selectSnapshotChild: {
625
626
  * Returns whether a state path is active.
626
627
  *
627
628
  * Valid paths are inferred from the bridge snapshot.
628
- * The derived atom suppresses equal updates. Runtime failures remain in the
629
- * typed failure channel.
629
+ * The derived atom suppresses equal updates. Repeated calls with the same
630
+ * bridge and path return the same atom. Runtime failures remain in the typed
631
+ * failure channel.
630
632
  *
631
633
  * **Example**
632
634
  *
@@ -697,8 +699,8 @@ export const matches: {
697
699
  * Returns whether a state path is active in a directly owned child.
698
700
  *
699
701
  * Valid paths are inferred from the child bridge snapshot.
700
- * An inactive child produces `false`. Keep the returned atom stable when
701
- * constructing it inside a component.
702
+ * An inactive child produces `false`. Repeated calls with the same child
703
+ * bridge and path return the same atom.
702
704
  *
703
705
  * @category combinators
704
706
  * @since 0.4.0
@@ -904,7 +906,7 @@ type ResumedMachineAtomOf<M extends Machine.Machine.Any, RuntimeError> = Machine
904
906
  >
905
907
 
906
908
  /**
907
- * An `AtomMachine` factory with one owned Effect runtime.
909
+ * `AtomMachine` constructors bound to one owned Effect runtime.
908
910
  *
909
911
  * @category models
910
912
  * @since 0.4.0
@@ -927,6 +929,22 @@ export interface Bound<Services, RuntimeError = never> {
927
929
  ...args: MachineInputArgsOf<M>
928
930
  ) => MachineAtomOf<M, RuntimeError>
929
931
 
932
+ /**
933
+ * Specializes a machine definition into a reusable bridge constructor.
934
+ *
935
+ * Every call creates an independent machine bridge. Startup remains lazy
936
+ * and begins only when an `AtomRegistry` reads or mounts the bridge.
937
+ *
938
+ * @since 0.30.0
939
+ */
940
+ readonly factory: <M extends Machine.Machine.Any>(
941
+ machine:
942
+ & M
943
+ & EnsureBoundRequirements<Services, NoInfer<M>>
944
+ & EnsureMachineExecutable<NoInfer<M>>
945
+ & Machine.Machine.RootCompatible<Machine.Machine.ParentEvents<NoInfer<M>>>
946
+ ) => (...args: MachineInputArgsOf<M>) => MachineAtomOf<M, RuntimeError>
947
+
930
948
  /** Creates a lazy bridge from a decoded logical snapshot. */
931
949
  readonly resume: <M extends Machine.Machine.Any>(
932
950
  machine:
@@ -1138,6 +1156,33 @@ export const make: {
1138
1156
  >
1139
1157
  } = internal.make
1140
1158
 
1159
+ /**
1160
+ * Specializes a machine definition into a reusable bridge constructor.
1161
+ *
1162
+ * The returned function preserves the machine's startup input arity and exact
1163
+ * bridge type. Every call creates a fresh `MachineAtom`; it does not cache by
1164
+ * input or start the machine before an `AtomRegistry` reads or mounts it.
1165
+ *
1166
+ * **Example**
1167
+ *
1168
+ * ```ts
1169
+ * const makeSearchMachine = AtomMachine.factory(searchMachine)
1170
+ * const search = makeSearchMachine({ query: "effect" })
1171
+ *
1172
+ * type SearchMachineAtom = ReturnType<typeof makeSearchMachine>
1173
+ * ```
1174
+ *
1175
+ * @category constructors
1176
+ * @since 0.30.0
1177
+ */
1178
+ export const factory: <M extends Machine.Machine.Any>(
1179
+ machine:
1180
+ & M
1181
+ & EnsureNoExternalRequirements<MachineRequirementsOf<NoInfer<M>>>
1182
+ & EnsureMachineExecutable<NoInfer<M>>
1183
+ & Machine.Machine.RootCompatible<Machine.Machine.ParentEvents<NoInfer<M>>>
1184
+ ) => (...args: MachineInputArgsOf<M>) => MachineAtomOf<M, never> = internal.factory
1185
+
1141
1186
  /**
1142
1187
  * Creates a lazy atom bridge from a decoded logical snapshot.
1143
1188
  *
@@ -1160,11 +1205,12 @@ export const resume: {
1160
1205
  } = internal.resume
1161
1206
 
1162
1207
  /**
1163
- * Creates an `AtomMachine` factory that owns a shared Effect runtime.
1208
+ * Binds `AtomMachine` constructors to a shared Effect runtime.
1164
1209
  *
1165
1210
  * Use this when an application runs many machines from the same service layer.
1166
- * The returned factory keeps runtime provisioning at the composition boundary,
1167
- * while every call to `make` still creates an independent machine bridge.
1211
+ * The returned interface keeps runtime provisioning at the composition seam,
1212
+ * while every call to `make` or a specialized `factory` still creates an
1213
+ * independent machine bridge.
1168
1214
  *
1169
1215
  * @category constructors
1170
1216
  * @since 0.4.0