@typed/fx 1.1.1 → 1.2.1

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