lulz 1.0.3 → 2.0.1
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/AGENTS.md +609 -0
- package/README.md +242 -140
- package/TODO.md +132 -0
- package/examples.js +169 -197
- package/index.js +164 -14
- package/package.json +16 -17
- package/src/flow.js +362 -215
- package/src/red-lib.js +595 -0
- package/src/rx-lib.js +679 -0
- package/src/utils.js +270 -0
- package/src/workers.js +367 -0
- package/test.js +505 -279
- package/src/nodes.js +0 -520
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
|
+
}
|