@effectionx/stream-helpers 0.4.1 → 0.6.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 (98) hide show
  1. package/README.md +47 -0
  2. package/batch.test.ts +112 -0
  3. package/batch.ts +107 -0
  4. package/dist/batch.d.ts.map +1 -0
  5. package/dist/batch.js +86 -0
  6. package/dist/filter.d.ts.map +1 -0
  7. package/dist/filter.js +43 -0
  8. package/dist/for-each.d.ts.map +1 -0
  9. package/dist/lines.d.ts +29 -0
  10. package/dist/lines.d.ts.map +1 -0
  11. package/dist/lines.js +61 -0
  12. package/dist/map.d.ts.map +1 -0
  13. package/dist/map.js +26 -0
  14. package/dist/mod.d.ts +9 -0
  15. package/dist/mod.d.ts.map +1 -0
  16. package/{esm → dist}/mod.js +2 -0
  17. package/dist/subject.d.ts +38 -0
  18. package/dist/subject.d.ts.map +1 -0
  19. package/dist/subject.js +60 -0
  20. package/dist/test-helpers/faucet.d.ts.map +1 -0
  21. package/dist/test-helpers.d.ts +2 -0
  22. package/dist/test-helpers.d.ts.map +1 -0
  23. package/dist/tracker.d.ts.map +1 -0
  24. package/{esm → dist}/tracker.js +12 -14
  25. package/dist/tsconfig.tsbuildinfo +1 -0
  26. package/dist/valve.d.ts.map +1 -0
  27. package/dist/valve.js +46 -0
  28. package/filter.test.ts +47 -0
  29. package/filter.ts +48 -0
  30. package/for-each.test.ts +42 -0
  31. package/{script/for-each.d.ts → for-each.ts} +13 -2
  32. package/lines.ts +74 -0
  33. package/map.test.ts +50 -0
  34. package/map.ts +33 -0
  35. package/mod.ts +8 -0
  36. package/package.json +29 -23
  37. package/subject.test.ts +93 -0
  38. package/subject.ts +67 -0
  39. package/test-helpers/faucet.test.ts +120 -0
  40. package/{script/test-helpers/faucet.d.ts → test-helpers/faucet.ts} +56 -25
  41. package/test-helpers.ts +1 -0
  42. package/tracker.test.ts +109 -0
  43. package/tracker.ts +57 -0
  44. package/tsconfig.json +20 -0
  45. package/valve.test.ts +52 -0
  46. package/valve.ts +73 -0
  47. package/esm/batch.d.ts.map +0 -1
  48. package/esm/batch.js +0 -89
  49. package/esm/filter.d.ts.map +0 -1
  50. package/esm/filter.js +0 -45
  51. package/esm/for-each.d.ts.map +0 -1
  52. package/esm/map.d.ts.map +0 -1
  53. package/esm/map.js +0 -28
  54. package/esm/mod.d.ts +0 -7
  55. package/esm/mod.d.ts.map +0 -1
  56. package/esm/package.json +0 -3
  57. package/esm/test-helpers/faucet.d.ts.map +0 -1
  58. package/esm/test-helpers.d.ts +0 -2
  59. package/esm/test-helpers.d.ts.map +0 -1
  60. package/esm/tracker.d.ts.map +0 -1
  61. package/esm/valve.d.ts.map +0 -1
  62. package/esm/valve.js +0 -48
  63. package/script/batch.d.ts +0 -21
  64. package/script/batch.d.ts.map +0 -1
  65. package/script/batch.js +0 -92
  66. package/script/filter.d.ts +0 -23
  67. package/script/filter.d.ts.map +0 -1
  68. package/script/filter.js +0 -48
  69. package/script/for-each.d.ts.map +0 -1
  70. package/script/for-each.js +0 -36
  71. package/script/map.d.ts +0 -9
  72. package/script/map.d.ts.map +0 -1
  73. package/script/map.js +0 -31
  74. package/script/mod.d.ts +0 -7
  75. package/script/mod.d.ts.map +0 -1
  76. package/script/mod.js +0 -22
  77. package/script/package.json +0 -3
  78. package/script/test-helpers/faucet.d.ts.map +0 -1
  79. package/script/test-helpers/faucet.js +0 -75
  80. package/script/test-helpers.d.ts +0 -2
  81. package/script/test-helpers.d.ts.map +0 -1
  82. package/script/test-helpers.js +0 -17
  83. package/script/tracker.d.ts +0 -24
  84. package/script/tracker.d.ts.map +0 -1
  85. package/script/tracker.js +0 -42
  86. package/script/valve.d.ts +0 -37
  87. package/script/valve.d.ts.map +0 -1
  88. package/script/valve.js +0 -51
  89. /package/{esm → dist}/batch.d.ts +0 -0
  90. /package/{esm → dist}/filter.d.ts +0 -0
  91. /package/{esm → dist}/for-each.d.ts +0 -0
  92. /package/{esm → dist}/for-each.js +0 -0
  93. /package/{esm → dist}/map.d.ts +0 -0
  94. /package/{esm → dist}/test-helpers/faucet.d.ts +0 -0
  95. /package/{esm → dist}/test-helpers/faucet.js +0 -0
  96. /package/{esm → dist}/test-helpers.js +0 -0
  97. /package/{esm → dist}/tracker.d.ts +0 -0
  98. /package/{esm → dist}/valve.d.ts +0 -0
package/package.json CHANGED
@@ -1,36 +1,42 @@
1
1
  {
2
2
  "name": "@effectionx/stream-helpers",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
+ "type": "module",
5
+ "main": "./dist/mod.js",
6
+ "types": "./dist/mod.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "development": "./mod.ts",
10
+ "default": "./dist/mod.js"
11
+ },
12
+ "./test-helpers": {
13
+ "development": "./test-helpers.ts",
14
+ "default": "./dist/test-helpers.js"
15
+ }
16
+ },
17
+ "peerDependencies": {
18
+ "effection": "^3 || ^4"
19
+ },
20
+ "dependencies": {
21
+ "@effectionx/signals": "workspace:*",
22
+ "@effectionx/timebox": "workspace:*",
23
+ "immutable": "^5",
24
+ "remeda": "^2"
25
+ },
26
+ "license": "MIT",
4
27
  "author": "engineering@frontside.com",
5
28
  "repository": {
6
29
  "type": "git",
7
30
  "url": "git+https://github.com/thefrontside/effectionx.git"
8
31
  },
9
- "license": "MIT",
10
32
  "bugs": {
11
33
  "url": "https://github.com/thefrontside/effectionx/issues"
12
34
  },
13
- "main": "./script/mod.js",
14
- "module": "./esm/mod.js",
15
- "exports": {
16
- ".": {
17
- "import": "./esm/mod.js",
18
- "require": "./script/mod.js"
19
- },
20
- "./test-helpers": {
21
- "import": "./esm/test-helpers.js",
22
- "require": "./script/test-helpers.js"
23
- }
24
- },
25
- "scripts": {},
26
35
  "engines": {
27
- "node": ">= 16"
36
+ "node": ">= 22"
28
37
  },
29
38
  "sideEffects": false,
30
- "dependencies": {
31
- "@effectionx/signals": "^0.4.0",
32
- "@effectionx/timebox": "^0.3.0",
33
- "effection": "^3 || ^4.0.0-0"
34
- },
35
- "_generatedBy": "dnt@dev"
36
- }
39
+ "devDependencies": {
40
+ "@effectionx/bdd": "workspace:*"
41
+ }
42
+ }
@@ -0,0 +1,93 @@
1
+ import {
2
+ createChannel,
3
+ type Operation,
4
+ type Stream,
5
+ type Subscription,
6
+ } from "effection";
7
+ import { beforeEach, describe, it } from "@effectionx/bdd";
8
+ import { expect } from "expect";
9
+
10
+ import { createSubject } from "./subject.ts";
11
+
12
+ function* next<T, TClose>(
13
+ subscription: Subscription<T, TClose>,
14
+ ): Operation<T | TClose> {
15
+ const item = yield* subscription.next();
16
+ if (item.done) {
17
+ return item.value;
18
+ }
19
+ return item.value;
20
+ }
21
+
22
+ describe("subject", () => {
23
+ let subject = createSubject<number>();
24
+ let upstream = createChannel<number, string>();
25
+ let downstream: Stream<number, string>;
26
+
27
+ beforeEach(function* () {
28
+ subject = createSubject();
29
+
30
+ upstream = createChannel();
31
+
32
+ downstream = subject(upstream);
33
+ });
34
+
35
+ it("allows multiple subscribers", function* () {
36
+ const subscriber1 = yield* downstream;
37
+ const subscriber2 = yield* downstream;
38
+
39
+ yield* upstream.send(1);
40
+ yield* upstream.send(2);
41
+
42
+ // 1 multicast to both
43
+ expect(yield* next(subscriber1)).toEqual(1);
44
+ expect(yield* next(subscriber2)).toEqual(1);
45
+
46
+ // 2 multicast to both
47
+ expect(yield* next(subscriber1)).toEqual(2);
48
+ expect(yield* next(subscriber2)).toEqual(2);
49
+ });
50
+
51
+ it("each later subscribers get latest value", function* () {
52
+ const subscriber1 = yield* downstream;
53
+ yield* upstream.send(1);
54
+ expect(yield* next(subscriber1)).toEqual(1);
55
+
56
+ yield* upstream.send(2);
57
+ expect(yield* next(subscriber1)).toEqual(2);
58
+
59
+ const subscriber2 = yield* downstream;
60
+ expect(yield* next(subscriber2)).toEqual(2);
61
+ });
62
+
63
+ it("sends closing value to all subscribers", function* () {
64
+ const subscriber1 = yield* downstream;
65
+ const subscriber2 = yield* downstream;
66
+
67
+ yield* upstream.send(1);
68
+ yield* upstream.close("bye");
69
+
70
+ // 1 multicast to both
71
+ expect(yield* next(subscriber1)).toEqual(1);
72
+ expect(yield* next(subscriber2)).toEqual(1);
73
+
74
+ // 2 multicast to both
75
+ expect(yield* next(subscriber1)).toEqual("bye");
76
+ expect(yield* next(subscriber2)).toEqual("bye");
77
+ });
78
+
79
+ it("subscriber after close receives last value and close value", function* () {
80
+ const subscriber1 = yield* downstream;
81
+
82
+ yield* upstream.send(1);
83
+ yield* upstream.close("bye");
84
+
85
+ // First subscriber gets value and close
86
+ expect(yield* next(subscriber1)).toEqual(1);
87
+ expect(yield* next(subscriber1)).toEqual("bye");
88
+
89
+ // Late subscriber after close should get last value and close value
90
+ const subscriber2 = yield* downstream;
91
+ expect(yield* next(subscriber2)).toEqual("bye");
92
+ });
93
+ });
package/subject.ts ADDED
@@ -0,0 +1,67 @@
1
+ import type { Stream, Subscription } from "effection";
2
+
3
+ /**
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).
7
+ *
8
+ * @returns A function that takes a stream and returns a multicast stream
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const subject = createSubject<number>();
13
+ * const downstream = subject(upstream);
14
+ *
15
+ * const sub1 = yield* downstream; // subscribes to upstream
16
+ * yield* upstream.send(1);
17
+ * yield* sub1.next(); // { done: false, value: 1 }
18
+ *
19
+ * 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
+ * ]);
36
+ * ```
37
+ */
38
+ export function createSubject<T>(): <TClose>(
39
+ stream: Stream<T, TClose>,
40
+ ) => Stream<T, TClose> {
41
+ let current: IteratorResult<T> | undefined = undefined;
42
+
43
+ return <TClose>(stream: Stream<T, TClose>) => ({
44
+ *[Symbol.iterator]() {
45
+ let upstream = yield* stream;
46
+
47
+ let iterator: Subscription<T, TClose> = current
48
+ ? {
49
+ *next() {
50
+ iterator = upstream;
51
+ // biome-ignore lint/style/noNonNullAssertion: current checked in ternary condition
52
+ return current!;
53
+ },
54
+ }
55
+ : {
56
+ *next() {
57
+ current = yield* upstream.next();
58
+ return current;
59
+ },
60
+ };
61
+
62
+ return {
63
+ next: () => iterator.next(),
64
+ };
65
+ },
66
+ });
67
+ }
@@ -0,0 +1,120 @@
1
+ import { describe, it } from "@effectionx/bdd";
2
+ import { createArraySignal, is } from "@effectionx/signals";
3
+ import { expect } from "expect";
4
+ import { each, race, sleep, spawn } from "effection";
5
+
6
+ import { useFaucet } from "./faucet.ts";
7
+
8
+ describe("useFaucet", () => {
9
+ it("creates a faucet that can pour items", function* () {
10
+ const faucet = yield* useFaucet<number>({ open: true });
11
+ const results = yield* createArraySignal<number>([]);
12
+
13
+ // Spawn a subscription to the faucet
14
+ yield* spawn(function* () {
15
+ for (const item of yield* each(faucet)) {
16
+ results.push(item);
17
+ yield* each.next();
18
+ }
19
+ });
20
+
21
+ yield* spawn(function* () {
22
+ yield* sleep(1);
23
+ // Pour an array of items
24
+ yield* faucet.pour([1, 2, 3]);
25
+ });
26
+
27
+ // Wait for processing
28
+ yield* is(results, (results) => results.length === 3);
29
+
30
+ expect(results.valueOf()).toEqual([1, 2, 3]);
31
+ });
32
+
33
+ it("respects the open state", function* () {
34
+ expect.assertions(2);
35
+ const faucet = yield* useFaucet<number>({ open: false });
36
+ const results = yield* createArraySignal<number>([]);
37
+
38
+ // Spawn a subscription to the faucet
39
+ yield* spawn(function* () {
40
+ for (const item of yield* each(faucet)) {
41
+ results.push(item);
42
+ yield* each.next();
43
+ }
44
+ });
45
+
46
+ // Try to pour while closed, give it a timeout to avoid it getting stuck
47
+ yield* race([faucet.pour([1, 2, 3]), sleep(1)]);
48
+
49
+ expect(results.valueOf()).toEqual([]);
50
+
51
+ faucet.open();
52
+
53
+ yield* faucet.pour([4, 5, 6]);
54
+
55
+ yield* is(results, (results) => results.length === 3);
56
+
57
+ expect(results.valueOf()).toEqual([4, 5, 6]);
58
+ });
59
+
60
+ it("supports pouring with an operation", function* () {
61
+ const faucet = yield* useFaucet<number>({ open: true });
62
+ const results = yield* createArraySignal<number>([]);
63
+
64
+ // Spawn a subscription to the faucet
65
+ yield* spawn(function* () {
66
+ for (const item of yield* each(faucet)) {
67
+ results.push(item);
68
+ yield* each.next();
69
+ }
70
+ });
71
+
72
+ // Pour using a generator function
73
+ yield* spawn(function* () {
74
+ yield* sleep(1);
75
+ yield* faucet.pour(function* (send) {
76
+ yield* send(1);
77
+ yield* sleep(10);
78
+ yield* send(2);
79
+ yield* sleep(10);
80
+ yield* send(3);
81
+ });
82
+ });
83
+
84
+ yield* is(results, (results) => results.length === 3);
85
+
86
+ expect(results.valueOf()).toEqual([1, 2, 3]);
87
+ });
88
+
89
+ it("stops pouring when closed", function* () {
90
+ const faucet = yield* useFaucet<number>({ open: true });
91
+ const results = yield* createArraySignal<number>([]);
92
+
93
+ // Spawn a subscription to the faucet
94
+ yield* spawn(function* () {
95
+ for (const item of yield* each(faucet)) {
96
+ results.push(item);
97
+ yield* each.next();
98
+ }
99
+ });
100
+
101
+ // Start pouring with a generator
102
+ yield* spawn(function* () {
103
+ yield* sleep(1);
104
+ yield* faucet.pour(function* (send) {
105
+ yield* send(1);
106
+ yield* sleep(10);
107
+ yield* send(2);
108
+ yield* sleep(10);
109
+ // Close the faucet
110
+ faucet.close();
111
+ // This should not be sent because we'll close the faucet
112
+ yield* send(3);
113
+ });
114
+ });
115
+
116
+ yield* is(results, (results) => results.length === 2);
117
+
118
+ expect(results.valueOf()).toEqual([1, 2]);
119
+ });
120
+ });
@@ -1,36 +1,42 @@
1
- import { type Operation, type Stream } from "effection";
1
+ import { createChannel, type Operation, type Stream } from "effection";
2
+ import { createBooleanSignal, is } from "@effectionx/signals";
3
+
2
4
  /**
3
5
  * Interface of the stream returned by `useFaucet`.
4
6
  */
5
7
  export interface Faucet<T> extends Stream<T, never> {
6
- /**
7
- * Pour items to the stream synchronously.
8
- * @param items - The items to pour to the stream.
9
- */
10
- pour(items: T[]): Operation<void>;
11
- /**
12
- * Pour items to the stream using an operation that can be asynchronous.
13
- * @param op - The generator function to pour items to the stream.
14
- */
15
- pour(op: (send: (item: T) => Operation<void>) => Operation<void>): Operation<void>;
16
- /**
17
- * Open the stream to allow items to be sent to the stream.
18
- */
19
- open(): void;
20
- /**
21
- * Close the stream to prevent items from being sent to the stream.
22
- */
23
- close(): void;
8
+ /**
9
+ * Pour items to the stream synchronously.
10
+ * @param items - The items to pour to the stream.
11
+ */
12
+ pour(items: T[]): Operation<void>;
13
+ /**
14
+ * Pour items to the stream using an operation that can be asynchronous.
15
+ * @param op - The generator function to pour items to the stream.
16
+ */
17
+ pour(
18
+ op: (send: (item: T) => Operation<void>) => Operation<void>,
19
+ ): Operation<void>;
20
+ /**
21
+ * Open the stream to allow items to be sent to the stream.
22
+ */
23
+ open(): void;
24
+ /**
25
+ * Close the stream to prevent items from being sent to the stream.
26
+ */
27
+ close(): void;
24
28
  }
29
+
25
30
  /**
26
31
  * Options for the faucet.
27
32
  */
28
33
  export interface FaucetOptions {
29
- /**
30
- * Whether the faucet is open when created.
31
- */
32
- open?: boolean;
34
+ /**
35
+ * Whether the faucet is open when created.
36
+ */
37
+ open?: boolean;
33
38
  }
39
+
34
40
  /**
35
41
  * Creates a stream that can be used to test the behavior of streams that use backpressure.
36
42
  * It's useful in tests where it can be used as a source stream. This function is used to create
@@ -74,5 +80,30 @@ export interface FaucetOptions {
74
80
  * @param options.open - Whether the faucet is open.
75
81
  * @returns stream of items coming from the faucet
76
82
  */
77
- export declare function useFaucet<T>(options: FaucetOptions): Operation<Faucet<T>>;
78
- //# sourceMappingURL=faucet.d.ts.map
83
+ export function* useFaucet<T>(options: FaucetOptions): Operation<Faucet<T>> {
84
+ let signal = createChannel<T, never>();
85
+ let open = yield* createBooleanSignal(options.open);
86
+
87
+ return {
88
+ [Symbol.iterator]: signal[Symbol.iterator],
89
+ *pour(items) {
90
+ if (Array.isArray(items)) {
91
+ for (let i of items) {
92
+ yield* is(open, (open) => open);
93
+ yield* signal.send(i);
94
+ }
95
+ } else {
96
+ yield* items(function* (item) {
97
+ yield* is(open, (open) => open);
98
+ yield* signal.send(item);
99
+ });
100
+ }
101
+ },
102
+ close() {
103
+ open.set(false);
104
+ },
105
+ open() {
106
+ open.set(true);
107
+ },
108
+ };
109
+ }
@@ -0,0 +1 @@
1
+ export * from "./test-helpers/faucet.ts";
@@ -0,0 +1,109 @@
1
+ import { describe, it } from "@effectionx/bdd";
2
+ import { expect } from "expect";
3
+ import { each, sleep, spawn } from "effection";
4
+ import { pipe } from "remeda";
5
+
6
+ import { batch } from "./batch.ts";
7
+ import { map } from "./map.ts";
8
+ import { useFaucet } from "./test-helpers/faucet.ts";
9
+ import { createTracker } from "./tracker.ts";
10
+ import { createArraySignal, is } from "@effectionx/signals";
11
+
12
+ describe("tracker", () => {
13
+ it("waits for all items to be processed", function* () {
14
+ const tracker = yield* createTracker();
15
+ const faucet = yield* useFaucet<number>({ open: true });
16
+ const received = yield* createArraySignal<number>([]);
17
+
18
+ const stream = pipe(
19
+ faucet,
20
+ tracker.passthrough(),
21
+ map(function* (x: number) {
22
+ yield* sleep(10);
23
+ return x;
24
+ }),
25
+ );
26
+
27
+ yield* spawn(function* () {
28
+ for (const item of yield* each(stream)) {
29
+ yield* sleep(10);
30
+ received.push(item);
31
+ tracker.markOne(item);
32
+ yield* each.next();
33
+ }
34
+ });
35
+
36
+ yield* sleep(1);
37
+
38
+ yield* faucet.pour(function* (send) {
39
+ yield* send(1);
40
+ yield* sleep(1);
41
+ yield* send(2);
42
+ yield* sleep(1);
43
+ yield* send(3);
44
+ });
45
+
46
+ yield* tracker;
47
+
48
+ yield* is(received, (received) => received.length === 3);
49
+
50
+ expect(received.valueOf()).toEqual([1, 2, 3]);
51
+ });
52
+
53
+ it("tracks batched items", function* () {
54
+ const tracker = yield* createTracker();
55
+ const faucet = yield* useFaucet<number>({ open: true });
56
+ const received = yield* createArraySignal<readonly number[]>([]);
57
+
58
+ const stream = pipe(
59
+ faucet,
60
+ tracker.passthrough(),
61
+ batch({
62
+ maxSize: 3,
63
+ }),
64
+ map(function* (items) {
65
+ yield* sleep(10);
66
+ return items;
67
+ }),
68
+ );
69
+
70
+ yield* spawn(function* () {
71
+ for (const items of yield* each(stream)) {
72
+ received.push(items);
73
+ tracker.markMany(items);
74
+ yield* each.next();
75
+ }
76
+ });
77
+
78
+ yield* sleep(1);
79
+
80
+ yield* faucet.pour(function* (send) {
81
+ yield* send(1);
82
+ yield* sleep(10);
83
+ yield* send(2);
84
+ yield* sleep(10);
85
+ yield* send(3);
86
+ yield* sleep(10);
87
+ yield* send(4);
88
+ yield* sleep(10);
89
+ yield* send(5);
90
+ yield* sleep(10);
91
+ yield* send(6);
92
+ yield* sleep(10);
93
+ yield* send(7);
94
+ yield* sleep(10);
95
+ yield* send(8);
96
+ yield* sleep(10);
97
+ yield* send(9);
98
+ });
99
+
100
+ yield* is(received, (received) => received.flat().length >= 9);
101
+ yield* tracker;
102
+
103
+ expect(received.valueOf()).toEqual([
104
+ [1, 2, 3],
105
+ [4, 5, 6],
106
+ [7, 8, 9],
107
+ ]);
108
+ });
109
+ });
package/tracker.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { type Operation, resource, type Stream } from "effection";
2
+ import { createSetSignal, is } from "@effectionx/signals";
3
+
4
+ export interface Tracker extends Operation<void> {
5
+ /**
6
+ * Returns a stream helper that doesn't modify the items passing through the stream,
7
+ * but will capture a reference to the item. Call the `markOne` or `markMany` methods
8
+ * with the item to indicate that it has exited the stream.
9
+ */
10
+ passthrough(): <T>(stream: Stream<T, never>) => Stream<T, never>;
11
+ /**
12
+ * Call this method with an item that has passed through the stream to indicate that it has exited the stream.
13
+ */
14
+ markOne(item: unknown): void;
15
+ /**
16
+ * Call this method with an iterable of items that have passed through the stream to indicate that they have exited the stream.
17
+ */
18
+ markMany(items: Iterable<unknown>): void;
19
+ }
20
+
21
+ /**
22
+ * Creates a tracker that can be used to verify that all items that entered the stream
23
+ * eventually exit the stream. This is helpful when you want to ensure that all items
24
+ * were processed before terminating the operation that created the stream.
25
+ */
26
+ export function createTracker(): Operation<Tracker> {
27
+ return resource(function* (provide) {
28
+ const tracked = yield* createSetSignal();
29
+
30
+ yield* provide({
31
+ *[Symbol.iterator]() {
32
+ yield* is(tracked, (set) => set.size === 0);
33
+ },
34
+ passthrough() {
35
+ return <T, TDone>(stream: Stream<T, TDone>): Stream<T, TDone> => ({
36
+ *[Symbol.iterator]() {
37
+ const subscription = yield* stream;
38
+
39
+ return {
40
+ *next() {
41
+ const next = yield* subscription.next();
42
+ tracked.add(next.value);
43
+ return next;
44
+ },
45
+ };
46
+ },
47
+ });
48
+ },
49
+ markOne(item) {
50
+ tracked.delete(item);
51
+ },
52
+ markMany(items) {
53
+ tracked.difference(items);
54
+ },
55
+ });
56
+ });
57
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "."
6
+ },
7
+ "include": ["**/*.ts"],
8
+ "exclude": ["**/*.test.ts", "dist"],
9
+ "references": [
10
+ {
11
+ "path": "../bdd"
12
+ },
13
+ {
14
+ "path": "../signals"
15
+ },
16
+ {
17
+ "path": "../timebox"
18
+ }
19
+ ]
20
+ }
package/valve.test.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { describe, it } from "@effectionx/bdd";
2
+ import { createArraySignal, is } from "@effectionx/signals";
3
+ import { expect } from "expect";
4
+ import { mock } from "node:test";
5
+ import { each, sleep, spawn } from "effection";
6
+
7
+ import { valve } from "./valve.ts";
8
+ import { useFaucet } from "./test-helpers/faucet.ts";
9
+
10
+ describe("valve", () => {
11
+ it("closes and opens the valve", function* () {
12
+ const faucet = yield* useFaucet<number>({ open: true });
13
+
14
+ const closeFn = function* () {
15
+ faucet.close();
16
+ };
17
+ const close = mock.fn(closeFn);
18
+
19
+ const openFn = function* () {
20
+ faucet.open();
21
+ };
22
+ const open = mock.fn(openFn);
23
+
24
+ const stream = valve({
25
+ closeAt: 5,
26
+ close,
27
+ open,
28
+ openAt: 2,
29
+ });
30
+
31
+ const values = yield* createArraySignal<number>([]);
32
+
33
+ yield* spawn(function* () {
34
+ for (const value of yield* each(stream(faucet))) {
35
+ values.push(value);
36
+ yield* sleep(1);
37
+ yield* each.next();
38
+ }
39
+ });
40
+
41
+ yield* sleep(1);
42
+
43
+ yield* faucet.pour([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
44
+
45
+ yield* is(values, (values) => values.length === 10);
46
+
47
+ expect(values.valueOf()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
48
+
49
+ expect(close.mock.callCount()).toBe(1);
50
+ expect(open.mock.callCount()).toBe(1);
51
+ });
52
+ });