Skip to content

Commit c5f39d2

Browse files
committed
✨ Iterate Subscriptions with each()
A pesistent gotcha that folks have with iterating streams is that there can arise a race condition where you want to start consuming a stream, but items have already been sent to it by the time your consumer fires up. The fix is to capture a live subscription _before_ your consumer loop fires up. But this leaves you in a pickle because now you can't use the convenient `each()` helper to do your iteration. This allows the argument to `each()` to either be a stream or a subscription. That way, you can have the best of both worlds.
1 parent 3f41787 commit c5f39d2

2 files changed

Lines changed: 31 additions & 1 deletion

File tree

lib/each.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { race } from "./race.ts";
44
import { resource } from "./resource.ts";
55
import { withResolvers } from "./with-resolvers.ts";
66
import { useScope } from "./scope.ts";
7+
import { constant } from "./constant.ts";
78

89
/**
910
* Consume an effection stream using a simple for-of loop.
@@ -29,9 +30,17 @@ import { useScope } from "./scope.ts";
2930
* @param stream - the stream to iterate
3031
* @returns an operation to iterate `stream`
3132
*/
32-
export function each<T>(stream: Stream<T, unknown>): Operation<Iterable<T>> {
33+
export function each<T>(
34+
enumerable: Stream<T, unknown> | Subscription<T, unknown>,
35+
): Operation<Iterable<T>> {
3336
return {
3437
*[Symbol.iterator]() {
38+
let stream =
39+
typeof (enumerable as Subscription<T, unknown>).next ===
40+
"function"
41+
? constant(enumerable as Subscription<T, unknown>)
42+
: enumerable as Stream<T, unknown>;
43+
3544
let scope = yield* useScope();
3645
if (!scope.hasOwn(EachStack)) {
3746
scope.set(EachStack, []);

test/each.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it } from "./suite.ts";
22
import {
3+
createQueue,
34
each,
45
type Operation,
56
resource,
@@ -54,6 +55,26 @@ describe("each", () => {
5455
});
5556
});
5657

58+
it("can iterate subscriptions", async () => {
59+
await run(function* () {
60+
let queue = createQueue<number, void>();
61+
62+
queue.add(1);
63+
queue.add(2);
64+
queue.add(3);
65+
queue.close();
66+
67+
let items = [];
68+
69+
for (let num of yield* each(queue)) {
70+
items.push(num);
71+
yield* each.next();
72+
}
73+
74+
expect(items).toEqual([1, 2, 3]);
75+
});
76+
});
77+
5778
it("handles context correctly if you break out of a loop", async () => {
5879
await expect(run(function* () {
5980
let seq = sequence("hello world");

0 commit comments

Comments
 (0)