@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.
- package/dist/RefSubject.js +6 -6
- package/dist/RefSubject.js.map +1 -1
- package/dist/cjs/RefSubject.js +6 -6
- package/dist/cjs/RefSubject.js.map +1 -1
- package/dist/cjs/data-first.d.ts +1 -0
- package/dist/cjs/data-first.d.ts.map +1 -1
- package/dist/cjs/data-first.js +1 -0
- package/dist/cjs/data-first.js.map +1 -1
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/keyed.d.ts +4 -0
- package/dist/cjs/keyed.d.ts.map +1 -0
- package/dist/cjs/keyed.js +144 -0
- package/dist/cjs/keyed.js.map +1 -0
- package/dist/cjs/switchMap.d.ts.map +1 -1
- package/dist/cjs/switchMap.js.map +1 -1
- package/dist/cjs/switchMapCause.d.ts.map +1 -1
- package/dist/cjs/switchMapCause.js +3 -3
- package/dist/cjs/switchMapCause.js.map +1 -1
- package/dist/cjs/switchMatch.js.map +1 -1
- package/dist/cjs/toReadonlyArray.d.ts.map +1 -1
- package/dist/cjs/toReadonlyArray.js +2 -7
- package/dist/cjs/toReadonlyArray.js.map +1 -1
- package/dist/data-first.d.ts +1 -0
- package/dist/data-first.d.ts.map +1 -1
- package/dist/data-first.js +1 -0
- package/dist/data-first.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/keyed.d.ts +4 -0
- package/dist/keyed.d.ts.map +1 -0
- package/dist/keyed.js +114 -0
- package/dist/keyed.js.map +1 -0
- package/dist/switchMap.d.ts.map +1 -1
- package/dist/switchMap.js.map +1 -1
- package/dist/switchMapCause.d.ts.map +1 -1
- package/dist/switchMapCause.js +3 -3
- package/dist/switchMapCause.js.map +1 -1
- package/dist/switchMatch.js.map +1 -1
- package/dist/toReadonlyArray.d.ts.map +1 -1
- package/dist/toReadonlyArray.js +2 -7
- package/dist/toReadonlyArray.js.map +1 -1
- package/dist/tsconfig.cjs.build.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/src/RefSubject.ts +6 -6
- package/src/data-first.ts +1 -0
- package/src/index.ts +21 -0
- package/src/keyed.test.ts +43 -0
- package/src/keyed.ts +212 -0
- package/src/switchMap.ts +1 -3
- package/src/switchMapCause.ts +5 -4
- package/src/switchMatch.ts +1 -1
- package/src/toReadonlyArray.ts +2 -8
- 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(
|
|
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>(
|
package/src/switchMapCause.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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>(
|
package/src/switchMatch.ts
CHANGED
|
@@ -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(
|
|
14
|
+
return Fx((sink) =>
|
|
15
15
|
withSwitch((fork) =>
|
|
16
16
|
fx.run(
|
|
17
17
|
Sink(
|
package/src/toReadonlyArray.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
}
|