@typed/fx 1.1.1 → 1.2.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.
Files changed (166) hide show
  1. package/dist/Subject/Computed.d.ts +12 -0
  2. package/dist/Subject/Computed.d.ts.map +1 -0
  3. package/dist/Subject/Computed.js +54 -0
  4. package/dist/Subject/Computed.js.map +1 -0
  5. package/dist/Subject/RefSubject.d.ts +11 -4
  6. package/dist/Subject/RefSubject.d.ts.map +1 -1
  7. package/dist/Subject/RefSubject.js +68 -24
  8. package/dist/Subject/RefSubject.js.map +1 -1
  9. package/dist/Subject/SynchronizedSubject.d.ts.map +1 -1
  10. package/dist/Subject/SynchronizedSubject.js +5 -6
  11. package/dist/Subject/SynchronizedSubject.js.map +1 -1
  12. package/dist/cjs/Subject/Computed.d.ts +12 -0
  13. package/dist/cjs/Subject/Computed.d.ts.map +1 -0
  14. package/dist/cjs/Subject/Computed.js +80 -0
  15. package/dist/cjs/Subject/Computed.js.map +1 -0
  16. package/dist/cjs/Subject/RefSubject.d.ts +11 -4
  17. package/dist/cjs/Subject/RefSubject.d.ts.map +1 -1
  18. package/dist/cjs/Subject/RefSubject.js +68 -24
  19. package/dist/cjs/Subject/RefSubject.js.map +1 -1
  20. package/dist/cjs/Subject/SynchronizedSubject.d.ts.map +1 -1
  21. package/dist/cjs/Subject/SynchronizedSubject.js +5 -6
  22. package/dist/cjs/Subject/SynchronizedSubject.js.map +1 -1
  23. package/dist/cjs/operator/multicast.d.ts +1 -1
  24. package/dist/cjs/operator/multicast.d.ts.map +1 -1
  25. package/dist/cjs/operator/multicast.js.map +1 -1
  26. package/dist/operator/multicast.d.ts +1 -1
  27. package/dist/operator/multicast.d.ts.map +1 -1
  28. package/dist/operator/multicast.js.map +1 -1
  29. package/dist/tsconfig.cjs.build.tsbuildinfo +1 -1
  30. package/package.json +2 -2
  31. package/src/Subject/HoldSubject.ts +1 -6
  32. package/src/Subject/RefSubject.test.ts +81 -0
  33. package/src/Subject/RefSubject.ts +257 -37
  34. package/src/Subject/SynchronizedSubject.ts +8 -7
  35. package/src/operator/multicast.ts +1 -1
  36. package/tsconfig.build.tsbuildinfo +1 -1
  37. package/coverage/base.css +0 -224
  38. package/coverage/block-navigation.js +0 -87
  39. package/coverage/clover.xml +0 -1483
  40. package/coverage/coverage-final.json +0 -40
  41. package/coverage/favicon.png +0 -0
  42. package/coverage/index.html +0 -191
  43. package/coverage/prettify.css +0 -1
  44. package/coverage/prettify.js +0 -2
  45. package/coverage/sort-arrow-sprite.png +0 -0
  46. package/coverage/sorter.js +0 -196
  47. package/coverage/src/Fx.ts.html +0 -274
  48. package/coverage/src/Subject/Subject.ts.html +0 -214
  49. package/coverage/src/Subject/index.html +0 -116
  50. package/coverage/src/_internal/RefCounter.ts.html +0 -313
  51. package/coverage/src/_internal/earlyExit.ts.html +0 -136
  52. package/coverage/src/_internal/index.html +0 -131
  53. package/coverage/src/constructor/at.ts.html +0 -118
  54. package/coverage/src/constructor/fromArray.ts.html +0 -148
  55. package/coverage/src/constructor/fromEffect.ts.html +0 -148
  56. package/coverage/src/constructor/fromEmitter.ts.html +0 -265
  57. package/coverage/src/constructor/fromIterable.ts.html +0 -172
  58. package/coverage/src/constructor/index.html +0 -236
  59. package/coverage/src/constructor/never.ts.html +0 -133
  60. package/coverage/src/constructor/serviceWith.ts.html +0 -122
  61. package/coverage/src/constructor/succeed.ts.html +0 -138
  62. package/coverage/src/constructor/suspend.ts.html +0 -130
  63. package/coverage/src/index.html +0 -116
  64. package/coverage/src/operator/concatMap.ts.html +0 -112
  65. package/coverage/src/operator/continueWith.ts.html +0 -169
  66. package/coverage/src/operator/delay.ts.html +0 -142
  67. package/coverage/src/operator/filter.ts.html +0 -190
  68. package/coverage/src/operator/filterEffect.ts.html +0 -214
  69. package/coverage/src/operator/filterMap.ts.html +0 -194
  70. package/coverage/src/operator/filterMapEffect.ts.html +0 -217
  71. package/coverage/src/operator/flatMap.ts.html +0 -211
  72. package/coverage/src/operator/flatMapConcurrently.ts.html +0 -141
  73. package/coverage/src/operator/index.html +0 -431
  74. package/coverage/src/operator/loop.ts.html +0 -214
  75. package/coverage/src/operator/map.ts.html +0 -163
  76. package/coverage/src/operator/merge.ts.html +0 -235
  77. package/coverage/src/operator/multicast.ts.html +0 -424
  78. package/coverage/src/operator/provideService.ts.html +0 -154
  79. package/coverage/src/operator/provideServiceEffect.ts.html +0 -190
  80. package/coverage/src/operator/scan.ts.html +0 -146
  81. package/coverage/src/operator/scanEffect.ts.html +0 -283
  82. package/coverage/src/operator/skipRepeats.ts.html +0 -214
  83. package/coverage/src/operator/slice.ts.html +0 -221
  84. package/coverage/src/operator/startWith.ts.html +0 -131
  85. package/coverage/src/operator/switchMap.ts.html +0 -295
  86. package/coverage/src/operator/withPermit.ts.html +0 -139
  87. package/coverage/src/run/collectAll.ts.html +0 -136
  88. package/coverage/src/run/index.html +0 -161
  89. package/coverage/src/run/observe.ts.html +0 -142
  90. package/coverage/src/run/reduce.ts.html +0 -149
  91. package/coverage/src/run/run.ts.html +0 -232
  92. package/coverage/tmp/coverage-13497-1671499520588-32.json +0 -1
  93. package/coverage/tmp/coverage-13497-1671499520647-31.json +0 -1
  94. package/coverage/tmp/coverage-13497-1671499520712-30.json +0 -1
  95. package/coverage/tmp/coverage-13497-1671499520782-26.json +0 -1
  96. package/coverage/tmp/coverage-13497-1671499520842-29.json +0 -1
  97. package/coverage/tmp/coverage-13497-1671499520906-28.json +0 -1
  98. package/coverage/tmp/coverage-13497-1671499520969-27.json +0 -1
  99. package/dist/constructor/fromFxEffectGen.d.ts +0 -5
  100. package/dist/constructor/fromFxEffectGen.d.ts.map +0 -1
  101. package/dist/constructor/fromFxEffectGen.js +0 -6
  102. package/dist/constructor/fromFxEffectGen.js.map +0 -1
  103. package/dist/constructor/genFx.d.ts +0 -5
  104. package/dist/constructor/genFx.d.ts.map +0 -1
  105. package/dist/constructor/genFx.js +0 -6
  106. package/dist/constructor/genFx.js.map +0 -1
  107. package/dist/operator/catch.d.ts +0 -3
  108. package/dist/operator/catch.d.ts.map +0 -1
  109. package/dist/operator/catch.js +0 -9
  110. package/dist/operator/catch.js.map +0 -1
  111. package/dist/operator/duringEffect.d.ts +0 -2
  112. package/dist/operator/duringEffect.d.ts.map +0 -1
  113. package/dist/operator/duringEffect.js +0 -2
  114. package/dist/operator/duringEffect.js.map +0 -1
  115. package/dist/operator/orDie.d.ts +0 -2
  116. package/dist/operator/orDie.d.ts.map +0 -1
  117. package/dist/operator/orDie.js +0 -2
  118. package/dist/operator/orDie.js.map +0 -1
  119. package/dist/operator/orDieWith.d.ts +0 -2
  120. package/dist/operator/orDieWith.d.ts.map +0 -1
  121. package/dist/operator/orDieWith.js +0 -2
  122. package/dist/operator/orDieWith.js.map +0 -1
  123. package/dist/operator/orElseCause.d.ts +0 -2
  124. package/dist/operator/orElseCause.d.ts.map +0 -1
  125. package/dist/operator/orElseCause.js +0 -2
  126. package/dist/operator/orElseCause.js.map +0 -1
  127. package/dist/operator/orElseEither.d.ts +0 -2
  128. package/dist/operator/orElseEither.d.ts.map +0 -1
  129. package/dist/operator/orElseEither.js +0 -2
  130. package/dist/operator/orElseEither.js.map +0 -1
  131. package/dist/operator/orElseFail.d.ts +0 -2
  132. package/dist/operator/orElseFail.d.ts.map +0 -1
  133. package/dist/operator/orElseFail.js +0 -2
  134. package/dist/operator/orElseFail.js.map +0 -1
  135. package/dist/operator/orElseSucceed.d.ts +0 -2
  136. package/dist/operator/orElseSucceed.d.ts.map +0 -1
  137. package/dist/operator/orElseSucceed.js +0 -2
  138. package/dist/operator/orElseSucceed.js.map +0 -1
  139. package/dist/operator/sampleEffect.d.ts +0 -10
  140. package/dist/operator/sampleEffect.d.ts.map +0 -1
  141. package/dist/operator/sampleEffect.js +0 -17
  142. package/dist/operator/sampleEffect.js.map +0 -1
  143. package/dist/operator/skipAfterEffect.d.ts +0 -2
  144. package/dist/operator/skipAfterEffect.d.ts.map +0 -1
  145. package/dist/operator/skipAfterEffect.js +0 -2
  146. package/dist/operator/skipAfterEffect.js.map +0 -1
  147. package/dist/operator/skipUntilEffect.d.ts +0 -2
  148. package/dist/operator/skipUntilEffect.d.ts.map +0 -1
  149. package/dist/operator/skipUntilEffect.js +0 -2
  150. package/dist/operator/skipUntilEffect.js.map +0 -1
  151. package/dist/operator/skipWhileEffect.d.ts +0 -2
  152. package/dist/operator/skipWhileEffect.d.ts.map +0 -1
  153. package/dist/operator/skipWhileEffect.js +0 -2
  154. package/dist/operator/skipWhileEffect.js.map +0 -1
  155. package/dist/operator/suceed.d.ts +0 -2
  156. package/dist/operator/suceed.d.ts.map +0 -1
  157. package/dist/operator/suceed.js +0 -2
  158. package/dist/operator/suceed.js.map +0 -1
  159. package/dist/operator/takeUntilEffect.d.ts +0 -2
  160. package/dist/operator/takeUntilEffect.d.ts.map +0 -1
  161. package/dist/operator/takeUntilEffect.js +0 -2
  162. package/dist/operator/takeUntilEffect.js.map +0 -1
  163. package/dist/operator/takeWhileEffect.d.ts +0 -2
  164. package/dist/operator/takeWhileEffect.d.ts.map +0 -1
  165. package/dist/operator/takeWhileEffect.js +0 -2
  166. package/dist/operator/takeWhileEffect.js.map +0 -1
@@ -3,17 +3,12 @@ import { type MutableRef, make } from '@effect/data/MutableRef'
3
3
  import { type Option, none } from '@effect/data/Option'
4
4
  import * as Effect from '@effect/io/Effect'
5
5
 
6
- import type { Fx, Sink } from '../Fx.js'
7
6
  import { never } from '../constructor/never.js'
8
7
  import { HoldFx } from '../operator/hold.js'
9
8
 
10
9
  import { isSubject, Subject } from './Subject.js'
11
10
 
12
- export interface HoldSubject<E, A>
13
- extends Fx<never, E, A>,
14
- Sink<never, E, A>,
15
- Subject.Variance<E, A>,
16
- HoldSubject.Variance<E, A> {
11
+ export interface HoldSubject<E, A> extends Subject<E, A>, HoldSubject.Variance<E, A> {
17
12
  readonly value: Effect.Effect<never, never, Option<A>>
18
13
  }
19
14
 
@@ -0,0 +1,81 @@
1
+ import { deepStrictEqual } from 'assert'
2
+
3
+ import * as Effect from '@effect/io/Effect'
4
+ import { describe, it } from 'vitest'
5
+
6
+ import { makeRef, RefSubject } from './RefSubject.js'
7
+
8
+ describe(import.meta.url, () => {
9
+ describe('RefSubject.tuple', () => {
10
+ it('allows joining RefSubjects into a single interface', async () => {
11
+ const test = Effect.scoped(
12
+ Effect.gen(function* ($) {
13
+ const a = yield* $(makeRef(() => 1))
14
+ const b = yield* $(makeRef(() => 2))
15
+ const c = yield* $(makeRef(() => 3))
16
+ const tupled = yield* $(RefSubject.tuple(a, b, c))
17
+
18
+ deepStrictEqual(yield* $(tupled.get), [1, 2, 3])
19
+
20
+ yield* $(b.set(4))
21
+
22
+ deepStrictEqual(yield* $(tupled.get), [1, 4, 3])
23
+
24
+ yield* $(tupled.set([5, 6, 7]))
25
+
26
+ deepStrictEqual(yield* $(a.get), 5)
27
+ deepStrictEqual(yield* $(b.get), 6)
28
+ deepStrictEqual(yield* $(c.get), 7)
29
+ }),
30
+ )
31
+
32
+ await Effect.runPromise(test)
33
+ })
34
+ })
35
+
36
+ describe('RefSubject.struct', () => {
37
+ it('allows joining RefSubjects into a single interface', async () => {
38
+ const test = Effect.scoped(
39
+ Effect.gen(function* ($) {
40
+ const a = yield* $(makeRef(() => 1))
41
+ const b = yield* $(makeRef(() => 2))
42
+ const c = yield* $(makeRef(() => 3))
43
+ const struct = yield* $(RefSubject.struct({ a, b, c }))
44
+
45
+ deepStrictEqual(yield* $(struct.get), { a: 1, b: 2, c: 3 })
46
+
47
+ yield* $(b.set(4))
48
+
49
+ deepStrictEqual(yield* $(struct.get), { a: 1, b: 4, c: 3 })
50
+
51
+ yield* $(struct.set({ a: 5, b: 6, c: 7 }))
52
+
53
+ deepStrictEqual(yield* $(a.get), 5)
54
+ deepStrictEqual(yield* $(b.get), 6)
55
+ deepStrictEqual(yield* $(c.get), 7)
56
+ }),
57
+ )
58
+
59
+ await Effect.runPromise(test)
60
+ })
61
+ })
62
+
63
+ describe('Computed', () => {
64
+ it('allows computing values from a RefSubject', async () => {
65
+ const test = Effect.scoped(
66
+ Effect.gen(function* ($) {
67
+ const refSubject = yield* $(makeRef(() => 1))
68
+ const computed = yield* $(refSubject.computeSync((a) => a + 42))
69
+
70
+ deepStrictEqual(yield* $(computed.get), 43)
71
+
72
+ yield* $(refSubject.set(2))
73
+
74
+ deepStrictEqual(yield* $(computed.get), 44)
75
+ }),
76
+ )
77
+
78
+ await Effect.runPromise(test)
79
+ })
80
+ })
81
+ })
@@ -1,57 +1,65 @@
1
1
  import { equals } from '@effect/data/Equal'
2
- import { identity, pipe } from '@effect/data/Function'
2
+ import { identity, pipe, dual, flow } from '@effect/data/Function'
3
3
  import * as MutableRef from '@effect/data/MutableRef'
4
4
  import * as Option from '@effect/data/Option'
5
+ import * as ReadonlyArray from '@effect/data/ReadonlyArray'
6
+ import * as ReadonlyRecord from '@effect/data/ReadonlyRecord'
7
+ import * as Equivalence from '@effect/data/typeclass/Equivalence'
5
8
  import * as Effect from '@effect/io/Effect'
9
+ import type * as Exit from '@effect/io/Exit'
6
10
  import * as Ref from '@effect/io/Ref'
11
+ import type * as Scope from '@effect/io/Scope'
7
12
 
8
- import { Fx } from '../Fx.js'
13
+ import { Fx, Sink } from '../Fx.js'
14
+ import { flatMapEffect } from '../operator/flatMapEffect.js'
15
+ import { hold } from '../operator/hold.js'
16
+ import { skipWhile } from '../operator/skipWhile.js'
17
+ import { observe } from '../run/observe.js'
9
18
 
10
19
  import { HoldSubject, isHoldSubject } from './HoldSubject.js'
11
20
  import { Subject } from './Subject.js'
12
21
 
13
22
  export interface RefSubject<A> extends HoldSubject<never, A>, Ref.Ref<A> {
23
+ readonly eq: Equivalence.Equivalence<A>
14
24
  readonly get: Effect.Effect<never, never, A>
15
25
  readonly set: (a: A) => Effect.Effect<never, never, A>
16
26
  readonly update: (f: (a: A) => A) => Effect.Effect<never, never, A>
17
- readonly delete: Effect.Effect<never, never, Option.Option<A>>
27
+ readonly delete: Effect.Effect<never, never, A>
28
+
29
+ readonly compute: <R, E, B>(
30
+ f: (a: A) => Effect.Effect<R, E, B>,
31
+ ) => Effect.Effect<R | Scope.Scope, never, Computed<E, B>>
32
+
33
+ readonly computeSync: <B>(f: (a: A) => B) => Effect.Effect<Scope.Scope, never, Computed<never, B>>
18
34
  }
19
35
 
20
36
  export function makeRef<A>(
21
37
  initial: () => A,
22
- eq: (a: A, b: A) => boolean = equals,
38
+ eq: Equivalence.Equivalence<A> = equals,
23
39
  ): Effect.Effect<never, never, RefSubject<A>> {
24
40
  return Effect.sync(() => RefSubject.unsafeMake(initial, eq))
25
41
  }
26
42
 
27
43
  export namespace RefSubject {
44
+ export type ValueOf<T> = T extends RefSubject<infer A> ? A : never
45
+
28
46
  export function unsafeMake<A>(
29
47
  initial: () => A,
30
- eq: (a: A, b: A) => boolean = equals,
48
+ eq: Equivalence.Equivalence<A> = equals,
31
49
  ): RefSubject<A> {
32
- const mutableRef = MutableRef.make(Option.some(initial()))
33
- const subject = HoldSubject.unsafeMake<never, A>(mutableRef)
50
+ const current = MutableRef.make(Option.some(initial()))
51
+ const subject = HoldSubject.unsafeMake<never, A>(current)
34
52
 
35
- const getValue = () =>
36
- pipe(
37
- mutableRef.get(),
38
- Option.getOrElse(() => {
39
- const a = initial()
53
+ const getValue = () => pipe(current.get(), Option.getOrElse(initial))
40
54
 
41
- mutableRef.set(Option.some(a))
42
-
43
- return a
44
- }),
45
- )
46
-
47
- const modify = <B>(f: (a: A) => [B, A]): Effect.Effect<never, never, B> =>
55
+ const modify = <B>(f: (a: A) => readonly [B, A]): Effect.Effect<never, never, B> =>
48
56
  Effect.suspendSucceed(() => {
49
- const current = getValue()
50
- const [b, a] = f(current)
57
+ const currentValue = getValue()
58
+ const [b, a] = f(currentValue)
51
59
 
52
- mutableRef.set(Option.some(a))
60
+ current.set(Option.some(a))
53
61
 
54
- if (eq(current, a)) {
62
+ if (eq(currentValue, a)) {
55
63
  return Effect.succeed(b)
56
64
  }
57
65
 
@@ -70,30 +78,242 @@ export namespace RefSubject {
70
78
  error: subject.error.bind(subject),
71
79
  end: subject.end,
72
80
  value: subject.value,
81
+ eq,
73
82
  modify,
74
- get get() {
75
- return Ref.get(refSubject)
76
- },
77
- set(a: A) {
78
- return Ref.updateAndGet(() => a)(refSubject)
79
- },
80
- update(f: (a: A) => A) {
81
- return Ref.updateAndGet(f)(refSubject)
82
- },
83
+ ...makeDerivations(modify, Effect.sync(getValue)),
83
84
  delete: Effect.sync(() => {
84
- const option = mutableRef.get()
85
+ const option = current.get()
86
+ const reset = initial()
87
+ current.set(Option.some(reset))
85
88
 
86
- // Next pull should recompute the initial value
87
- mutableRef.set(Option.none())
88
-
89
- return option
89
+ return Option.getOrElse(option, () => reset)
90
90
  }),
91
+ compute: (f) => makeComputed(refSubject, f),
92
+ computeSync: (f) => makeComputed(refSubject, (a) => Effect.sync(() => f(a))),
91
93
  }
92
94
 
93
95
  return refSubject
94
96
  }
97
+
98
+ export function tuple<Subjects extends ReadonlyArray.NonEmptyReadonlyArray<RefSubject<any>>>(
99
+ ...subjects: Subjects
100
+ ): Effect.Effect<
101
+ Scope.Scope,
102
+ never,
103
+ RefSubject<{ readonly [K in keyof Subjects]: ValueOf<Subjects[K]> }>
104
+ > {
105
+ type Val = { readonly [K in keyof Subjects]: ValueOf<Subjects[K]> }
106
+ const length = subjects.length
107
+
108
+ const getUnderlyingValues = Effect.tuple(
109
+ ...ReadonlyArray.mapNonEmpty(subjects, (s) => s.get),
110
+ ) as Effect.Effect<never, never, Val>
111
+
112
+ const eq = Equivalence.tuple(
113
+ ...ReadonlyArray.mapNonEmpty(subjects, (s) => s.eq),
114
+ ) as Equivalence.Equivalence<Val>
115
+
116
+ return Effect.gen(function* ($) {
117
+ const initial = yield* $(getUnderlyingValues)
118
+ const refSubject = unsafeMake(() => initial, eq)
119
+
120
+ const updateAtIndex =
121
+ <I extends keyof Val & number>(i: I) =>
122
+ (a: Val[I]) =>
123
+ Effect.gen(function* ($) {
124
+ const current = yield* $(refSubject.get)
125
+ const next = current.slice()
126
+ next[i] = a
127
+
128
+ yield* $(refSubject.set(next as Val))
129
+ })
130
+
131
+ // Override event to replicate events into underlying subjects
132
+ const event = (val: Val) =>
133
+ Effect.tuple(...ReadonlyArray.mapNonEmpty(subjects, (s, i) => s.event(val[i])))
134
+
135
+ // Override modify to replicate events into underlying subjects
136
+ const modify = <B>(f: (a: Val) => readonly [B, Val]): Effect.Effect<never, never, B> =>
137
+ Effect.gen(function* ($) {
138
+ const current = yield* $(getUnderlyingValues)
139
+ const [b, a] = f(current)
140
+
141
+ if (!eq(current, a)) {
142
+ // Will update this RefSubject using observe below
143
+ yield* $(event(a))
144
+ }
145
+
146
+ return b
147
+ })
148
+
149
+ // Listen to changes to each subject and update the derived subject
150
+ for (let i = 0; i < length; i++) {
151
+ yield* $(Effect.forkScoped(observe(updateAtIndex(i))(subjects[i])))
152
+ }
153
+
154
+ // Allow underlying subscriptions to start
155
+ yield* $(Effect.yieldNow())
156
+
157
+ const derived: RefSubject<Val> = {
158
+ ...refSubject,
159
+ event,
160
+ modify,
161
+ ...makeDerivations(modify, refSubject.get),
162
+ compute: (f) => makeComputed(derived, f),
163
+ }
164
+
165
+ return derived
166
+ })
167
+ }
168
+
169
+ export function struct<Subjects extends Readonly<Record<string, RefSubject<any>>>>(
170
+ subjects: Subjects,
171
+ ): Effect.Effect<
172
+ Scope.Scope,
173
+ never,
174
+ RefSubject<{ readonly [K in keyof Subjects]: ValueOf<Subjects[K]> }>
175
+ > {
176
+ type Val = { readonly [K in keyof Subjects]: ValueOf<Subjects[K]> }
177
+
178
+ const getUnderlyingValues = Effect.struct(
179
+ ReadonlyRecord.map(subjects, (s) => s.get),
180
+ ) as Effect.Effect<never, never, Val>
181
+
182
+ const eq = Equivalence.struct(
183
+ ReadonlyRecord.map(subjects, (s) => s.eq),
184
+ ) as Equivalence.Equivalence<Val>
185
+
186
+ return Effect.gen(function* ($) {
187
+ const initial = yield* $(getUnderlyingValues)
188
+ const refSubject = unsafeMake(() => initial, eq)
189
+
190
+ const updateAtKey =
191
+ <I extends keyof Val & string>(i: I) =>
192
+ (a: Val[I]) =>
193
+ Effect.gen(function* ($) {
194
+ const current = yield* $(refSubject.get)
195
+ const next = { ...current, [i]: a }
196
+
197
+ yield* $(refSubject.set(next as Val))
198
+ })
199
+
200
+ // Override event to replicate events into underlying subjects
201
+ const event = (val: Val) =>
202
+ Effect.struct(ReadonlyRecord.map(subjects, (s, i) => s.event(val[i])))
203
+
204
+ // Override modify to replicate events into underlying subjects
205
+ const modify = <B>(f: (a: Val) => readonly [B, Val]): Effect.Effect<never, never, B> =>
206
+ Effect.gen(function* ($) {
207
+ const current = yield* $(getUnderlyingValues)
208
+ const [b, a] = f(current)
209
+
210
+ if (!eq(current, a)) {
211
+ // Will update this RefSubject using observe below
212
+ yield* $(event(a))
213
+ }
214
+
215
+ return b
216
+ })
217
+
218
+ // Listen to changes to each subject and update the derived subject
219
+ for (const i in subjects) {
220
+ yield* $(Effect.forkScoped(observe(updateAtKey(i))(subjects[i])))
221
+ }
222
+
223
+ // Allow underlying subscriptions to start
224
+ yield* $(Effect.yieldNow())
225
+
226
+ const derived: RefSubject<Val> = {
227
+ ...refSubject,
228
+ event,
229
+ modify,
230
+ ...makeDerivations(modify, refSubject.get),
231
+ compute: (f) => makeComputed(derived, f),
232
+ }
233
+
234
+ return derived
235
+ })
236
+ }
237
+
238
+ const makeDerivations = <A>(
239
+ modify: <B>(f: (a: A) => readonly [B, A]) => Effect.Effect<never, never, B>,
240
+ getValue: Effect.Effect<never, never, A>,
241
+ ) => {
242
+ return {
243
+ get: getValue,
244
+ set(a: A) {
245
+ return modify(() => [a, a])
246
+ },
247
+ update(f: (a: A) => A) {
248
+ return modify((a) => {
249
+ const a2 = f(a)
250
+ return [a2, a2]
251
+ })
252
+ },
253
+ }
254
+ }
95
255
  }
96
256
 
97
257
  export function isRefSubject<A>(u: unknown): u is RefSubject<A> {
98
258
  return isHoldSubject(u) && Ref.RefTypeId in u
99
259
  }
260
+
261
+ export interface Computed<E, A> extends Fx<never, E, A> {
262
+ readonly get: Effect.Effect<never, E, A>
263
+ }
264
+
265
+ export namespace Computed {
266
+ export function make<A, R, E, B>(
267
+ input: RefSubject<A>,
268
+ compute: (a: A) => Effect.Effect<R, E, B>,
269
+ ): Effect.Effect<R | Scope.Scope, never, Computed<E, B>> {
270
+ return Effect.gen(function* ($) {
271
+ const compute_ = flow(compute, Effect.exit)
272
+ const initialInput = yield* $(input.get)
273
+ const initial = yield* $(compute_(initialInput))
274
+ const refSubject = yield* $(makeRef(() => initial))
275
+
276
+ yield* $(
277
+ pipe(
278
+ input,
279
+ skipWhile((x) => input.eq(x, initialInput)), // We already have the initial value, don't waste downstream resources
280
+ observe((a) => Effect.flatMap(compute_(a), refSubject.set)),
281
+ Effect.forkScoped,
282
+ ),
283
+ )
284
+
285
+ // Allow subscription to finalize as HoldFx under the hood of RefSubject schedules the start of the Fx.
286
+ yield* $(Effect.yieldNow())
287
+
288
+ return new ComputedImpl(refSubject)
289
+ })
290
+ }
291
+
292
+ class ComputedImpl<E, A> extends Fx.Variance<never, E, A> implements Computed<E, A> {
293
+ readonly fx: Fx<never, E, A>
294
+
295
+ constructor(readonly input: RefSubject<Exit.Exit<E, A>>) {
296
+ super()
297
+
298
+ this.fx = pipe(input, flatMapEffect(Effect.done), hold)
299
+ }
300
+
301
+ run<R3>(sink: Sink<R3, E, A>) {
302
+ return this.fx.run(sink)
303
+ }
304
+
305
+ readonly get = Effect.flatMap(this.input.get, Effect.done)
306
+ }
307
+ }
308
+
309
+ export const makeComputed: {
310
+ <A, R, E, B>(input: RefSubject<A>, compute: (a: A) => Effect.Effect<R, E, B>): Effect.Effect<
311
+ R | Scope.Scope,
312
+ never,
313
+ Computed<E, B>
314
+ >
315
+
316
+ <A, R, E, B>(compute: (a: A) => Effect.Effect<R, E, B>): (
317
+ input: RefSubject<A>,
318
+ ) => Effect.Effect<R | Scope.Scope, never, Computed<E, B>>
319
+ } = dual(2, Computed.make)
@@ -9,7 +9,7 @@ import * as Synchronized from '@effect/io/Ref/Synchronized'
9
9
  import { Fx } from '../Fx.js'
10
10
 
11
11
  import { HoldSubject } from './HoldSubject.js'
12
- import { isRefSubject, RefSubject } from './RefSubject.js'
12
+ import { Computed, isRefSubject, RefSubject } from './RefSubject.js'
13
13
  import { Subject } from './Subject.js'
14
14
 
15
15
  export interface SynchronizedSubject<A> extends RefSubject<A>, Synchronized.Synchronized<A> {
@@ -30,8 +30,7 @@ export namespace SynchronizedSubject {
30
30
  ): SynchronizedSubject<A> {
31
31
  const mutableRef = MutableRef.make(Option.some(initial()))
32
32
  const subject = HoldSubject.unsafeMake<never, A>(mutableRef)
33
- const semaphore = Effect.unsafeMakeSemaphore(1)
34
- const locked = semaphore.withPermits(1)
33
+ const locked = Effect.unsafeMakeSemaphore(1).withPermits(1)
35
34
 
36
35
  const getValue = () =>
37
36
  pipe(
@@ -83,6 +82,7 @@ export namespace SynchronizedSubject {
83
82
  error: subject.error.bind(subject),
84
83
  end: subject.end,
85
84
  value: subject.value,
85
+ eq,
86
86
  modify,
87
87
  modifyEffect,
88
88
  get get() {
@@ -98,13 +98,14 @@ export namespace SynchronizedSubject {
98
98
  return Synchronized.updateAndGetEffect(f)(synchronizedSubject)
99
99
  },
100
100
  delete: Effect.sync(() => {
101
- const option = mutableRef.get()
101
+ const value = getValue()
102
102
 
103
- // Next pull should recompute the initial value
104
- mutableRef.set(Option.none())
103
+ mutableRef.set(Option.some(value))
105
104
 
106
- return option
105
+ return value
107
106
  }),
107
+ compute: (f) => Computed.make(synchronizedSubject, f),
108
+ computeSync: (f) => Computed.make(synchronizedSubject, (a) => Effect.sync(() => f(a))),
108
109
  }
109
110
 
110
111
  return synchronizedSubject
@@ -16,7 +16,7 @@ export class MulticastFx<R, E, A>
16
16
  extends Fx.Variance<R, E, A>
17
17
  implements Fx<R, E, A>, Fx.Sink<never, E, A>
18
18
  {
19
- protected observers: Array<MulticastObserver<any, E, A>> = []
19
+ readonly observers: Array<MulticastObserver<any, E, A>> = []
20
20
  protected fiber: RuntimeFiber<never, unknown> | undefined
21
21
 
22
22
  constructor(readonly fx: Fx<R, E, A>) {