@typeonce/effect-machine 0.27.1 → 0.29.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.
@@ -8,6 +8,7 @@ import * as Data from "effect/Data"
8
8
  import * as Effect from "effect/Effect"
9
9
  import * as Equal from "effect/Equal"
10
10
  import * as Fiber from "effect/Fiber"
11
+ import * as MutableHashMap from "effect/MutableHashMap"
11
12
  import * as Option from "effect/Option"
12
13
  import type * as Schema from "effect/Schema"
13
14
  import type * as Scope from "effect/Scope"
@@ -78,6 +79,47 @@ const preparedByMachineAtom = new WeakMap<
78
79
  Atom.Atom<AsyncResult.AsyncResult<Machine.Prepared<any, any, any, any, any, any, any>, any>>
79
80
  >()
80
81
 
82
+ type WeakFamilyEntry<Value extends object> = {
83
+ readonly ref: WeakRef<Value>
84
+ }
85
+
86
+ // This follows Atom.family, but cleanup is generation-aware. A finalizer for a
87
+ // collected value must not remove a newer value installed for the same key.
88
+ const retainedFamily = <Key, Value extends object>(
89
+ makeValue: (key: Key) => Value
90
+ ): (key: Key) => Value => {
91
+ if (typeof WeakRef === "undefined" || typeof FinalizationRegistry === "undefined") {
92
+ return Atom.family(makeValue)
93
+ }
94
+
95
+ const values = MutableHashMap.empty<Key, WeakFamilyEntry<Value>>()
96
+ const registry = new FinalizationRegistry<{
97
+ readonly key: Key
98
+ readonly entry: WeakFamilyEntry<Value>
99
+ }>(({ entry, key }) => {
100
+ const current = MutableHashMap.get(values, key)
101
+ if (Option.isSome(current) && current.value === entry) {
102
+ MutableHashMap.remove(values, key)
103
+ }
104
+ })
105
+
106
+ return (key) => {
107
+ const current = MutableHashMap.get(values, key)
108
+ if (Option.isSome(current)) {
109
+ const value = current.value.ref.deref()
110
+ if (value !== undefined) {
111
+ return value
112
+ }
113
+ }
114
+
115
+ const value = makeValue(key)
116
+ const entry = { ref: new WeakRef(value) }
117
+ MutableHashMap.set(values, key, entry)
118
+ registry.register(value, { key, entry })
119
+ return value
120
+ }
121
+ }
122
+
81
123
  const runMachineAtomEffect = <State, Event, Error, Output, Emitted, StartError, Requirements>(
82
124
  get: Atom.AtomContext,
83
125
  start: Effect.Effect<Machine.MachineRef<State, Event, Error, Output, Emitted>, StartError, Requirements>
@@ -515,6 +557,41 @@ const selectSnapshotByPath = <
515
557
  ): Option.Option<SnapshotByIdentifier<State, Path>> =>
516
558
  Topology.getSnapshotByPath(snapshot, path) as Option.Option<SnapshotByIdentifier<State, Path>>
517
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
+
518
595
  export const select = <
519
596
  State extends Machine.Machine.AtomicSnapshot<string, unknown>,
520
597
  Event,
@@ -529,8 +606,14 @@ export const select = <
529
606
  ): Atom.Atom<
530
607
  AsyncResult.AsyncResult<Option.Option<SnapshotValueByIdentifier<State, Path>>, StartError | Error>
531
608
  > =>
532
- Atom.mapResult(self.result, (snapshot) => selectValueByPath(snapshot, path)).pipe(
533
- 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
+ )
534
617
  )
535
618
 
536
619
  export const selectSnapshot = <
@@ -547,8 +630,14 @@ export const selectSnapshot = <
547
630
  ): Atom.Atom<
548
631
  AsyncResult.AsyncResult<Option.Option<SnapshotByIdentifier<State, Path>>, StartError | Error>
549
632
  > =>
550
- Atom.mapResult(self.result, (snapshot) => selectSnapshotByPath(snapshot, path)).pipe(
551
- 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
+ )
552
641
  )
553
642
 
554
643
  export const selectChild = <
@@ -564,10 +653,11 @@ export const selectChild = <
564
653
  StartError | RefError<Machine.ChildMachine.Ref<Child>>
565
654
  >
566
655
  > =>
567
- Atom.mapResult(
568
- self.result,
569
- Option.flatMap((snapshot) => selectValueByPath(snapshot, path))
570
- ).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)))
571
661
 
572
662
  export const selectSnapshotChild = <
573
663
  Child extends Machine.ChildMachine.Any,
@@ -582,10 +672,11 @@ export const selectSnapshotChild = <
582
672
  StartError | RefError<Machine.ChildMachine.Ref<Child>>
583
673
  >
584
674
  > =>
585
- Atom.mapResult(
586
- self.result,
587
- Option.flatMap((snapshot) => selectSnapshotByPath(snapshot, path))
588
- ).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)))
589
680
 
590
681
  export const matches = <
591
682
  State extends Machine.Machine.AtomicSnapshot<string, unknown>,
@@ -599,8 +690,14 @@ export const matches = <
599
690
  self: MachineAtom<State, Event, Error, Output, StartError, Emitted>,
600
691
  path: Path
601
692
  ): Atom.Atom<AsyncResult.AsyncResult<boolean, StartError | Error>> =>
602
- Atom.mapResult(self.result, (snapshot) => Option.isSome(Topology.getSnapshotByPath(snapshot, path))).pipe(
603
- 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
+ )
604
701
  )
605
702
 
606
703
  export const matchesChild = <
@@ -613,10 +710,11 @@ export const matchesChild = <
613
710
  ): Atom.Atom<
614
711
  AsyncResult.AsyncResult<boolean, StartError | RefError<Machine.ChildMachine.Ref<Child>>>
615
712
  > =>
616
- Atom.mapResult(
617
- self.result,
618
- Option.exists((snapshot) => Option.isSome(Topology.getSnapshotByPath(snapshot, path)))
619
- ).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)))
620
718
 
621
719
  type MachineResumeRequirementsOf<M extends Machine.Machine.Any> = MachineResumeRequirements<
622
720
  Machine.Machine.Services<M>,
@@ -738,6 +836,69 @@ const resumeWithRuntime = (
738
836
  return makeFromRefAtom(ref as any)
739
837
  }
740
838
 
839
+ type FamilyBridge = MachineAtom<any, never, any, any, any, any> | ChildMachineAtom<any, any>
840
+
841
+ type FamilyOptions = {
842
+ readonly atoms: Readonly<Record<string, (bridge: FamilyBridge) => Atom.Atom<any>>>
843
+ readonly label?: (key: any, atomName: string) => string | undefined
844
+ }
845
+
846
+ const retainFamilyOwner = <Source extends Atom.Atom<any>>(
847
+ owner: { readonly bridge: FamilyBridge },
848
+ source: Source,
849
+ label: string | undefined
850
+ ): Atom.WithoutSerializable<Source> => {
851
+ const retained = { owner, source }
852
+ let atom: Atom.Atom<any> = Atom.transform(
853
+ source,
854
+ (get) => get(retained.source),
855
+ { initialValueTarget: source }
856
+ ).pipe(
857
+ Atom.withEquality((value, next) => retained.source.equals(value, next))
858
+ )
859
+ if (label !== undefined) {
860
+ atom = atom.pipe(Atom.withLabel(label))
861
+ }
862
+ return atom as unknown as Atom.WithoutSerializable<Source>
863
+ }
864
+
865
+ const makeFamily = (
866
+ makeBridge: (key: any) => FamilyBridge,
867
+ options: FamilyOptions
868
+ ): Readonly<Record<string, (key: any) => Atom.Atom<any>>> => {
869
+ const owners = retainedFamily((key: any) => ({ bridge: makeBridge(key) }))
870
+ const atoms: Record<string, (key: any) => Atom.Atom<any>> = {}
871
+
872
+ for (const atomName of Object.keys(options.atoms)) {
873
+ const project = options.atoms[atomName]!
874
+ atoms[atomName] = retainedFamily((key: any) => {
875
+ const owner = owners(key)
876
+ const source = project(owner.bridge)
877
+ return retainFamilyOwner(owner, source, options.label?.(key, atomName))
878
+ })
879
+ }
880
+
881
+ return atoms
882
+ }
883
+
884
+ export const family = (
885
+ machine: Machine.Machine.Any,
886
+ options: FamilyOptions
887
+ ): Readonly<Record<string, (key: any) => Atom.Atom<any>>> =>
888
+ makeFamily(
889
+ (input) => (make as any)(machine, input),
890
+ options
891
+ )
892
+
893
+ export const familyChild = (
894
+ parent: MachineAtom<any, never, any, any, any, any> | ChildMachineAtom<any, any>,
895
+ options: FamilyOptions & { readonly child: (key: any) => Machine.ChildMachine.Any }
896
+ ): Readonly<Record<string, (key: any) => Atom.Atom<any>>> =>
897
+ makeFamily(
898
+ (key) => parent.child(options.child(key)),
899
+ options
900
+ )
901
+
741
902
  export const bind = <Services, RuntimeError>(
742
903
  runtime: Atom.AtomRuntime<Services, RuntimeError>
743
904
  ): Bound<Services, RuntimeError> => ({
@@ -749,5 +910,10 @@ export const bind = <Services, RuntimeError>(
749
910
  >["make"],
750
911
  resume:
751
912
  ((machine: Machine.Machine.Any, snapshot: Machine.Machine.Snapshot<any>) =>
752
- resumeWithRuntime(runtime, machine, snapshot)) as Bound<Services, RuntimeError>["resume"]
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"]
753
919
  })