lulz 1.0.2 → 2.0.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.
package/src/rx-lib.js ADDED
@@ -0,0 +1,679 @@
1
+ /**
2
+ * lulz - RxJS-Inspired Operators Library
3
+ *
4
+ * Reactive operators for stream processing.
5
+ * All operators follow the outer/inner function pattern.
6
+ */
7
+
8
+
9
+ // ─────────────────────────────────────────────────────────────
10
+ // Combination Operators
11
+ // ─────────────────────────────────────────────────────────────
12
+
13
+ /**
14
+ * combineLatest - Combines latest values from multiple pipes
15
+ *
16
+ * Creates a node that collects the latest value from each specified pipe
17
+ * and emits an array/object whenever any pipe updates.
18
+ *
19
+ * @param {Object} options
20
+ * @param {string[]} options.pipes - Array of pipe names to combine
21
+ * @param {string} options.output - Output format: 'array' or 'object' (default: 'object')
22
+ * @param {Object} options.app - The flow app instance (required)
23
+ *
24
+ * Usage:
25
+ * const app = flow([
26
+ * ['temp', combineLatest({ app: () => app, pipes: ['temp', 'humidity'] }), 'combined'],
27
+ * ]);
28
+ */
29
+ export function combineLatest(options = {}) {
30
+ const {
31
+ pipes = [],
32
+ output = 'object',
33
+ app: getApp = null
34
+ } = options;
35
+
36
+ if (pipes.length === 0) {
37
+ console.warn('[combineLatest] No pipes configured');
38
+ }
39
+
40
+ const latest = {};
41
+ const received = new Set();
42
+
43
+ return (send, packet) => {
44
+ // This node should be connected to trigger on any pipe update
45
+ // We track the pipe name from packet.topic or a special field
46
+ const pipeName = packet._pipe || packet.topic;
47
+
48
+ if (pipeName && pipes.includes(pipeName)) {
49
+ latest[pipeName] = packet.payload !== undefined ? packet.payload : packet;
50
+ received.add(pipeName);
51
+ }
52
+
53
+ // Only emit when all pipes have sent at least once
54
+ if (received.size === pipes.length) {
55
+ if (output === 'array') {
56
+ send({ payload: pipes.map(p => latest[p]) });
57
+ } else {
58
+ send({ payload: { ...latest } });
59
+ }
60
+ }
61
+ };
62
+ }
63
+
64
+ /**
65
+ * merge - Merges multiple inputs into single output
66
+ * Simply passes through all packets from any source.
67
+ */
68
+ export function merge(options = {}) {
69
+ return (send, packet) => send(packet);
70
+ }
71
+
72
+ /**
73
+ * concat - Concatenates inputs (waits for completion)
74
+ * Since we're push-based, this buffers until 'complete' signal.
75
+ */
76
+ export function concat(options = {}) {
77
+ const { waitFor = 'complete' } = options;
78
+ const buffer = [];
79
+
80
+ return (send, packet) => {
81
+ if (packet._complete || packet.complete) {
82
+ for (const p of buffer) send(p);
83
+ buffer.length = 0;
84
+ } else {
85
+ buffer.push(packet);
86
+ }
87
+ };
88
+ }
89
+
90
+ /**
91
+ * zip - Zips values from multiple sources by index
92
+ *
93
+ * @param {Object} options
94
+ * @param {number} options.sources - Number of sources to zip
95
+ */
96
+ export function zip(options = {}) {
97
+ const { sources = 2 } = options;
98
+ const buffers = Array.from({ length: sources }, () => []);
99
+ let sourceIndex = 0;
100
+
101
+ return (send, packet) => {
102
+ const idx = packet._sourceIndex ?? sourceIndex++ % sources;
103
+ buffers[idx].push(packet);
104
+
105
+ // Check if all buffers have at least one item
106
+ if (buffers.every(b => b.length > 0)) {
107
+ const zipped = buffers.map(b => b.shift());
108
+ send({ payload: zipped.map(p => p.payload !== undefined ? p.payload : p) });
109
+ }
110
+ };
111
+ }
112
+
113
+ /**
114
+ * withLatestFrom - Combines with latest from another source
115
+ */
116
+ export function withLatestFrom(options = {}) {
117
+ const { pipe = null, app: getApp = null } = options;
118
+ let latest = null;
119
+ let hasLatest = false;
120
+
121
+ return (send, packet) => {
122
+ if (packet._pipe === pipe || packet._fromLatest) {
123
+ latest = packet.payload !== undefined ? packet.payload : packet;
124
+ hasLatest = true;
125
+ } else if (hasLatest) {
126
+ send({
127
+ ...packet,
128
+ payload: [packet.payload, latest]
129
+ });
130
+ }
131
+ };
132
+ }
133
+
134
+
135
+ // ─────────────────────────────────────────────────────────────
136
+ // Transformation Operators
137
+ // ─────────────────────────────────────────────────────────────
138
+
139
+ /**
140
+ * map - Transform each value
141
+ *
142
+ * @param {Object} options
143
+ * @param {Function} options.fn - Transformation function
144
+ */
145
+ export function map(options = {}) {
146
+ const { fn = (x) => x } = options;
147
+
148
+ return (send, packet) => {
149
+ const result = fn(packet.payload !== undefined ? packet.payload : packet, packet);
150
+ send({ ...packet, payload: result });
151
+ };
152
+ }
153
+
154
+ /**
155
+ * pluck - Extract a property
156
+ *
157
+ * @param {Object} options
158
+ * @param {string} options.path - Property path to extract
159
+ */
160
+ export function pluck(options = {}) {
161
+ const { path = 'payload' } = options;
162
+
163
+ return (send, packet) => {
164
+ const parts = path.split('.');
165
+ let value = packet;
166
+ for (const part of parts) {
167
+ value = value?.[part];
168
+ }
169
+ send({ ...packet, payload: value });
170
+ };
171
+ }
172
+
173
+ /**
174
+ * scan - Accumulate values over time
175
+ *
176
+ * @param {Object} options
177
+ * @param {Function} options.reducer - Reducer function (acc, value) => newAcc
178
+ * @param {*} options.initial - Initial accumulator value
179
+ */
180
+ export function scan(options = {}) {
181
+ const { reducer = (acc, x) => acc + x, initial = 0 } = options;
182
+ let accumulator = initial;
183
+
184
+ return (send, packet) => {
185
+ const value = packet.payload !== undefined ? packet.payload : packet;
186
+ accumulator = reducer(accumulator, value);
187
+ send({ ...packet, payload: accumulator });
188
+ };
189
+ }
190
+
191
+ /**
192
+ * buffer - Buffer values until condition
193
+ *
194
+ * @param {Object} options
195
+ * @param {number} options.count - Emit after N values
196
+ * @param {number} options.time - Emit after N ms
197
+ */
198
+ export function buffer(options = {}) {
199
+ const { count = null, time = null } = options;
200
+ const buf = [];
201
+ let timer = null;
202
+
203
+ const flush = (send) => {
204
+ if (buf.length > 0) {
205
+ send({ payload: buf.map(p => p.payload !== undefined ? p.payload : p) });
206
+ buf.length = 0;
207
+ }
208
+ if (timer) {
209
+ clearTimeout(timer);
210
+ timer = null;
211
+ }
212
+ };
213
+
214
+ return (send, packet) => {
215
+ buf.push(packet);
216
+
217
+ if (time && !timer) {
218
+ timer = setTimeout(() => flush(send), time);
219
+ }
220
+
221
+ if (count && buf.length >= count) {
222
+ flush(send);
223
+ }
224
+ };
225
+ }
226
+
227
+ /**
228
+ * window - Divide into windows
229
+ *
230
+ * @param {Object} options
231
+ * @param {number} options.count - Window size
232
+ */
233
+ export function window(options = {}) {
234
+ const { count = 10 } = options;
235
+ let windowBuf = [];
236
+
237
+ return (send, packet) => {
238
+ windowBuf.push(packet);
239
+
240
+ if (windowBuf.length >= count) {
241
+ send({ payload: windowBuf });
242
+ windowBuf = [];
243
+ }
244
+ };
245
+ }
246
+
247
+ /**
248
+ * pairwise - Emit previous and current value
249
+ */
250
+ export function pairwise(options = {}) {
251
+ let previous = null;
252
+ let hasPrevious = false;
253
+
254
+ return (send, packet) => {
255
+ if (hasPrevious) {
256
+ const prev = previous.payload !== undefined ? previous.payload : previous;
257
+ const curr = packet.payload !== undefined ? packet.payload : packet;
258
+ send({ ...packet, payload: [prev, curr] });
259
+ }
260
+ previous = packet;
261
+ hasPrevious = true;
262
+ };
263
+ }
264
+
265
+
266
+ // ─────────────────────────────────────────────────────────────
267
+ // Filtering Operators
268
+ // ─────────────────────────────────────────────────────────────
269
+
270
+ /**
271
+ * filter - Filter based on predicate
272
+ *
273
+ * @param {Object} options
274
+ * @param {Function} options.predicate - Filter function
275
+ */
276
+ export function filter(options = {}) {
277
+ const { predicate = () => true } = options;
278
+
279
+ return (send, packet) => {
280
+ const value = packet.payload !== undefined ? packet.payload : packet;
281
+ if (predicate(value, packet)) {
282
+ send(packet);
283
+ }
284
+ };
285
+ }
286
+
287
+ /**
288
+ * distinct - Only emit distinct values
289
+ *
290
+ * @param {Object} options
291
+ * @param {Function} options.keyFn - Key extraction function
292
+ */
293
+ export function distinct(options = {}) {
294
+ const { keyFn = (x) => x } = options;
295
+ const seen = new Set();
296
+
297
+ return (send, packet) => {
298
+ const value = packet.payload !== undefined ? packet.payload : packet;
299
+ const key = keyFn(value);
300
+
301
+ if (!seen.has(key)) {
302
+ seen.add(key);
303
+ send(packet);
304
+ }
305
+ };
306
+ }
307
+
308
+ /**
309
+ * distinctUntilChanged - Only emit when different from previous
310
+ *
311
+ * @param {Object} options
312
+ * @param {Function} options.comparator - Comparison function
313
+ */
314
+ export function distinctUntilChanged(options = {}) {
315
+ const { comparator = (a, b) => a === b } = options;
316
+ let previous;
317
+ let hasPrevious = false;
318
+
319
+ return (send, packet) => {
320
+ const value = packet.payload !== undefined ? packet.payload : packet;
321
+
322
+ if (!hasPrevious || !comparator(previous, value)) {
323
+ previous = value;
324
+ hasPrevious = true;
325
+ send(packet);
326
+ }
327
+ };
328
+ }
329
+
330
+ /**
331
+ * take - Take only first N values
332
+ *
333
+ * @param {Object} options
334
+ * @param {number} options.count - Number to take
335
+ */
336
+ export function take(options = {}) {
337
+ const { count = 1 } = options;
338
+ let taken = 0;
339
+
340
+ return (send, packet) => {
341
+ if (taken < count) {
342
+ taken++;
343
+ send(packet);
344
+ }
345
+ };
346
+ }
347
+
348
+ /**
349
+ * skip - Skip first N values
350
+ *
351
+ * @param {Object} options
352
+ * @param {number} options.count - Number to skip
353
+ */
354
+ export function skip(options = {}) {
355
+ const { count = 1 } = options;
356
+ let skipped = 0;
357
+
358
+ return (send, packet) => {
359
+ if (skipped >= count) {
360
+ send(packet);
361
+ } else {
362
+ skipped++;
363
+ }
364
+ };
365
+ }
366
+
367
+ /**
368
+ * takeWhile - Take while condition is true
369
+ *
370
+ * @param {Object} options
371
+ * @param {Function} options.predicate - Condition function
372
+ */
373
+ export function takeWhile(options = {}) {
374
+ const { predicate = () => true } = options;
375
+ let active = true;
376
+
377
+ return (send, packet) => {
378
+ if (active) {
379
+ const value = packet.payload !== undefined ? packet.payload : packet;
380
+ if (predicate(value, packet)) {
381
+ send(packet);
382
+ } else {
383
+ active = false;
384
+ }
385
+ }
386
+ };
387
+ }
388
+
389
+ /**
390
+ * skipWhile - Skip while condition is true
391
+ *
392
+ * @param {Object} options
393
+ * @param {Function} options.predicate - Condition function
394
+ */
395
+ export function skipWhile(options = {}) {
396
+ const { predicate = () => true } = options;
397
+ let skipping = true;
398
+
399
+ return (send, packet) => {
400
+ const value = packet.payload !== undefined ? packet.payload : packet;
401
+
402
+ if (skipping && !predicate(value, packet)) {
403
+ skipping = false;
404
+ }
405
+
406
+ if (!skipping) {
407
+ send(packet);
408
+ }
409
+ };
410
+ }
411
+
412
+
413
+ // ─────────────────────────────────────────────────────────────
414
+ // Timing Operators
415
+ // ─────────────────────────────────────────────────────────────
416
+
417
+ /**
418
+ * debounce - Debounce emissions
419
+ *
420
+ * @param {Object} options
421
+ * @param {number} options.time - Debounce time in ms
422
+ */
423
+ export function debounce(options = {}) {
424
+ const { time = 300 } = options;
425
+ let timer = null;
426
+ let latest = null;
427
+
428
+ return (send, packet) => {
429
+ latest = packet;
430
+
431
+ if (timer) clearTimeout(timer);
432
+
433
+ timer = setTimeout(() => {
434
+ send(latest);
435
+ timer = null;
436
+ }, time);
437
+ };
438
+ }
439
+
440
+ /**
441
+ * throttle - Throttle emissions
442
+ *
443
+ * @param {Object} options
444
+ * @param {number} options.time - Throttle time in ms
445
+ * @param {boolean} options.leading - Emit on leading edge (default: true)
446
+ * @param {boolean} options.trailing - Emit on trailing edge (default: true)
447
+ */
448
+ export function throttle(options = {}) {
449
+ const { time = 300, leading = true, trailing = true } = options;
450
+ let lastEmit = 0;
451
+ let timer = null;
452
+ let latest = null;
453
+
454
+ return (send, packet) => {
455
+ const now = Date.now();
456
+ latest = packet;
457
+
458
+ if (now - lastEmit >= time) {
459
+ if (leading) {
460
+ lastEmit = now;
461
+ send(packet);
462
+ }
463
+ } else if (trailing && !timer) {
464
+ timer = setTimeout(() => {
465
+ lastEmit = Date.now();
466
+ send(latest);
467
+ timer = null;
468
+ }, time - (now - lastEmit));
469
+ }
470
+ };
471
+ }
472
+
473
+ /**
474
+ * delay - Delay each emission
475
+ *
476
+ * @param {Object} options
477
+ * @param {number} options.time - Delay in ms
478
+ */
479
+ export function delay(options = {}) {
480
+ const { time = 0 } = options;
481
+
482
+ return (send, packet) => {
483
+ setTimeout(() => send(packet), time);
484
+ };
485
+ }
486
+
487
+ /**
488
+ * timeout - Error if no value within time
489
+ *
490
+ * @param {Object} options
491
+ * @param {number} options.time - Timeout in ms
492
+ */
493
+ export function timeout(options = {}) {
494
+ const { time = 5000 } = options;
495
+ let timer = null;
496
+
497
+ return (send, packet) => {
498
+ if (timer) clearTimeout(timer);
499
+
500
+ timer = setTimeout(() => {
501
+ send({ ...packet, error: 'timeout', _timeout: true });
502
+ }, time);
503
+
504
+ send(packet);
505
+ };
506
+ }
507
+
508
+ /**
509
+ * timestamp - Add timestamp to each value
510
+ */
511
+ export function timestamp(options = {}) {
512
+ return (send, packet) => {
513
+ send({
514
+ ...packet,
515
+ timestamp: Date.now()
516
+ });
517
+ };
518
+ }
519
+
520
+
521
+ // ─────────────────────────────────────────────────────────────
522
+ // Error Handling Operators
523
+ // ─────────────────────────────────────────────────────────────
524
+
525
+ /**
526
+ * catchError - Handle errors
527
+ *
528
+ * @param {Object} options
529
+ * @param {Function} options.handler - Error handler function
530
+ */
531
+ export function catchError(options = {}) {
532
+ const { handler = (err, packet) => packet } = options;
533
+
534
+ return (send, packet) => {
535
+ if (packet.error) {
536
+ const result = handler(packet.error, packet);
537
+ if (result !== null && result !== undefined) {
538
+ send(result);
539
+ }
540
+ } else {
541
+ send(packet);
542
+ }
543
+ };
544
+ }
545
+
546
+ /**
547
+ * retry - Retry on error
548
+ *
549
+ * @param {Object} options
550
+ * @param {number} options.count - Number of retries
551
+ */
552
+ export function retry(options = {}) {
553
+ const { count = 3 } = options;
554
+ let retries = 0;
555
+
556
+ return (send, packet) => {
557
+ if (packet.error && retries < count) {
558
+ retries++;
559
+ send({ ...packet, _retry: retries });
560
+ } else {
561
+ retries = 0;
562
+ send(packet);
563
+ }
564
+ };
565
+ }
566
+
567
+
568
+ // ─────────────────────────────────────────────────────────────
569
+ // Utility Operators
570
+ // ─────────────────────────────────────────────────────────────
571
+
572
+ /**
573
+ * tap - Side effect without transformation
574
+ *
575
+ * @param {Object} options
576
+ * @param {Function} options.fn - Side effect function
577
+ */
578
+ export function tap(options = {}) {
579
+ const { fn = () => {} } = options;
580
+
581
+ return (send, packet) => {
582
+ fn(packet);
583
+ send(packet);
584
+ };
585
+ }
586
+
587
+ /**
588
+ * log - Log values (alias for debug)
589
+ *
590
+ * @param {Object} options
591
+ * @param {string} options.label - Log label
592
+ */
593
+ export function log(options = {}) {
594
+ const { label = 'log', logger = console.log } = options;
595
+
596
+ return (send, packet) => {
597
+ logger(`[${label}]`, packet.payload !== undefined ? packet.payload : packet);
598
+ send(packet);
599
+ };
600
+ }
601
+
602
+ /**
603
+ * count - Count emissions
604
+ */
605
+ export function count(options = {}) {
606
+ let counter = 0;
607
+
608
+ return (send, packet) => {
609
+ counter++;
610
+ send({ ...packet, count: counter });
611
+ };
612
+ }
613
+
614
+ /**
615
+ * toArray - Collect all values into array
616
+ *
617
+ * @param {Object} options
618
+ * @param {Function} options.until - Condition to emit
619
+ */
620
+ export function toArray(options = {}) {
621
+ const { until = null } = options;
622
+ const arr = [];
623
+
624
+ return (send, packet) => {
625
+ arr.push(packet.payload !== undefined ? packet.payload : packet);
626
+
627
+ if (until && until(packet, arr)) {
628
+ send({ payload: [...arr] });
629
+ arr.length = 0;
630
+ }
631
+ };
632
+ }
633
+
634
+ /**
635
+ * defaultIfEmpty - Emit default if no values
636
+ *
637
+ * @param {Object} options
638
+ * @param {*} options.value - Default value
639
+ */
640
+ export function defaultIfEmpty(options = {}) {
641
+ const { value = null } = options;
642
+ let hasEmitted = false;
643
+
644
+ return (send, packet) => {
645
+ if (packet._complete && !hasEmitted) {
646
+ send({ payload: value });
647
+ } else {
648
+ hasEmitted = true;
649
+ send(packet);
650
+ }
651
+ };
652
+ }
653
+
654
+ /**
655
+ * share - Multicast to multiple subscribers
656
+ * Converts cold observable behavior to hot
657
+ */
658
+ export function share(options = {}) {
659
+ const subscribers = new Set();
660
+ let hasValue = false;
661
+ let latest = null;
662
+
663
+ const node = (send, packet) => {
664
+ latest = packet;
665
+ hasValue = true;
666
+ for (const sub of subscribers) {
667
+ sub(packet);
668
+ }
669
+ send(packet);
670
+ };
671
+
672
+ node.subscribe = (fn) => {
673
+ subscribers.add(fn);
674
+ if (hasValue) fn(latest);
675
+ return () => subscribers.delete(fn);
676
+ };
677
+
678
+ return node;
679
+ }