@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/README.md CHANGED
@@ -192,6 +192,53 @@ function* exampleWithCloseValue() {
192
192
  }
193
193
  ```
194
194
 
195
+ ### Subject
196
+
197
+ Subject helper converts any stream into a multicast stream that replays the
198
+ latest value to new subscribers. It's analogous to
199
+ [RxJS BehaviorSubject](https://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject).
200
+
201
+ ```typescript
202
+ import { createSubject } from "@effectionx/stream-helpers";
203
+ import { createChannel, spawn } from "effection";
204
+
205
+ function* example() {
206
+ const subject = createSubject<number>();
207
+ const channel = createChannel<number, void>();
208
+ const downstream = subject(channel);
209
+
210
+ // First subscriber
211
+ const sub1 = yield* downstream;
212
+
213
+ yield* channel.send(1);
214
+ yield* channel.send(2);
215
+
216
+ console.log(yield* sub1.next()); // { done: false, value: 1 }
217
+ console.log(yield* sub1.next()); // { done: false, value: 2 }
218
+
219
+ // Late subscriber gets the latest value immediately
220
+ const sub2 = yield* downstream;
221
+ console.log(yield* sub2.next()); // { done: false, value: 2 }
222
+ }
223
+ ```
224
+
225
+ Use it with a pipe operator to convert any stream into a behavior subject:
226
+
227
+ ```typescript
228
+ import { createSubject, map } from "@effectionx/stream-helpers";
229
+ import { pipe } from "remeda";
230
+
231
+ const subject = createSubject<string>();
232
+
233
+ const stream = pipe(
234
+ source,
235
+ map(function* (x) {
236
+ return x.toString();
237
+ }),
238
+ subject,
239
+ );
240
+ ```
241
+
195
242
  ### Passthrough Tracker
196
243
 
197
244
  Passthrough Tracker stream helper provides a way to know if all items that
package/batch.test.ts ADDED
@@ -0,0 +1,112 @@
1
+ import { describe, it } from "@effectionx/bdd";
2
+ import { createArraySignal, is } from "@effectionx/signals";
3
+ import { expect } from "expect";
4
+ import { createChannel, sleep, spawn } from "effection";
5
+
6
+ import { batch } from "./batch.ts";
7
+ import { forEach } from "./for-each.ts";
8
+ import { useFaucet } from "./test-helpers/faucet.ts";
9
+
10
+ describe("batch", () => {
11
+ it("creates a batch when maxTime expires", function* () {
12
+ const source = createChannel<number, never>();
13
+ const stream = batch({ maxTime: 50 })(source);
14
+
15
+ const subscription = yield* stream;
16
+
17
+ let next = yield* spawn(() => subscription.next());
18
+
19
+ yield* source.send(1);
20
+ yield* source.send(2);
21
+ yield* source.send(3);
22
+
23
+ expect((yield* next).value).toEqual([1, 2, 3]);
24
+ });
25
+
26
+ it("creates a batch by maxSize when maxTime is not set", function* () {
27
+ const faucet = yield* useFaucet<number>({ open: true });
28
+ const stream = batch({ maxSize: 3 })(faucet);
29
+
30
+ const subscription = yield* stream;
31
+
32
+ yield* faucet.pour([1, 2, 3, 4, 5, 6]);
33
+
34
+ let next = yield* subscription.next();
35
+ expect(next.value).toEqual([1, 2, 3]);
36
+
37
+ next = yield* subscription.next();
38
+ expect(next.value).toEqual([4, 5, 6]);
39
+ });
40
+
41
+ it("creates a batch within maxTime when maxSize is never reached", function* () {
42
+ const faucet = yield* useFaucet<number>({ open: true });
43
+ const stream = batch({ maxSize: 8, maxTime: 50 })(faucet);
44
+
45
+ const batches = yield* createArraySignal<readonly number[]>([]);
46
+ const windows: number[] = [];
47
+
48
+ let last = performance.now();
49
+
50
+ yield* spawn(() =>
51
+ forEach<readonly number[], void>(function* (batch) {
52
+ const now = performance.now();
53
+ windows.push(now - last);
54
+ last = now;
55
+
56
+ batches.push(batch);
57
+ }, stream),
58
+ );
59
+
60
+ yield* sleep(1);
61
+
62
+ yield* faucet.pour(function* (send) {
63
+ for (let i = 1; i <= 10; i++) {
64
+ yield* send(i);
65
+ yield* sleep(20);
66
+ }
67
+ });
68
+
69
+ yield* is(batches, (list) => list.flat().length >= 10);
70
+
71
+ expect(windows.length).toBeGreaterThanOrEqual(3);
72
+
73
+ const avg = average(windows);
74
+ const percentDiff = Math.abs((avg - 50) / 50) * 100;
75
+ expect(percentDiff).toBeLessThanOrEqual(30);
76
+
77
+ expect(batches.valueOf().flat()).toHaveLength(10);
78
+ });
79
+
80
+ it("creates a batch within maxSize in maxTime window", function* () {
81
+ const faucet = yield* useFaucet<number>({ open: true });
82
+ const stream = batch({ maxSize: 5, maxTime: 3 })(faucet);
83
+
84
+ const batches = yield* createArraySignal<readonly number[]>([]);
85
+
86
+ yield* spawn(() =>
87
+ forEach<readonly number[], void>(function* (batch) {
88
+ batches.push(batch);
89
+ }, stream),
90
+ );
91
+
92
+ yield* sleep(1);
93
+
94
+ yield* faucet.pour([1, 2, 3, 4, 5, 6]);
95
+
96
+ yield* is(batches, (batches) => batches.flat().length >= 6);
97
+
98
+ expect(batches.length).toBeGreaterThan(1);
99
+ expect(batches.valueOf().every((batch) => batch.length <= 5)).toBe(true);
100
+ });
101
+ });
102
+
103
+ function average(arr: number[]) {
104
+ if (arr.length === 0) {
105
+ return 0;
106
+ }
107
+ const sum = arr.reduce(
108
+ (accumulator, currentValue) => accumulator + currentValue,
109
+ 0,
110
+ );
111
+ return sum / arr.length;
112
+ }
package/batch.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { timebox } from "@effectionx/timebox";
2
+ import { type Stream, type Task, spawn } from "effection";
3
+
4
+ type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<
5
+ T,
6
+ Exclude<keyof T, Keys>
7
+ > &
8
+ {
9
+ [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
10
+ }[Keys];
11
+
12
+ export interface BatchOptions {
13
+ readonly maxTime: number;
14
+ readonly maxSize: number;
15
+ }
16
+
17
+ /**
18
+ * Creates batches of items from the source stream. The batches can be created either by
19
+ * specifying a maximum time or a maximum size. If both are specified, the batch will be
20
+ * created when either condition is met.
21
+ *
22
+ * @param options - The options for the batch.
23
+ * @param options.maxTime - The maximum time to wait for a batch.
24
+ * @param options.maxSize - The maximum size of a batch.
25
+ * @returns A stream of arrays of items from the source stream.
26
+ */
27
+ export function batch(
28
+ options: RequireAtLeastOne<BatchOptions>,
29
+ ): <T>(stream: Stream<T, never>) => Stream<Readonly<T[]>, never> {
30
+ return <T>(stream: Stream<T, never>): Stream<Readonly<T[]>, never> => ({
31
+ *[Symbol.iterator]() {
32
+ const subscription = yield* stream;
33
+ let lastPull: Task<IteratorResult<T, never>> | undefined;
34
+
35
+ return {
36
+ *next() {
37
+ let start: DOMHighResTimeStamp = performance.now();
38
+ const batch: T[] = [];
39
+ let next: IteratorResult<T, never> = {
40
+ done: true as const,
41
+ value: undefined as never,
42
+ };
43
+ if (lastPull && options.maxTime) {
44
+ // biome-ignore lint/style/noNonNullAssertion: lastPull checked above
45
+ const timeout = yield* timebox(options.maxTime, () => lastPull!);
46
+ if (timeout.timeout) {
47
+ yield* lastPull.halt();
48
+ lastPull = undefined;
49
+ } else {
50
+ next = timeout.value;
51
+ lastPull = undefined;
52
+ }
53
+ } else {
54
+ next = yield* subscription.next();
55
+ }
56
+ // push the next value into the batch
57
+ while (!next.done) {
58
+ batch.push(next.value);
59
+ const now = performance.now();
60
+ if (options.maxSize && batch.length >= options.maxSize) {
61
+ return {
62
+ done: false as const,
63
+ value: batch,
64
+ };
65
+ }
66
+ if (options.maxTime && start + options.maxTime <= now) {
67
+ return {
68
+ done: false as const,
69
+ value: batch,
70
+ };
71
+ }
72
+ if (options.maxTime) {
73
+ const task = yield* spawn(() => subscription.next());
74
+
75
+ const timeout = yield* timebox(
76
+ start + options.maxTime - performance.now(),
77
+ () => task,
78
+ );
79
+
80
+ if (timeout.timeout) {
81
+ // produce the batch that we have, save task for next batch
82
+ lastPull = task;
83
+ return {
84
+ done: false as const,
85
+ value: batch,
86
+ };
87
+ }
88
+ next = timeout.value;
89
+ } else {
90
+ next = yield* subscription.next();
91
+ }
92
+ }
93
+
94
+ // Stream is done, return any remaining batch
95
+ if (batch.length > 0) {
96
+ return {
97
+ done: false as const,
98
+ value: batch,
99
+ };
100
+ }
101
+
102
+ return next;
103
+ },
104
+ };
105
+ },
106
+ });
107
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../batch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAoB,MAAM,WAAW,CAAC;AAE1D,KAAK,iBAAiB,CAAC,CAAC,EAAE,IAAI,SAAS,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,CAC9D,CAAC,EACD,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CACvB,GACC;KACG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;CACzE,CAAC,IAAI,CAAC,CAAC;AAEV,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CACnB,OAAO,EAAE,iBAAiB,CAAC,YAAY,CAAC,GACvC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CA8E/D"}
package/dist/batch.js ADDED
@@ -0,0 +1,86 @@
1
+ import { timebox } from "@effectionx/timebox";
2
+ import { spawn } from "effection";
3
+ /**
4
+ * Creates batches of items from the source stream. The batches can be created either by
5
+ * specifying a maximum time or a maximum size. If both are specified, the batch will be
6
+ * created when either condition is met.
7
+ *
8
+ * @param options - The options for the batch.
9
+ * @param options.maxTime - The maximum time to wait for a batch.
10
+ * @param options.maxSize - The maximum size of a batch.
11
+ * @returns A stream of arrays of items from the source stream.
12
+ */
13
+ export function batch(options) {
14
+ return (stream) => ({
15
+ *[Symbol.iterator]() {
16
+ const subscription = yield* stream;
17
+ let lastPull;
18
+ return {
19
+ *next() {
20
+ let start = performance.now();
21
+ const batch = [];
22
+ let next = {
23
+ done: true,
24
+ value: undefined,
25
+ };
26
+ if (lastPull && options.maxTime) {
27
+ // biome-ignore lint/style/noNonNullAssertion: lastPull checked above
28
+ const timeout = yield* timebox(options.maxTime, () => lastPull);
29
+ if (timeout.timeout) {
30
+ yield* lastPull.halt();
31
+ lastPull = undefined;
32
+ }
33
+ else {
34
+ next = timeout.value;
35
+ lastPull = undefined;
36
+ }
37
+ }
38
+ else {
39
+ next = yield* subscription.next();
40
+ }
41
+ // push the next value into the batch
42
+ while (!next.done) {
43
+ batch.push(next.value);
44
+ const now = performance.now();
45
+ if (options.maxSize && batch.length >= options.maxSize) {
46
+ return {
47
+ done: false,
48
+ value: batch,
49
+ };
50
+ }
51
+ if (options.maxTime && start + options.maxTime <= now) {
52
+ return {
53
+ done: false,
54
+ value: batch,
55
+ };
56
+ }
57
+ if (options.maxTime) {
58
+ const task = yield* spawn(() => subscription.next());
59
+ const timeout = yield* timebox(start + options.maxTime - performance.now(), () => task);
60
+ if (timeout.timeout) {
61
+ // produce the batch that we have, save task for next batch
62
+ lastPull = task;
63
+ return {
64
+ done: false,
65
+ value: batch,
66
+ };
67
+ }
68
+ next = timeout.value;
69
+ }
70
+ else {
71
+ next = yield* subscription.next();
72
+ }
73
+ }
74
+ // Stream is done, return any remaining batch
75
+ if (batch.length > 0) {
76
+ return {
77
+ done: false,
78
+ value: batch,
79
+ };
80
+ }
81
+ return next;
82
+ },
83
+ };
84
+ },
85
+ });
86
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,MAAM,CAAC,CAAC,EACtB,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,SAAS,CAAC,OAAO,CAAC,GAC1C,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,CAuBvD"}
package/dist/filter.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Filters items from the stream based on a predicate function.
3
+ *
4
+ * @param predicate - The function to test each item
5
+ * @returns A stream transformer that only emits items that pass the predicate
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { filter } from "@effectionx/stream-helpers";
10
+ * import { run, each } from "effection";
11
+ *
12
+ * await run(function* () {
13
+ * const stream = filter((x: number) => x > 5)(sourceStream);
14
+ *
15
+ * for (const value of yield* each(stream)) {
16
+ * console.log(value); // Only values > 5
17
+ * }
18
+ * });
19
+ * ```
20
+ */
21
+ export function filter(predicate) {
22
+ return (stream) => ({
23
+ *[Symbol.iterator]() {
24
+ const subscription = yield* stream;
25
+ return {
26
+ *next() {
27
+ while (true) {
28
+ const next = yield* subscription.next();
29
+ if (next.done) {
30
+ return next;
31
+ }
32
+ if (yield* predicate(next.value)) {
33
+ return {
34
+ done: false,
35
+ value: next.value,
36
+ };
37
+ }
38
+ }
39
+ },
40
+ };
41
+ },
42
+ });
43
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"for-each.d.ts","sourceRoot":"","sources":["../for-each.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAiB,OAAO,CAAC,CAAC,EAAE,MAAM,EAChC,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,CAAC,IAAI,CAAC,EAChC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,GACxB,SAAS,CAAC,MAAM,CAAC,CAQnB"}
@@ -0,0 +1,29 @@
1
+ import type { Stream } from "effection";
2
+ /**
3
+ * Represents the close value of a lines stream, containing any
4
+ * remaining content that didn't end with a newline.
5
+ */
6
+ export interface Remainder<T> {
7
+ remainder: string;
8
+ result: T;
9
+ }
10
+ /**
11
+ * Stream helper that transforms a stream of binary chunks into a stream of lines.
12
+ *
13
+ * Lines are split on newline characters (`\n`). The final line (content after
14
+ * the last newline) is returned as the `remainder` in the close value.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { lines } from "@effectionx/stream-helpers";
19
+ * import { pipe, each } from "effection";
20
+ *
21
+ * const lineStream = pipe(process.stdout, lines());
22
+ * for (const line of yield* each(lineStream)) {
23
+ * console.log(line);
24
+ * yield* each.next();
25
+ * }
26
+ * ```
27
+ */
28
+ export declare function lines(): <T extends Uint8Array, TReturn>(stream: Stream<T, TReturn>) => Stream<string, Remainder<TReturn>>;
29
+ //# sourceMappingURL=lines.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lines.d.ts","sourceRoot":"","sources":["../lines.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC;;;GAGG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,CAAC,CAAC;CACX;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,KAAK,IAAI,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,EACrD,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,KACvB,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC,CA0CtC"}
package/dist/lines.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Stream helper that transforms a stream of binary chunks into a stream of lines.
3
+ *
4
+ * Lines are split on newline characters (`\n`). The final line (content after
5
+ * the last newline) is returned as the `remainder` in the close value.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { lines } from "@effectionx/stream-helpers";
10
+ * import { pipe, each } from "effection";
11
+ *
12
+ * const lineStream = pipe(process.stdout, lines());
13
+ * for (const line of yield* each(lineStream)) {
14
+ * console.log(line);
15
+ * yield* each.next();
16
+ * }
17
+ * ```
18
+ */
19
+ export function lines() {
20
+ const decoder = new TextDecoder();
21
+ return (stream) => ({
22
+ *[Symbol.iterator]() {
23
+ const subscription = yield* stream;
24
+ const buffer = [];
25
+ let remainder = "";
26
+ return {
27
+ *next() {
28
+ while (buffer.length === 0) {
29
+ const next = yield* subscription.next();
30
+ if (next.done) {
31
+ return {
32
+ done: true,
33
+ value: {
34
+ remainder,
35
+ result: next.value,
36
+ },
37
+ };
38
+ }
39
+ const current = remainder + decoder.decode(next.value);
40
+ const lines = current.split("\n");
41
+ if (lines.length > 0) {
42
+ buffer.push(...lines.slice(0, -1));
43
+ remainder = lines.at(-1) ?? "";
44
+ }
45
+ else {
46
+ remainder = current;
47
+ }
48
+ }
49
+ const value = buffer.shift();
50
+ if (value === undefined) {
51
+ throw new Error("Unexpected empty buffer");
52
+ }
53
+ return {
54
+ done: false,
55
+ value,
56
+ };
57
+ },
58
+ };
59
+ },
60
+ });
61
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../map.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnD;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,CAAC,EAAE,CAAC,EACtB,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,GAC7B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,CAsB1D"}
package/dist/map.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Transforms each item in the stream using the provided function.
3
+ *
4
+ * @param fn - The function to transform each item
5
+ * @returns A stream transformer that applies the function to each item
6
+ */
7
+ export function map(fn) {
8
+ return (stream) => ({
9
+ *[Symbol.iterator]() {
10
+ const subscription = yield* stream;
11
+ return {
12
+ *next() {
13
+ const next = yield* subscription.next();
14
+ if (next.done) {
15
+ return next;
16
+ }
17
+ const value = yield* fn(next.value);
18
+ return {
19
+ done: false,
20
+ value,
21
+ };
22
+ },
23
+ };
24
+ },
25
+ });
26
+ }
package/dist/mod.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from "./batch.ts";
2
+ export * from "./valve.ts";
3
+ export * from "./map.ts";
4
+ export * from "./filter.ts";
5
+ export * from "./tracker.ts";
6
+ export * from "./for-each.ts";
7
+ export * from "./subject.ts";
8
+ export * from "./lines.ts";
9
+ //# sourceMappingURL=mod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../mod.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
@@ -4,3 +4,5 @@ export * from "./map.js";
4
4
  export * from "./filter.js";
5
5
  export * from "./tracker.js";
6
6
  export * from "./for-each.js";
7
+ export * from "./subject.js";
8
+ export * from "./lines.js";
@@ -0,0 +1,38 @@
1
+ import type { Stream } from "effection";
2
+ /**
3
+ * Converts any stream into a multicast stream that produces latest value
4
+ * to new subscribers. It's designed to be analagous in function to [RxJS
5
+ * BehaviorSubject](https://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject).
6
+ *
7
+ * @returns A function that takes a stream and returns a multicast stream
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const subject = createSubject<number>();
12
+ * const downstream = subject(upstream);
13
+ *
14
+ * const sub1 = yield* downstream; // subscribes to upstream
15
+ * yield* upstream.send(1);
16
+ * yield* sub1.next(); // { done: false, value: 1 }
17
+ *
18
+ * const sub2 = yield* downstream; // late subscriber
19
+ * yield* sub2.next(); // { done: false, value: 1 } - gets latest value
20
+ * ```
21
+ *
22
+ * Use it with a pipe operator to convert any stream into a behavior subject.
23
+ *
24
+ * @example
25
+ * ```
26
+ * let source = createChannel<string, void>();
27
+ * let subject = createSubject<string>();
28
+ *
29
+ * let pipeline = pipe([
30
+ * top,
31
+ * transform1,
32
+ * transform2,
33
+ * subject,
34
+ * ]);
35
+ * ```
36
+ */
37
+ export declare function createSubject<T>(): <TClose>(stream: Stream<T, TClose>) => Stream<T, TClose>;
38
+ //# sourceMappingURL=subject.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subject.d.ts","sourceRoot":"","sources":["../subject.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAgB,MAAM,WAAW,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,aAAa,CAAC,CAAC,KAAK,CAAC,MAAM,EACzC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,KACtB,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,CA2BrB"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Converts any stream into a multicast stream that produces latest value
3
+ * to new subscribers. It's designed to be analagous in function to [RxJS
4
+ * BehaviorSubject](https://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject).
5
+ *
6
+ * @returns A function that takes a stream and returns a multicast stream
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const subject = createSubject<number>();
11
+ * const downstream = subject(upstream);
12
+ *
13
+ * const sub1 = yield* downstream; // subscribes to upstream
14
+ * yield* upstream.send(1);
15
+ * yield* sub1.next(); // { done: false, value: 1 }
16
+ *
17
+ * const sub2 = yield* downstream; // late subscriber
18
+ * yield* sub2.next(); // { done: false, value: 1 } - gets latest value
19
+ * ```
20
+ *
21
+ * Use it with a pipe operator to convert any stream into a behavior subject.
22
+ *
23
+ * @example
24
+ * ```
25
+ * let source = createChannel<string, void>();
26
+ * let subject = createSubject<string>();
27
+ *
28
+ * let pipeline = pipe([
29
+ * top,
30
+ * transform1,
31
+ * transform2,
32
+ * subject,
33
+ * ]);
34
+ * ```
35
+ */
36
+ export function createSubject() {
37
+ let current = undefined;
38
+ return (stream) => ({
39
+ *[Symbol.iterator]() {
40
+ let upstream = yield* stream;
41
+ let iterator = current
42
+ ? {
43
+ *next() {
44
+ iterator = upstream;
45
+ // biome-ignore lint/style/noNonNullAssertion: current checked in ternary condition
46
+ return current;
47
+ },
48
+ }
49
+ : {
50
+ *next() {
51
+ current = yield* upstream.next();
52
+ return current;
53
+ },
54
+ };
55
+ return {
56
+ next: () => iterator.next(),
57
+ };
58
+ },
59
+ });
60
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"faucet.d.ts","sourceRoot":"","sources":["../../test-helpers/faucet.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAGvE;;GAEG;AACH,MAAM,WAAW,MAAM,CAAC,CAAC,CAAE,SAAQ,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC;IACjD;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAClC;;;OAGG;IACH,IAAI,CACF,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC,IAAI,CAAC,GAC1D,SAAS,CAAC,IAAI,CAAC,CAAC;IACnB;;OAEG;IACH,IAAI,IAAI,IAAI,CAAC;IACb;;OAEG;IACH,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAiB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CA0B1E"}
@@ -0,0 +1,2 @@
1
+ export * from "./test-helpers/faucet.ts";
2
+ //# sourceMappingURL=test-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../test-helpers.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC"}