@typed/fx 1.11.10 → 1.12.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 (58) hide show
  1. package/dist/RefSubject.js +6 -6
  2. package/dist/RefSubject.js.map +1 -1
  3. package/dist/cjs/RefSubject.js +6 -6
  4. package/dist/cjs/RefSubject.js.map +1 -1
  5. package/dist/cjs/data-first.d.ts +1 -0
  6. package/dist/cjs/data-first.d.ts.map +1 -1
  7. package/dist/cjs/data-first.js +1 -0
  8. package/dist/cjs/data-first.js.map +1 -1
  9. package/dist/cjs/index.d.ts +4 -0
  10. package/dist/cjs/index.d.ts.map +1 -1
  11. package/dist/cjs/index.js +2 -1
  12. package/dist/cjs/index.js.map +1 -1
  13. package/dist/cjs/keyed.d.ts +4 -0
  14. package/dist/cjs/keyed.d.ts.map +1 -0
  15. package/dist/cjs/keyed.js +144 -0
  16. package/dist/cjs/keyed.js.map +1 -0
  17. package/dist/cjs/switchMap.d.ts.map +1 -1
  18. package/dist/cjs/switchMap.js.map +1 -1
  19. package/dist/cjs/switchMapCause.d.ts.map +1 -1
  20. package/dist/cjs/switchMapCause.js +3 -3
  21. package/dist/cjs/switchMapCause.js.map +1 -1
  22. package/dist/cjs/switchMatch.js.map +1 -1
  23. package/dist/cjs/toReadonlyArray.d.ts.map +1 -1
  24. package/dist/cjs/toReadonlyArray.js +2 -7
  25. package/dist/cjs/toReadonlyArray.js.map +1 -1
  26. package/dist/data-first.d.ts +1 -0
  27. package/dist/data-first.d.ts.map +1 -1
  28. package/dist/data-first.js +1 -0
  29. package/dist/data-first.js.map +1 -1
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/keyed.d.ts +4 -0
  35. package/dist/keyed.d.ts.map +1 -0
  36. package/dist/keyed.js +114 -0
  37. package/dist/keyed.js.map +1 -0
  38. package/dist/switchMap.d.ts.map +1 -1
  39. package/dist/switchMap.js.map +1 -1
  40. package/dist/switchMapCause.d.ts.map +1 -1
  41. package/dist/switchMapCause.js +3 -3
  42. package/dist/switchMapCause.js.map +1 -1
  43. package/dist/switchMatch.js.map +1 -1
  44. package/dist/toReadonlyArray.d.ts.map +1 -1
  45. package/dist/toReadonlyArray.js +2 -7
  46. package/dist/toReadonlyArray.js.map +1 -1
  47. package/dist/tsconfig.cjs.build.tsbuildinfo +1 -1
  48. package/package.json +5 -4
  49. package/src/RefSubject.ts +6 -6
  50. package/src/data-first.ts +1 -0
  51. package/src/index.ts +21 -0
  52. package/src/keyed.test.ts +43 -0
  53. package/src/keyed.ts +212 -0
  54. package/src/switchMap.ts +1 -3
  55. package/src/switchMapCause.ts +5 -4
  56. package/src/switchMatch.ts +1 -1
  57. package/src/toReadonlyArray.ts +2 -8
  58. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,43 @@
1
+ import { millis } from '@effect/data/Duration'
2
+ import * as Effect from '@effect/io/Effect'
3
+ import { describe, it, expect } from 'vitest'
4
+
5
+ import { at } from './at.js'
6
+ import { keyed } from './keyed.js'
7
+ import { mergeAll } from './mergeAll.js'
8
+ import { succeed } from './succeed.js'
9
+ import { toReadonlyArray } from './toReadonlyArray.js'
10
+
11
+ describe(__filename, () => {
12
+ describe(keyed.name, () => {
13
+ it('allows replaying latest events to late subscribers', async () => {
14
+ const test = Effect.gen(function* ($) {
15
+ const inputs = mergeAll(
16
+ succeed([1, 2, 3]),
17
+ at([3, 2, 1], millis(200)),
18
+ at([4, 5, 6, 1], millis(400)),
19
+ )
20
+
21
+ let calls = 0
22
+
23
+ const fx = keyed(inputs, (source) => {
24
+ calls++
25
+ return source
26
+ })
27
+
28
+ const events = yield* $(toReadonlyArray(fx))
29
+
30
+ expect(events).toEqual([
31
+ [1, 2, 3],
32
+ [3, 2, 1],
33
+ [4, 5, 6, 1],
34
+ ])
35
+
36
+ // Should only be called once for each unique value
37
+ expect(calls).toEqual(6)
38
+ })
39
+
40
+ await Effect.runPromise(Effect.scoped(test))
41
+ })
42
+ })
43
+ })
package/src/keyed.ts ADDED
@@ -0,0 +1,212 @@
1
+ import * as MutableHashMap from '@effect/data/MutableHashMap'
2
+ import * as Option from '@effect/data/Option'
3
+ import * as ReadonlyArray from '@effect/data/ReadonlyArray'
4
+ import { Equivalence } from '@effect/data/typeclass/Equivalence'
5
+ import * as Effect from '@effect/io/Effect'
6
+ import * as Fiber from '@effect/io/Fiber'
7
+ import fastDeepEqual from 'fast-deep-equal'
8
+
9
+ import { Fx, Sink } from './Fx.js'
10
+ import { RefSubject } from './RefSubject.js'
11
+ import { Subject, makeHoldSubject } from './Subject.js'
12
+ import { Cause } from './externals.js'
13
+ import { ScopedFork, withScopedFork } from './helpers.js'
14
+
15
+ export function keyed<R, E, A, R2, E2, B>(
16
+ fx: Fx<R, E, readonly A[]>,
17
+ f: (a: Fx<never, never, A>) => Fx<R2, E2, B>,
18
+ eq: Equivalence<A> = fastDeepEqual,
19
+ ): Fx<R | R2, E | E2, readonly B[]> {
20
+ return Fx(<R3>(sink: Sink<R3, E | E2, readonly B[]>) =>
21
+ withScopedFork((fork) =>
22
+ Effect.gen(function* ($) {
23
+ const state = createKeyedState<A, B>()
24
+ const difference = ReadonlyArray.difference(eq)
25
+ const intersection = ReadonlyArray.intersection(eq)
26
+ const emit = emitWhenReady(state)
27
+
28
+ // Let output emit to the sink
29
+ const fiber = yield* $(fork(state.output.run(sink)))
30
+
31
+ // Listen to the input and update the state
32
+ yield* $(
33
+ fx.run(
34
+ Sink(
35
+ (as) =>
36
+ updateState({
37
+ state,
38
+ updated: as,
39
+ f,
40
+ fork,
41
+ difference,
42
+ intersection,
43
+ emit,
44
+ error: sink.error,
45
+ }),
46
+ sink.error,
47
+ ),
48
+ ),
49
+ )
50
+
51
+ yield* $(endAll(state))
52
+
53
+ // When the source stream ends we wait for the remaining fibers to end
54
+ yield* $(Fiber.joinAll(Array.from(state.fibers).map((x) => x[1])))
55
+
56
+ // Terminate the output fiber
57
+ yield* $(Fiber.interrupt(fiber))
58
+ }),
59
+ ),
60
+ )
61
+ }
62
+
63
+ type KeyedState<A, B> = {
64
+ previous: readonly A[]
65
+ ended: boolean
66
+
67
+ readonly subjects: MutableHashMap.MutableHashMap<A, Subject<never, A>>
68
+ readonly fibers: MutableHashMap.MutableHashMap<A, Fiber.RuntimeFiber<never, void>>
69
+ readonly values: MutableHashMap.MutableHashMap<A, B>
70
+ readonly output: Subject<never, readonly B[]>
71
+ }
72
+
73
+ function createKeyedState<A, B>(): KeyedState<A, B> {
74
+ return {
75
+ previous: [],
76
+ ended: false,
77
+ subjects: MutableHashMap.empty(),
78
+ fibers: MutableHashMap.empty(),
79
+ values: MutableHashMap.empty(),
80
+ output: makeHoldSubject<never, readonly B[]>(),
81
+ }
82
+ }
83
+
84
+ function updateState<A, B, R2, E2, R3>({
85
+ state,
86
+ updated,
87
+ f,
88
+ fork,
89
+ difference,
90
+ intersection,
91
+ emit,
92
+ error,
93
+ }: {
94
+ state: KeyedState<A, B>
95
+ updated: readonly A[]
96
+ f: (a: Fx<never, never, A>) => Fx<R2, E2, B>
97
+ fork: ScopedFork
98
+ difference: (self: Iterable<A>, that: Iterable<A>) => A[]
99
+ intersection: (self: Iterable<A>, that: Iterable<A>) => A[]
100
+ emit: Effect.Effect<never, never, void>
101
+ error: (e: Cause.Cause<E2>) => Effect.Effect<R3, never, void>
102
+ }) {
103
+ return Effect.gen(function* ($) {
104
+ const added = difference(updated, state.previous)
105
+ const removed = difference(state.previous, updated)
106
+ const unchanged = intersection(updated, state.previous)
107
+
108
+ state.previous = updated
109
+
110
+ // Remove values that are no longer in the stream
111
+ yield* $(Effect.forEachParDiscard(removed, (value) => removeValue(state, value)))
112
+
113
+ // Add values that are new to the stream
114
+ yield* $(
115
+ Effect.forEachParDiscard(added, (value) => addValue({ state, value, f, fork, emit, error })),
116
+ )
117
+
118
+ // Update values that are still in the stream
119
+ yield* $(Effect.forEachParDiscard(unchanged, (value) => updateValue(state, value)))
120
+
121
+ // If nothing was added or removed, emit the current values
122
+ if (added.length === 0 && removed.length === 0) {
123
+ yield* $(emit)
124
+ }
125
+ })
126
+ }
127
+
128
+ function removeValue<A, B>(state: KeyedState<A, B>, value: A) {
129
+ return Effect.gen(function* ($) {
130
+ const subject = MutableHashMap.get(state.subjects, value)
131
+
132
+ if (Option.isSome(subject)) yield* $(subject.value.end())
133
+
134
+ const fiber = MutableHashMap.get(state.fibers, value)
135
+
136
+ if (Option.isSome(fiber)) yield* $(Fiber.interrupt(fiber.value))
137
+
138
+ MutableHashMap.remove(state.values, value)
139
+ MutableHashMap.remove(state.subjects, value)
140
+ MutableHashMap.remove(state.fibers, value)
141
+ })
142
+ }
143
+
144
+ function addValue<A, B, R2, E2, R3>({
145
+ state,
146
+ value,
147
+ f,
148
+ fork,
149
+ emit,
150
+ error,
151
+ }: {
152
+ state: KeyedState<A, B>
153
+ value: A
154
+ f: (a: Fx<never, never, A>) => Fx<R2, E2, B>
155
+ fork: ScopedFork
156
+ emit: Effect.Effect<never, never, void>
157
+ error: (e: Cause.Cause<E2>) => Effect.Effect<R3, never, void>
158
+ }) {
159
+ return Effect.gen(function* ($) {
160
+ const subject = RefSubject.unsafeMake<never, A>(Effect.succeed(value))
161
+ const fx = f(subject)
162
+ const fiber = yield* $(
163
+ fork(
164
+ fx.run(
165
+ Sink(
166
+ (b: B) =>
167
+ Effect.suspend(() => {
168
+ MutableHashMap.set(state.values, value, b)
169
+ return emit
170
+ }),
171
+ error,
172
+ ),
173
+ ),
174
+ ),
175
+ )
176
+
177
+ MutableHashMap.set(state.subjects, value, subject)
178
+ MutableHashMap.set(state.fibers, value, fiber)
179
+ })
180
+ }
181
+
182
+ function updateValue<A, B>(state: KeyedState<A, B>, value: A) {
183
+ return Effect.gen(function* ($) {
184
+ const subject = MutableHashMap.get(state.subjects, value)
185
+
186
+ // Send the current value
187
+ if (Option.isSome(subject)) {
188
+ yield* $(subject.value.event(value))
189
+ }
190
+ })
191
+ }
192
+
193
+ function emitWhenReady<A, B>(state: KeyedState<A, B>) {
194
+ return Effect.suspend(() => {
195
+ const values = ReadonlyArray.filterMap(state.previous, (value) =>
196
+ MutableHashMap.get(state.values, value),
197
+ )
198
+
199
+ // When all of the values have resolved at least once, emit the output
200
+ if (values.length === state.previous.length) {
201
+ return state.output.event(values)
202
+ }
203
+
204
+ return Effect.unit()
205
+ })
206
+ }
207
+
208
+ function endAll<A, B>(state: KeyedState<A, B>) {
209
+ return Effect.gen(function* ($) {
210
+ yield* $(Effect.forEachParDiscard(state.subjects, ([, subject]) => subject.end()))
211
+ })
212
+ }
package/src/switchMap.ts CHANGED
@@ -7,9 +7,7 @@ export function switchMap<R, E, A, R2, E2, B>(
7
7
  fx: Fx<R, E, A>,
8
8
  f: (a: A) => Fx<R2, E2, B>,
9
9
  ): Fx<R | R2, E | E2, B> {
10
- return Fx(<R3>(sink: Sink<R3, E | E2, B>) =>
11
- withSwitch((fork) => fx.run(Sink((a) => fork(f(a).run(sink)), sink.error))),
12
- )
10
+ return Fx((sink) => withSwitch((fork) => fx.run(Sink((a) => fork(f(a).run(sink)), sink.error))))
13
11
  }
14
12
 
15
13
  export function switchMapEffect<R, E, A, R2, E2, B>(
@@ -1,17 +1,18 @@
1
1
  import { pipe } from '@effect/data/Function'
2
2
 
3
- import { Fx } from './Fx.js'
3
+ import { Fx, Sink } from './Fx.js'
4
4
  import { Cause, Effect, Either } from './externals.js'
5
5
  import { failCause } from './failCause.js'
6
6
  import { fromEffect } from './fromEffect.js'
7
- import { succeed } from './succeed.js'
8
- import { switchMatchCause } from './switchMatch.js'
7
+ import { withSwitch } from './helpers.js'
9
8
 
10
9
  export function switchMapCause<R, E, A, R2, E2, B>(
11
10
  fx: Fx<R, E, A>,
12
11
  f: (cause: Cause.Cause<E>) => Fx<R2, E2, B>,
13
12
  ): Fx<R | R2, E2, A | B> {
14
- return switchMatchCause(fx, f, succeed)
13
+ return Fx((sink) =>
14
+ withSwitch((fork) => fx.run(Sink(sink.event, (cause) => fork(f(cause).run(sink))))),
15
+ )
15
16
  }
16
17
 
17
18
  export function switchMapCauseEffect<R, E, A, R2, E2, B>(
@@ -11,7 +11,7 @@ export function switchMatchCause<R, E, A, R2, E2, B, R3, E3, C>(
11
11
  f: (cause: Cause.Cause<E>) => Fx<R2, E2, B>,
12
12
  g: (a: A) => Fx<R3, E3, C>,
13
13
  ): Fx<R | R2 | R3, E2 | E3, B | C> {
14
- return Fx(<R4>(sink: Sink<R4, E2 | E3, B | C>) =>
14
+ return Fx((sink) =>
15
15
  withSwitch((fork) =>
16
16
  fx.run(
17
17
  Sink(
@@ -2,16 +2,10 @@ import type { Scope } from '@effect/io/Scope'
2
2
 
3
3
  import type { Fx } from './Fx.js'
4
4
  import { Effect } from './externals.js'
5
- import { observe } from './observe.js'
5
+ import { toArray } from './toArray.js'
6
6
 
7
7
  export function toReadonlyArray<R, E, A>(
8
8
  fx: Fx<R, E, A>,
9
9
  ): Effect.Effect<R | Scope, E, ReadonlyArray<A>> {
10
- return Effect.gen(function* ($) {
11
- const array: Array<A> = []
12
-
13
- yield* $(observe(fx, (a) => Effect.sync(() => array.push(a))))
14
-
15
- return array
16
- })
10
+ return toArray(fx)
17
11
  }