|
1 | | -import type { Stream, Subscription } from "effection"; |
| 1 | +import { |
| 2 | + createQueue, |
| 3 | + createSignal, |
| 4 | + resource, |
| 5 | + SignalQueueFactory, |
| 6 | + spawn, |
| 7 | + type Operation, |
| 8 | + type Stream, |
| 9 | + type Subscription, |
| 10 | +} from "effection"; |
2 | 11 |
|
3 | 12 | /** |
4 | | - * Converts any stream into a multicast stream that produces latest value |
5 | | - * to new subscribers. It's designed to be analagous in function to [RxJS |
6 | | - * BehaviorSubject](https://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject). |
| 13 | + * Converts any stream into a multicast stream that replays the latest value |
| 14 | + * to new subscribers. Analogous to |
| 15 | + * [RxJS BehaviorSubject](https://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject). |
7 | 16 | * |
8 | | - * @returns A function that takes a stream and returns a multicast stream |
| 17 | + * Applying the subject to a stream returns a resource. Yielding that resource |
| 18 | + * starts an internal drain that actively tracks the upstream, so late |
| 19 | + * subscribers always receive the most recent value — even if no other |
| 20 | + * subscriber has pulled it. |
| 21 | + * |
| 22 | + * @returns A function that takes a stream and returns a resource providing |
| 23 | + * a multicast stream. |
9 | 24 | * |
10 | 25 | * @example |
11 | 26 | * ```ts |
12 | 27 | * const subject = createSubject<number>(); |
13 | | - * const downstream = subject(upstream); |
| 28 | + * const downstream = yield* subject(upstream); |
14 | 29 | * |
15 | | - * const sub1 = yield* downstream; // subscribes to upstream |
| 30 | + * const sub1 = yield* downstream; // subscribes |
16 | 31 | * yield* upstream.send(1); |
17 | 32 | * yield* sub1.next(); // { done: false, value: 1 } |
18 | 33 | * |
19 | 34 | * const sub2 = yield* downstream; // late subscriber |
20 | | - * yield* sub2.next(); // { done: false, value: 1 } - gets latest value |
21 | | - * ``` |
22 | | - * |
23 | | - * Use it with a pipe operator to convert any stream into a behavior subject. |
24 | | - * |
25 | | - * @example |
26 | | - * ``` |
27 | | - * let source = createChannel<string, void>(); |
28 | | - * let subject = createSubject<string>(); |
29 | | - * |
30 | | - * let pipeline = pipe([ |
31 | | - * top, |
32 | | - * transform1, |
33 | | - * transform2, |
34 | | - * subject, |
35 | | - * ]); |
| 35 | + * yield* sub2.next(); // { done: false, value: 1 } — gets latest value |
36 | 36 | * ``` |
37 | 37 | */ |
38 | 38 | export function createSubject<T>( |
39 | 39 | initial?: T, |
40 | | -): <TClose>(stream: Stream<T, TClose>) => Stream<T, TClose> { |
| 40 | +): <TClose>(stream: Stream<T, TClose>) => Operation<Stream<T, TClose>> { |
41 | 41 | let current: IteratorResult<T> | undefined = |
42 | 42 | typeof initial !== "undefined" |
43 | 43 | ? { done: false, value: initial } |
44 | 44 | : undefined; |
45 | 45 |
|
46 | | - return <TClose>(stream: Stream<T, TClose>) => ({ |
47 | | - *[Symbol.iterator]() { |
48 | | - let upstream = yield* stream; |
| 46 | + return <TClose>(stream: Stream<T, TClose>) => |
| 47 | + resource<Stream<T, TClose>>(function* (provide) { |
| 48 | + const relay = createSignal<T, TClose>(); |
| 49 | + let closed = false; |
49 | 50 |
|
50 | | - let tracking: Subscription<T, TClose> = { |
51 | | - *next() { |
52 | | - current = yield* upstream.next(); |
53 | | - return current; |
54 | | - }, |
55 | | - }; |
| 51 | + // Install a custom queue that updates `current` synchronously |
| 52 | + // when values arrive — before the drain task gets scheduled. |
| 53 | + // This guarantees late subscribers always see the latest value |
| 54 | + // even when signal.send() is called within a running operation. |
| 55 | + yield* SignalQueueFactory.set(<U, UClose = never>() => { |
| 56 | + const queue = createQueue<U, UClose>(); |
| 57 | + return { |
| 58 | + ...queue, |
| 59 | + add(value: U) { |
| 60 | + current = { done: false, value: value as unknown as T }; |
| 61 | + queue.add(value); |
| 62 | + }, |
| 63 | + close(value: UClose) { |
| 64 | + current = { done: true, value } as unknown as IteratorResult<T>; |
| 65 | + closed = true; |
| 66 | + queue.close(value); |
| 67 | + }, |
| 68 | + }; |
| 69 | + }); |
56 | 70 |
|
57 | | - let iterator: Subscription<T, TClose> = current |
58 | | - ? { |
59 | | - *next() { |
60 | | - iterator = tracking; |
61 | | - return current!; |
62 | | - }, |
| 71 | + // Subscribe to upstream eagerly so its queue starts buffering |
| 72 | + // before provide() hands back control to callers. |
| 73 | + const upstream = yield* stream; |
| 74 | + |
| 75 | + // Drain owns the read loop that multicasts values to relay subscribers. |
| 76 | + // Lives in the resource scope — outlives any individual subscriber. |
| 77 | + yield* spawn(function* () { |
| 78 | + let result = yield* upstream.next(); |
| 79 | + while (!result.done) { |
| 80 | + relay.send(result.value); |
| 81 | + result = yield* upstream.next(); |
| 82 | + } |
| 83 | + relay.close(result.value); |
| 84 | + }); |
| 85 | + |
| 86 | + yield* provide({ |
| 87 | + *[Symbol.iterator]() { |
| 88 | + // Post-close: Effection signals are stateless — subscribing after |
| 89 | + // relay.close() would hang. Return captured close result directly. |
| 90 | + if (closed && current) { |
| 91 | + let snapshot = current; |
| 92 | + return { |
| 93 | + *next() { |
| 94 | + return snapshot as IteratorResult<T, TClose>; |
| 95 | + }, |
| 96 | + }; |
63 | 97 | } |
64 | | - : tracking; |
65 | 98 |
|
66 | | - return { |
67 | | - next: () => iterator.next(), |
68 | | - }; |
69 | | - }, |
70 | | - }); |
| 99 | + let subscription: Subscription<T, TClose> = yield* relay; |
| 100 | + let snapshot = current; |
| 101 | + let replayed = !snapshot; |
| 102 | + |
| 103 | + return { |
| 104 | + *next() { |
| 105 | + if (!replayed) { |
| 106 | + replayed = true; |
| 107 | + return snapshot! as IteratorResult<T, TClose>; |
| 108 | + } |
| 109 | + return yield* subscription.next(); |
| 110 | + }, |
| 111 | + }; |
| 112 | + }, |
| 113 | + }); |
| 114 | + }); |
71 | 115 | } |
0 commit comments