@mearie/core 0.1.0 → 0.2.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.
@@ -0,0 +1,712 @@
1
+ //#region src/stream/pipe.ts
2
+ /**
3
+ * @param source - The source stream.
4
+ * @param operators - The operators to apply.
5
+ * @returns The result of the last operator.
6
+ */
7
+ function pipe(source, ...operators) {
8
+ return operators.reduce((src, operator) => operator(src), source);
9
+ }
10
+
11
+ //#endregion
12
+ //#region src/stream/operators/merge-map.ts
13
+ /**
14
+ * Maps each value to a source and flattens all sources into a single output source.
15
+ * Similar to flatMap. Values from all inner sources are merged concurrently.
16
+ * @param fn - Function that returns a source for each value.
17
+ * @returns An operator that flattens mapped sources.
18
+ */
19
+ const mergeMap = (fn) => {
20
+ return (source) => {
21
+ return (sink) => {
22
+ let outerCompleted = false;
23
+ let activeInner = 0;
24
+ let ended = false;
25
+ const innerSubscriptions = [];
26
+ const checkComplete = () => {
27
+ if (outerCompleted && activeInner === 0 && !ended) {
28
+ ended = true;
29
+ sink.complete();
30
+ }
31
+ };
32
+ const outerSubscription = source({
33
+ next(value) {
34
+ if (ended) return;
35
+ activeInner++;
36
+ const innerSubscription = fn(value)({
37
+ next(innerValue) {
38
+ if (!ended) sink.next(innerValue);
39
+ },
40
+ complete() {
41
+ activeInner--;
42
+ checkComplete();
43
+ }
44
+ });
45
+ innerSubscriptions.push(innerSubscription);
46
+ },
47
+ complete() {
48
+ outerCompleted = true;
49
+ checkComplete();
50
+ }
51
+ });
52
+ return { unsubscribe() {
53
+ ended = true;
54
+ outerSubscription.unsubscribe();
55
+ for (const sub of innerSubscriptions) sub.unsubscribe();
56
+ innerSubscriptions.length = 0;
57
+ } };
58
+ };
59
+ };
60
+ };
61
+
62
+ //#endregion
63
+ //#region src/stream/operators/filter.ts
64
+ function filter(predicate) {
65
+ return (source) => {
66
+ return (sink) => {
67
+ return source({
68
+ next(value) {
69
+ if (predicate(value)) sink.next(value);
70
+ },
71
+ complete() {
72
+ sink.complete();
73
+ }
74
+ });
75
+ };
76
+ };
77
+ }
78
+
79
+ //#endregion
80
+ //#region src/stream/sources/from-promise.ts
81
+ const fromPromise = (promise) => {
82
+ return (sink) => {
83
+ let cancelled = false;
84
+ promise.then((value) => {
85
+ if (!cancelled) {
86
+ sink.next(value);
87
+ sink.complete();
88
+ }
89
+ }, () => {
90
+ if (!cancelled) sink.complete();
91
+ });
92
+ return { unsubscribe() {
93
+ cancelled = true;
94
+ } };
95
+ };
96
+ };
97
+
98
+ //#endregion
99
+ //#region src/stream/operators/merge.ts
100
+ /**
101
+ * Merges multiple sources into a single source.
102
+ * Values are emitted as soon as they arrive from any source.
103
+ * Completes when all sources complete.
104
+ * @param sources - The sources to merge.
105
+ * @returns A merged source.
106
+ */
107
+ const merge = (...sources) => {
108
+ return (sink) => {
109
+ if (sources.length === 0) {
110
+ sink.complete();
111
+ return { unsubscribe() {} };
112
+ }
113
+ let activeCount = sources.length;
114
+ const subscriptions = [];
115
+ let ended = false;
116
+ let ready = false;
117
+ const buffer = [];
118
+ const checkComplete = () => {
119
+ if (activeCount === 0 && !ended) {
120
+ ended = true;
121
+ sink.complete();
122
+ }
123
+ };
124
+ for (const source of sources) {
125
+ const subscription = source({
126
+ next(value) {
127
+ if (!ended) if (ready) sink.next(value);
128
+ else buffer.push(value);
129
+ },
130
+ complete() {
131
+ activeCount--;
132
+ if (ready) checkComplete();
133
+ }
134
+ });
135
+ subscriptions.push(subscription);
136
+ }
137
+ ready = true;
138
+ for (const value of buffer) if (!ended) sink.next(value);
139
+ buffer.length = 0;
140
+ checkComplete();
141
+ return { unsubscribe() {
142
+ ended = true;
143
+ for (const sub of subscriptions) sub.unsubscribe();
144
+ } };
145
+ };
146
+ };
147
+
148
+ //#endregion
149
+ //#region src/stream/operators/tap.ts
150
+ /**
151
+ * Executes a side effect for each value without modifying the stream.
152
+ * Useful for debugging, logging, or triggering side effects.
153
+ * @param fn - The side effect function.
154
+ * @returns An operator that taps into the stream.
155
+ */
156
+ const tap = (fn) => {
157
+ return (source) => {
158
+ return (sink) => {
159
+ return source({
160
+ next(value) {
161
+ fn(value);
162
+ sink.next(value);
163
+ },
164
+ complete() {
165
+ sink.complete();
166
+ }
167
+ });
168
+ };
169
+ };
170
+ };
171
+
172
+ //#endregion
173
+ //#region src/stream/sources/from-array.ts
174
+ /**
175
+ * Creates a source that emits values from an array and completes.
176
+ * @param values - The array of values to emit.
177
+ * @returns A source containing the array values.
178
+ */
179
+ const fromArray = (values) => {
180
+ return (sink) => {
181
+ let cancelled = false;
182
+ for (const value of values) {
183
+ if (cancelled) break;
184
+ sink.next(value);
185
+ }
186
+ if (!cancelled) sink.complete();
187
+ return { unsubscribe() {
188
+ cancelled = true;
189
+ } };
190
+ };
191
+ };
192
+
193
+ //#endregion
194
+ //#region src/stream/compose.ts
195
+ /**
196
+ * @param operators - The operators to compose.
197
+ * @returns A composed operator.
198
+ */
199
+ function compose(...operators) {
200
+ return (source) => {
201
+ return operators.reduce((src, operator) => operator(src), source);
202
+ };
203
+ }
204
+
205
+ //#endregion
206
+ //#region src/stream/sinks/subscribe.ts
207
+ /**
208
+ * Subscribe to a Source with an Observer.
209
+ * This is a terminal operator that starts the source execution.
210
+ * @param observer - The observer to receive values.
211
+ * @returns A function that takes a source and returns a subscription.
212
+ */
213
+ const subscribe = (observer) => {
214
+ return (source) => {
215
+ let closed = false;
216
+ const subscription = source({
217
+ next(value) {
218
+ if (!closed) observer.next?.(value);
219
+ },
220
+ complete() {
221
+ if (!closed) {
222
+ closed = true;
223
+ observer.complete?.();
224
+ }
225
+ }
226
+ });
227
+ return () => {
228
+ closed = true;
229
+ subscription.unsubscribe();
230
+ };
231
+ };
232
+ };
233
+
234
+ //#endregion
235
+ //#region src/stream/sinks/publish.ts
236
+ const publish = (source) => {
237
+ source({
238
+ next() {},
239
+ complete() {}
240
+ });
241
+ };
242
+
243
+ //#endregion
244
+ //#region src/stream/sinks/collect.ts
245
+ /**
246
+ * Collects the last value emitted by a source.
247
+ * This is a terminal operator that returns the last emitted value.
248
+ * Rejects if the source completes without emitting any values.
249
+ * @param source - The source to collect from.
250
+ * @returns A promise that resolves with the last emitted value.
251
+ */
252
+ const collect = (source) => {
253
+ return new Promise((resolve, reject) => {
254
+ let lastValue;
255
+ let hasValue = false;
256
+ source({
257
+ next(value) {
258
+ lastValue = value;
259
+ hasValue = true;
260
+ },
261
+ complete() {
262
+ if (hasValue) resolve(lastValue);
263
+ else reject(/* @__PURE__ */ new Error("Source completed without emitting any values"));
264
+ }
265
+ });
266
+ });
267
+ };
268
+
269
+ //#endregion
270
+ //#region src/stream/sinks/collect-all.ts
271
+ /**
272
+ * Collects all values from a source into an array.
273
+ * This is a terminal operator that accumulates all emitted values.
274
+ * @param source - The source to collect values from.
275
+ * @returns A promise that resolves with an array of all emitted values.
276
+ */
277
+ const collectAll = (source) => {
278
+ return new Promise((resolve) => {
279
+ const results = [];
280
+ source({
281
+ next(value) {
282
+ results.push(value);
283
+ },
284
+ complete() {
285
+ resolve(results);
286
+ }
287
+ });
288
+ });
289
+ };
290
+
291
+ //#endregion
292
+ //#region src/stream/sinks/peek.ts
293
+ /**
294
+ * Synchronously reads the first value from a source and immediately unsubscribes.
295
+ * This is useful for reading the current state without maintaining a long-lived subscription.
296
+ * Throws if the source does not emit a value synchronously.
297
+ * @param source - The source to peek from.
298
+ * @returns The first value emitted by the source.
299
+ */
300
+ const peek = (source) => {
301
+ let value;
302
+ let hasValue = false;
303
+ source({
304
+ next(v) {
305
+ if (!hasValue) {
306
+ value = v;
307
+ hasValue = true;
308
+ }
309
+ },
310
+ complete() {}
311
+ }).unsubscribe();
312
+ if (hasValue) return value;
313
+ else throw new Error("Source did not emit a value synchronously");
314
+ };
315
+
316
+ //#endregion
317
+ //#region src/stream/operators/map.ts
318
+ /**
319
+ * Maps each value from the source through a transformation function.
320
+ * @param fn - The transformation function.
321
+ * @returns An operator that maps values.
322
+ */
323
+ const map = (fn) => {
324
+ return (source) => {
325
+ return (sink) => {
326
+ return source({
327
+ next(value) {
328
+ sink.next(fn(value));
329
+ },
330
+ complete() {
331
+ sink.complete();
332
+ }
333
+ });
334
+ };
335
+ };
336
+ };
337
+
338
+ //#endregion
339
+ //#region src/stream/operators/take.ts
340
+ /**
341
+ * Takes only the first N values from the source and completes.
342
+ * @param count - The number of values to take.
343
+ * @returns An operator that takes values.
344
+ */
345
+ const take = (count) => {
346
+ return (source) => {
347
+ return (sink) => {
348
+ let subscription = null;
349
+ const limit = Math.floor(count);
350
+ if (limit <= 0) {
351
+ sink.complete();
352
+ return { unsubscribe() {} };
353
+ }
354
+ let taken = 0;
355
+ let completed = false;
356
+ subscription = source({
357
+ next(value) {
358
+ if (!completed && taken < limit) {
359
+ sink.next(value);
360
+ taken++;
361
+ if (taken >= limit) {
362
+ completed = true;
363
+ sink.complete();
364
+ subscription?.unsubscribe();
365
+ }
366
+ }
367
+ },
368
+ complete() {
369
+ if (!completed) {
370
+ completed = true;
371
+ sink.complete();
372
+ }
373
+ }
374
+ });
375
+ if (completed) subscription?.unsubscribe();
376
+ return { unsubscribe() {
377
+ if (!completed) {
378
+ completed = true;
379
+ subscription.unsubscribe();
380
+ }
381
+ } };
382
+ };
383
+ };
384
+ };
385
+
386
+ //#endregion
387
+ //#region src/stream/operators/take-until.ts
388
+ /**
389
+ * Emits values from the source until the notifier source emits a value.
390
+ * When the notifier emits, the source is cancelled and completes immediately.
391
+ * @param notifier - Source that signals when to complete.
392
+ * @returns Operator that completes when notifier emits.
393
+ */
394
+ const takeUntil = (notifier) => {
395
+ return (source) => {
396
+ return (sink) => {
397
+ let sourceSubscription = null;
398
+ let notifierSubscription = null;
399
+ let completed = false;
400
+ const complete = () => {
401
+ if (completed) return;
402
+ completed = true;
403
+ if (sourceSubscription) sourceSubscription.unsubscribe();
404
+ if (notifierSubscription) notifierSubscription.unsubscribe();
405
+ sink.complete();
406
+ };
407
+ notifierSubscription = notifier({
408
+ next() {
409
+ complete();
410
+ },
411
+ complete() {}
412
+ });
413
+ sourceSubscription = source({
414
+ next(value) {
415
+ if (!completed) sink.next(value);
416
+ },
417
+ complete() {
418
+ complete();
419
+ }
420
+ });
421
+ return { unsubscribe() {
422
+ complete();
423
+ } };
424
+ };
425
+ };
426
+ };
427
+
428
+ //#endregion
429
+ //#region src/stream/operators/share.ts
430
+ /**
431
+ * Shares a single source across multiple subscribers (multicast).
432
+ * The source is only executed once, and all subscribers receive the same values.
433
+ * This is essential for deduplication and caching scenarios.
434
+ * @returns An operator that shares the source.
435
+ */
436
+ const share = () => {
437
+ return (source) => {
438
+ const sinks = [];
439
+ let subscription = null;
440
+ let started = false;
441
+ let completed = false;
442
+ return (sink) => {
443
+ if (completed) {
444
+ sink.complete();
445
+ return { unsubscribe() {} };
446
+ }
447
+ sinks.push(sink);
448
+ if (!started) {
449
+ started = true;
450
+ subscription = source({
451
+ next(value) {
452
+ for (const s of [...sinks]) {
453
+ if (completed) break;
454
+ s.next(value);
455
+ }
456
+ },
457
+ complete() {
458
+ if (!completed) {
459
+ completed = true;
460
+ for (const s of [...sinks]) s.complete();
461
+ sinks.length = 0;
462
+ }
463
+ }
464
+ });
465
+ }
466
+ return { unsubscribe() {
467
+ const idx = sinks.indexOf(sink);
468
+ if (idx !== -1) sinks.splice(idx, 1);
469
+ if (sinks.length === 0 && subscription) {
470
+ subscription.unsubscribe();
471
+ subscription = null;
472
+ started = false;
473
+ }
474
+ } };
475
+ };
476
+ };
477
+ };
478
+
479
+ //#endregion
480
+ //#region src/stream/operators/switch-map.ts
481
+ const switchMap = (fn) => {
482
+ return (source) => {
483
+ return (sink) => {
484
+ let outerCompleted = false;
485
+ let ended = false;
486
+ let innerSubscription = null;
487
+ let hasInner = false;
488
+ const checkComplete = () => {
489
+ if (outerCompleted && !hasInner && !ended) {
490
+ ended = true;
491
+ sink.complete();
492
+ }
493
+ };
494
+ const outerSubscription = source({
495
+ next(value) {
496
+ if (ended) return;
497
+ if (innerSubscription) {
498
+ innerSubscription.unsubscribe();
499
+ innerSubscription = null;
500
+ }
501
+ hasInner = true;
502
+ innerSubscription = fn(value)({
503
+ next(innerValue) {
504
+ if (!ended) sink.next(innerValue);
505
+ },
506
+ complete() {
507
+ hasInner = false;
508
+ innerSubscription = null;
509
+ checkComplete();
510
+ }
511
+ });
512
+ },
513
+ complete() {
514
+ outerCompleted = true;
515
+ checkComplete();
516
+ }
517
+ });
518
+ return { unsubscribe() {
519
+ ended = true;
520
+ outerSubscription.unsubscribe();
521
+ if (innerSubscription) {
522
+ innerSubscription.unsubscribe();
523
+ innerSubscription = null;
524
+ }
525
+ } };
526
+ };
527
+ };
528
+ };
529
+
530
+ //#endregion
531
+ //#region src/stream/operators/initialize.ts
532
+ /**
533
+ * Executes a side effect when the source is initialized (being subscribed to).
534
+ * @param fn - The side effect function.
535
+ * @returns An operator that executes the side effect when the source is initialized.
536
+ */
537
+ const initialize = (fn) => {
538
+ return (source) => {
539
+ return (sink) => {
540
+ let completed = false;
541
+ const subscription = source({
542
+ next(value) {
543
+ if (!completed) sink.next(value);
544
+ },
545
+ complete() {
546
+ if (!completed) {
547
+ completed = true;
548
+ sink.complete();
549
+ }
550
+ }
551
+ });
552
+ fn();
553
+ return { unsubscribe() {
554
+ completed = true;
555
+ subscription.unsubscribe();
556
+ } };
557
+ };
558
+ };
559
+ };
560
+
561
+ //#endregion
562
+ //#region src/stream/operators/finalize.ts
563
+ /**
564
+ * Executes a side effect when the source terminates (completes or unsubscribes).
565
+ * @param fn - The side effect function.
566
+ * @returns An operator that executes the side effect when the source terminates.
567
+ */
568
+ const finalize = (fn) => {
569
+ return (source) => {
570
+ return (sink) => {
571
+ let completed = false;
572
+ const subscription = source({
573
+ next(value) {
574
+ if (!completed) sink.next(value);
575
+ },
576
+ complete() {
577
+ if (!completed) {
578
+ completed = true;
579
+ fn();
580
+ sink.complete();
581
+ }
582
+ }
583
+ });
584
+ return { unsubscribe() {
585
+ if (!completed) {
586
+ completed = true;
587
+ fn();
588
+ }
589
+ subscription.unsubscribe();
590
+ } };
591
+ };
592
+ };
593
+ };
594
+
595
+ //#endregion
596
+ //#region src/stream/sources/from-value.ts
597
+ /**
598
+ * Creates a source that emits a single value and completes.
599
+ * @param value - The value to emit.
600
+ * @returns A source containing the single value.
601
+ */
602
+ const fromValue = (value) => {
603
+ return (sink) => {
604
+ let cancelled = false;
605
+ if (!cancelled) {
606
+ sink.next(value);
607
+ sink.complete();
608
+ }
609
+ return { unsubscribe() {
610
+ cancelled = true;
611
+ } };
612
+ };
613
+ };
614
+
615
+ //#endregion
616
+ //#region src/stream/sources/make-subject.ts
617
+ /**
618
+ * Creates a new Subject which can be used as an IO event hub.
619
+ * @returns A new Subject.
620
+ */
621
+ const makeSubject = () => {
622
+ const sinks = [];
623
+ const source = (sink) => {
624
+ sinks.push(sink);
625
+ return { unsubscribe() {
626
+ const idx = sinks.indexOf(sink);
627
+ if (idx !== -1) sinks.splice(idx, 1);
628
+ } };
629
+ };
630
+ const next = (value) => {
631
+ for (const sink of [...sinks]) sink.next(value);
632
+ };
633
+ const complete = () => {
634
+ for (const sink of [...sinks]) sink.complete();
635
+ sinks.length = 0;
636
+ };
637
+ return {
638
+ source,
639
+ next,
640
+ complete
641
+ };
642
+ };
643
+
644
+ //#endregion
645
+ //#region src/stream/sources/from-subscription.ts
646
+ const fromSubscription = (pull, poke) => {
647
+ return (sink) => {
648
+ let teardown = null;
649
+ let cancelled = false;
650
+ const initialValue = pull();
651
+ sink.next(initialValue);
652
+ if (cancelled) return { unsubscribe() {
653
+ cancelled = true;
654
+ } };
655
+ teardown = poke(() => {
656
+ if (!cancelled) {
657
+ const value = pull();
658
+ sink.next(value);
659
+ }
660
+ });
661
+ return { unsubscribe() {
662
+ cancelled = true;
663
+ if (teardown) {
664
+ teardown();
665
+ teardown = null;
666
+ }
667
+ } };
668
+ };
669
+ };
670
+
671
+ //#endregion
672
+ //#region src/stream/sources/make.ts
673
+ /**
674
+ * Creates a new Source from scratch from a passed subscriber function.
675
+ *
676
+ * The subscriber function receives an observer with next and complete callbacks.
677
+ * It must return a teardown function which is called when the source is cancelled.
678
+ * @internal
679
+ * @param subscriber - A callback that is called when the Source is subscribed to.
680
+ * @returns A Source created from the subscriber parameter.
681
+ */
682
+ const make = (subscriber) => {
683
+ return (sink) => {
684
+ let cancelled = false;
685
+ let teardown = null;
686
+ teardown = subscriber({
687
+ next: (value) => {
688
+ if (!cancelled) sink.next(value);
689
+ },
690
+ complete: () => {
691
+ if (!cancelled) {
692
+ cancelled = true;
693
+ if (teardown) {
694
+ teardown();
695
+ teardown = null;
696
+ }
697
+ sink.complete();
698
+ }
699
+ }
700
+ });
701
+ return { unsubscribe() {
702
+ cancelled = true;
703
+ if (teardown) {
704
+ teardown();
705
+ teardown = null;
706
+ }
707
+ } };
708
+ };
709
+ };
710
+
711
+ //#endregion
712
+ export { mergeMap as C, filter as S, compose as _, finalize as a, merge as b, share as c, map as d, peek as f, subscribe as g, publish as h, fromValue as i, takeUntil as l, collect as m, fromSubscription as n, initialize as o, collectAll as p, makeSubject as r, switchMap as s, make as t, take as u, fromArray as v, pipe as w, fromPromise as x, tap as y };