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/examples.js CHANGED
@@ -1,317 +1,289 @@
1
1
  /**
2
- * lulz Examples
3
- *
4
- * Various patterns and use cases
2
+ * lulz - Examples
3
+ *
4
+ * Various usage patterns and demonstrations.
5
5
  */
6
6
 
7
7
  import {
8
8
  flow,
9
9
  subflow,
10
10
  compose,
11
- Inject,
12
- Debug,
13
- Function as Fn,
14
- Change,
15
- Switch,
16
- Template,
17
- Delay,
18
- Join,
19
- Split,
11
+ parallel,
12
+ series,
13
+ inject,
14
+ debug,
15
+ func,
16
+ change,
17
+ template,
18
+ delay,
19
+ map,
20
+ filter,
21
+ scan,
22
+ debounce,
23
+ take,
24
+ pairwise,
25
+ tap,
20
26
  } from './index.js';
21
27
 
22
- // Custom logger to capture output
23
- const logs = [];
24
- const logger = (...args) => {
25
- logs.push(args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' '));
26
- console.log(...args);
27
- };
28
28
 
29
- // ============================================================
30
- // EXAMPLE 1: Basic pipe connection
31
- // ============================================================
29
+ // ─────────────────────────────────────────────────────────────
30
+ // Example 1: Basic Inject → Debug
31
+ // ─────────────────────────────────────────────────────────────
32
32
 
33
- console.log('\n=== Example 1: Basic Inject → Debug ===\n');
33
+ console.log('\n═══ Example 1: Basic Inject → Debug ═══\n');
34
34
 
35
35
  const example1 = flow([
36
- [Inject({ payload: 'Hello FlowGraph!', once: true }), Debug({ name: 'output', logger })],
36
+ [inject({ payload: 'Hello lulz!', once: true }), debug({ name: 'output' })],
37
37
  ]);
38
38
 
39
39
  example1.start();
40
40
 
41
- // ============================================================
42
- // EXAMPLE 2: Series processing with [a, b, c] syntax
43
- // ============================================================
44
41
 
45
- console.log('\n=== Example 2: Series Processing ===\n');
42
+ // ─────────────────────────────────────────────────────────────
43
+ // Example 2: EventEmitter API
44
+ // ─────────────────────────────────────────────────────────────
46
45
 
47
- // Each step adds to the message
48
- function addStep(name) {
49
- return function(options) {
50
- return (send, packet) => {
51
- const steps = packet.steps || [];
52
- logger(`[${name}] Processing...`);
53
- send({ ...packet, steps: [...steps, name] });
54
- };
55
- };
56
- }
46
+ console.log('\n═══ Example 2: EventEmitter API ═══\n');
57
47
 
58
48
  const example2 = flow([
59
- ['input', [addStep('validate'), addStep('transform'), addStep('enrich')], Debug({ name: 'result', logger })],
49
+ ['input', func({ func: (msg) => ({ ...msg, payload: msg.payload.toUpperCase() }) }), 'output'],
60
50
  ]);
61
51
 
62
- example2.inject('input', { payload: { data: 'test' } });
52
+ // Listen to output pipe
53
+ example2.on('output', (packet) => {
54
+ console.log('[Listener] Received:', packet.payload);
55
+ });
63
56
 
64
- // ============================================================
65
- // EXAMPLE 3: Fan-out (parallel processing)
66
- // ============================================================
57
+ // Inject via emit
58
+ example2.emit('input', { payload: 'hello via emit!' });
67
59
 
68
- console.log('\n=== Example 3: Fan-out Processing ===\n');
69
60
 
70
- function processA(options) {
71
- return (send, packet) => {
72
- logger('[A] Fast processing');
73
- send({ ...packet, processedBy: 'A' });
74
- };
75
- }
61
+ // ─────────────────────────────────────────────────────────────
62
+ // Example 3: Series Processing (Default)
63
+ // ─────────────────────────────────────────────────────────────
76
64
 
77
- function processB(options) {
78
- return (send, packet) => {
79
- logger('[B] Detailed processing');
80
- send({ ...packet, processedBy: 'B', extra: 'details' });
65
+ console.log('\n═══ Example 3: Series Processing ═══\n');
66
+
67
+ function addStep(name) {
68
+ return function(options) {
69
+ return (send, packet) => {
70
+ console.log(`[${name}] Processing...`);
71
+ send({ ...packet, steps: [...(packet.steps || []), name] });
72
+ };
81
73
  };
82
74
  }
83
75
 
84
76
  const example3 = flow([
85
- ['input', processA, processB, 'output'], // Both receive same input
86
- ['output', Debug({ name: 'fan-out-result', logger })],
77
+ ['input', addStep('validate'), addStep('transform'), addStep('save'), debug({ name: 'result', complete: true })],
87
78
  ]);
88
79
 
89
- example3.inject('input', { payload: 'parallel test' });
80
+ example3.emit('input', { payload: { data: 'test' } });
90
81
 
91
- // ============================================================
92
- // EXAMPLE 4: Mixed pre-configured and auto-configured functions
93
- // ============================================================
94
82
 
95
- console.log('\n=== Example 4: Pre-configured Functions ===\n');
83
+ // ─────────────────────────────────────────────────────────────
84
+ // Example 4: Parallel Processing with []
85
+ // ─────────────────────────────────────────────────────────────
96
86
 
97
- function multiplier(options) {
98
- const factor = options.factor || 1;
99
- return (send, packet) => {
100
- send({ ...packet, payload: packet.payload * factor });
87
+ console.log('\n═══ Example 4: Parallel Processing ═══\n');
88
+
89
+ function process(name, delay = 0) {
90
+ return function(options) {
91
+ return (send, packet) => {
92
+ setTimeout(() => {
93
+ console.log(`[${name}] Done`);
94
+ send({ ...packet, processor: name });
95
+ }, delay);
96
+ };
101
97
  };
102
98
  }
103
99
 
104
100
  const example4 = flow([
105
- ['input', [
106
- multiplier({ factor: 2 }), // Pre-configured: ×2
107
- multiplier, // Auto-configured with {}: ×1
108
- multiplier({ factor: 5 }), // Pre-configured: ×5
109
- ], Debug({ name: 'multiplied', logger })],
101
+ ['input', [process('fast', 10), process('slow', 50), process('medium', 30)], 'output'],
102
+ ['output', debug({ name: 'parallel-result' })],
110
103
  ]);
111
104
 
112
- example4.inject('input', { payload: 10 }); // 10 → 20 → 20 → 100
105
+ example4.emit('input', { payload: 'parallel test' });
106
+
113
107
 
114
- // ============================================================
115
- // EXAMPLE 5: Conditional routing with Switch
116
- // ============================================================
108
+ // ─────────────────────────────────────────────────────────────
109
+ // Example 5: Helper Functions
110
+ // ─────────────────────────────────────────────────────────────
117
111
 
118
- console.log('\n=== Example 5: Conditional Routing ===\n');
112
+ console.log('\n═══ Example 5: series() and parallel() Helpers ═══\n');
113
+
114
+ function multiply(n) {
115
+ return function(options) {
116
+ return (send, packet) => {
117
+ send({ ...packet, payload: packet.payload * n });
118
+ };
119
+ };
120
+ }
119
121
 
120
122
  const example5 = flow([
121
- ['input', Switch({
122
- property: 'payload.temperature',
123
- rules: [
124
- { type: 'gte', value: 30 },
125
- ]
126
- }), 'hot'],
127
-
128
- ['input', Switch({
129
- property: 'payload.temperature',
130
- rules: [
131
- { type: 'lt', value: 30 },
132
- ]
133
- }), 'cold'],
134
-
135
- ['hot', Debug({ name: '🔥 HOT', logger })],
136
- ['cold', Debug({ name: '❄️ COLD', logger })],
123
+ // series: 10 → 20 → 60 → 120
124
+ ['input', series(multiply(2), multiply(3), multiply(2)), debug({ name: 'series-result' })],
137
125
  ]);
138
126
 
139
- example5.inject('input', { payload: { temperature: 35, city: 'Phoenix' } });
140
- example5.inject('input', { payload: { temperature: 15, city: 'Seattle' } });
127
+ example5.emit('input', { payload: 10 });
128
+
141
129
 
142
- // ============================================================
143
- // EXAMPLE 6: Template rendering
144
- // ============================================================
130
+ // ─────────────────────────────────────────────────────────────
131
+ // Example 6: Subflows (Reusable Components)
132
+ // ─────────────────────────────────────────────────────────────
145
133
 
146
- console.log('\n=== Example 6: Template Rendering ===\n');
134
+ console.log('\n═══ Example 6: Subflows ═══\n');
147
135
 
148
- const example6 = flow([
149
- ['input', Template({
150
- template: '{{payload.name}} from {{payload.city}} says: "{{payload.message}}"'
151
- }), Debug({ name: 'message', logger })],
136
+ // Create reusable sanitizer
137
+ const sanitizer = subflow([
138
+ ['in', func({ func: (msg) => ({
139
+ ...msg,
140
+ payload: String(msg.payload).trim().toLowerCase()
141
+ })}), 'out'],
142
+ ]);
143
+
144
+ // Create reusable validator
145
+ const validator = subflow([
146
+ ['in', func({ func: (msg) => ({
147
+ ...msg,
148
+ payload: msg.payload,
149
+ valid: msg.payload.length > 0
150
+ })}), 'out'],
152
151
  ]);
153
152
 
154
- example6.inject('input', {
155
- payload: {
156
- name: 'Alice',
157
- city: 'Wonderland',
158
- message: 'Down the rabbit hole!'
159
- }
153
+ // Compose them
154
+ const pipeline = compose(sanitizer, validator);
155
+
156
+ pipeline.on('out', (packet) => {
157
+ console.log('[Pipeline Result]', packet);
160
158
  });
161
159
 
162
- // ============================================================
163
- // EXAMPLE 7: Subflow embedding
164
- // ============================================================
160
+ pipeline.emit('in', { payload: ' HELLO WORLD ' });
165
161
 
166
- console.log('\n=== Example 7: Subflow (Reusable Component) ===\n');
167
162
 
168
- // Create a reusable "sanitizer" subflow
169
- const sanitizer = subflow([
170
- ['in', Fn({
171
- func: (msg) => ({
172
- ...msg,
173
- payload: String(msg.payload).trim().toLowerCase()
174
- })
175
- }), 'out'],
176
- ]);
163
+ // ─────────────────────────────────────────────────────────────
164
+ // Example 7: RxJS-Style Operators
165
+ // ─────────────────────────────────────────────────────────────
166
+
167
+ console.log('\n═══ Example 7: RxJS-Style Operators ═══\n');
177
168
 
178
- // Use it in main flow
179
169
  const example7 = flow([
180
- ['input', 'process'],
181
- ['process', Debug({ name: 'sanitized', logger })],
170
+ ['input',
171
+ map({ fn: (x) => x * 2 }),
172
+ filter({ predicate: (x) => x > 5 }),
173
+ scan({ reducer: (acc, x) => acc + x, initial: 0 }),
174
+ debug({ name: 'rx-result' })
175
+ ],
182
176
  ]);
183
177
 
184
- // Wire up subflow
185
- example7.pipes['input'].connect(sanitizer._input);
186
- sanitizer._output.connect(example7.pipes['process']);
178
+ [1, 2, 3, 4, 5].forEach(n => {
179
+ example7.emit('input', { payload: n });
180
+ });
187
181
 
188
- example7.inject('input', { payload: ' HELLO WORLD ' });
189
182
 
190
- // ============================================================
191
- // EXAMPLE 8: Blog builder (original use case)
192
- // ============================================================
183
+ // ─────────────────────────────────────────────────────────────
184
+ // Example 8: Blog Builder Pattern
185
+ // ─────────────────────────────────────────────────────────────
193
186
 
194
- console.log('\n=== Example 8: Blog Builder Pattern ===\n');
187
+ console.log('\n═══ Example 8: Blog Builder ═══\n');
195
188
 
196
- // Simulated producer functions
189
+ // Simulated producers
197
190
  function socket(channel) {
198
191
  return (send) => {
199
- logger(`[socket] Listening on: ${channel}`);
200
- // Simulate receiving data
192
+ console.log(`[socket] Listening: ${channel}`);
201
193
  setTimeout(() => {
202
- send({ payload: { type: 'new-post', id: 123 }, channel });
203
- }, 50);
194
+ send({ payload: { type: 'new-post', id: 123 }, topic: channel });
195
+ }, 100);
204
196
  };
205
197
  }
206
198
 
207
199
  function watch(folder) {
208
200
  return (send) => {
209
- logger(`[watch] Watching: ${folder}`);
210
- // Simulate file change
201
+ console.log(`[watch] Watching: ${folder}`);
211
202
  setTimeout(() => {
212
- send({ payload: { type: 'file-changed', path: `${folder}/image.png` }, folder });
213
- }, 75);
203
+ send({ payload: { type: 'file-changed', path: `${folder}/image.png` }, topic: folder });
204
+ }, 150);
214
205
  };
215
206
  }
216
207
 
217
208
  // Processing functions
218
209
  function cover(options) {
219
210
  return (send, packet) => {
220
- logger('[cover] Generating cover image...');
211
+ console.log('[cover] Generating cover...');
221
212
  send({ ...packet, cover: true });
222
213
  };
223
214
  }
224
215
 
225
216
  function audio(options) {
226
217
  return (send, packet) => {
227
- logger('[audio] Processing audio...');
218
+ console.log('[audio] Processing audio...');
228
219
  send({ ...packet, audio: true });
229
220
  };
230
221
  }
231
222
 
232
223
  function post(options) {
233
224
  return (send, packet) => {
234
- logger('[post] Building post...');
225
+ console.log('[post] Building post...');
235
226
  send({ ...packet, built: true });
236
227
  };
237
228
  }
238
229
 
239
230
  function assets(options) {
240
231
  return (send, packet) => {
241
- logger('[assets] Processing asset:', packet.payload?.path);
232
+ console.log('[assets] Processing asset');
242
233
  send(packet);
243
234
  };
244
235
  }
245
236
 
246
237
  function pagerizer(options) {
247
238
  return (send, packet) => {
248
- logger('[pagerizer] Updating pagination...');
249
- send({ ...packet, paginated: true });
239
+ console.log('[pagerizer] Updating pagination');
240
+ send(packet);
250
241
  };
251
242
  }
252
243
 
253
244
  const blogBuilder = flow([
254
- [socket('post'), 'post'], // Socket events → post pipe
255
- [watch('assets'), 'asset'], // File watcher → asset pipe
256
- ['post', Debug({ name: 'new-post', logger })], // Log new posts
257
- ['asset', assets], // Process assets
258
- ['post', cover, audio, post, 'updated'], // Fan-out: cover, audio, post all run
259
- ['updated', pagerizer, Debug({ name: 'updated', logger })],
245
+ [socket('post'), 'post'],
246
+ [watch('assets'), 'asset'],
247
+ ['post', debug({ name: 'new-post' })],
248
+ ['asset', assets],
249
+ // Fan-out: cover, audio, post all run in parallel
250
+ ['post', [cover, audio, post], 'updated'],
251
+ ['updated', pagerizer, debug({ name: 'updated' })],
260
252
  ], { username: 'alice' });
261
253
 
262
254
  blogBuilder.start();
263
255
 
264
- // ============================================================
265
- // EXAMPLE 9: Series with mixed fan-out
266
- // ============================================================
267
-
268
- console.log('\n=== Example 9: Complex Pipeline ===\n');
269
-
270
- function validate(options) {
271
- return (send, packet) => {
272
- if (packet.payload) {
273
- logger('[validate] ✓ Valid');
274
- send(packet);
275
- } else {
276
- logger('[validate] ✗ Invalid - dropped');
277
- }
278
- };
279
- }
280
-
281
- function enrichA(options) {
282
- return (send, packet) => {
283
- logger('[enrichA] Adding metadata A');
284
- send({ ...packet, metaA: true });
285
- };
286
- }
287
256
 
288
- function enrichB(options) {
289
- return (send, packet) => {
290
- logger('[enrichB] Adding metadata B');
291
- send({ ...packet, metaB: true });
292
- };
293
- }
257
+ // ─────────────────────────────────────────────────────────────
258
+ // Example 9: Temperature Monitor with Pairwise
259
+ // ─────────────────────────────────────────────────────────────
294
260
 
295
- function finalize(options) {
296
- return (send, packet) => {
297
- logger('[finalize] Completing...');
298
- send({ ...packet, finalized: true });
299
- };
300
- }
261
+ console.log('\n═══ Example 9: Temperature Delta ═══\n');
301
262
 
302
- // Complex pipeline: validate → (enrichA + enrichB in parallel) → finalize
303
- const example9 = flow([
304
- ['input', [validate], 'validated'], // Series: just validate
305
- ['validated', enrichA, enrichB, 'enriched'], // Fan-out: both enrichers
306
- ['enriched', [finalize], Debug({ name: 'final', logger })], // Series: finalize
263
+ const tempMonitor = flow([
264
+ ['temp',
265
+ pairwise(),
266
+ func({ func: (msg) => ({
267
+ ...msg,
268
+ payload: {
269
+ prev: msg.payload[0],
270
+ curr: msg.payload[1],
271
+ delta: msg.payload[1] - msg.payload[0]
272
+ }
273
+ })}),
274
+ debug({ name: 'temp-delta', complete: true })
275
+ ],
307
276
  ]);
308
277
 
309
- example9.inject('input', { payload: { data: 'important' } });
278
+ [20, 22, 21, 25, 23].forEach((temp, i) => {
279
+ setTimeout(() => tempMonitor.emit('temp', { payload: temp }), i * 10);
280
+ });
281
+
310
282
 
311
- // ============================================================
312
- // Summary
313
- // ============================================================
283
+ // ─────────────────────────────────────────────────────────────
284
+ // Cleanup
285
+ // ─────────────────────────────────────────────────────────────
314
286
 
315
287
  setTimeout(() => {
316
- console.log('\n=== All Examples Complete ===\n');
317
- }, 200);
288
+ console.log('\n═══ All Examples Complete ═══\n');
289
+ }, 500);
package/index.js CHANGED
@@ -1,21 +1,171 @@
1
1
  /**
2
- * lulz - A reactive dataflow system
3
- *
4
- * Inspired by FFmpeg filtergraph notation and Node-RED
2
+ * lulz - A reactive dataflow system that makes coders happy
3
+ *
4
+ * https://github.com/catpea/lulz
5
+ *
6
+ * @example
7
+ * import { flow, inject, debug, series, parallel } from 'lulz';
8
+ *
9
+ * const app = flow([
10
+ * [inject({ payload: 'Hello!' }), debug({ name: 'out' })],
11
+ * ]);
12
+ *
13
+ * app.start();
5
14
  */
6
15
 
7
- export { flow, subflow, compose, makeNode, makePipe, isOuter, isInner } from './src/flow.js';
16
+ // ─────────────────────────────────────────────────────────────
17
+ // Core Flow Engine
18
+ // ─────────────────────────────────────────────────────────────
19
+
20
+ export {
21
+ flow,
22
+ subflow,
23
+ compose,
24
+ parallel,
25
+ series,
26
+ makeNode,
27
+ isOuter,
28
+ isInner,
29
+ isFlow,
30
+ } from './src/flow.js';
31
+
32
+
33
+ // ─────────────────────────────────────────────────────────────
34
+ // Node-RED Style Library
35
+ // ─────────────────────────────────────────────────────────────
36
+
8
37
  export {
9
- Inject,
10
- Debug,
11
- Function,
12
- Change,
13
- Switch,
14
- Template,
15
- Delay,
16
- Join,
17
- Split,
38
+ // Core nodes
39
+ inject,
40
+ debug,
41
+ func,
42
+ change,
43
+ switchNode as switch, // 'switch' is reserved, use switchNode
44
+ switchNode,
45
+ template,
46
+ delay,
47
+ split,
48
+ join,
49
+ filter,
50
+ linkIn,
51
+ linkOut,
52
+ catchError,
53
+ status,
54
+
55
+ // Property helpers
18
56
  getProperty,
19
57
  setProperty,
20
58
  deleteProperty,
21
- } from './src/nodes.js';
59
+ } from './src/red-lib.js';
60
+
61
+
62
+ // ─────────────────────────────────────────────────────────────
63
+ // RxJS-Inspired Operators
64
+ // ─────────────────────────────────────────────────────────────
65
+
66
+ export {
67
+ // Combination
68
+ combineLatest,
69
+ merge,
70
+ concat,
71
+ zip,
72
+ withLatestFrom,
73
+
74
+ // Transformation
75
+ map,
76
+ pluck,
77
+ scan,
78
+ buffer,
79
+ window,
80
+ pairwise,
81
+
82
+ // Filtering
83
+ filter as rxFilter,
84
+ distinct,
85
+ distinctUntilChanged,
86
+ take,
87
+ skip,
88
+ takeWhile,
89
+ skipWhile,
90
+
91
+ // Timing
92
+ debounce,
93
+ throttle,
94
+ delay as rxDelay,
95
+ timeout,
96
+ timestamp,
97
+
98
+ // Error handling
99
+ catchError as rxCatchError,
100
+ retry,
101
+
102
+ // Utility
103
+ tap,
104
+ log,
105
+ count,
106
+ toArray,
107
+ defaultIfEmpty,
108
+ share,
109
+ } from './src/rx-lib.js';
110
+
111
+
112
+ // ─────────────────────────────────────────────────────────────
113
+ // Worker Task Queue
114
+ // ─────────────────────────────────────────────────────────────
115
+
116
+ export {
117
+ taskQueue,
118
+ worker,
119
+ parallelMap,
120
+ cpuTask,
121
+ } from './src/workers.js';
122
+
123
+
124
+ // ─────────────────────────────────────────────────────────────
125
+ // Utilities
126
+ // ─────────────────────────────────────────────────────────────
127
+
128
+ export {
129
+ // Packet helpers
130
+ packet,
131
+ clonePacket,
132
+ mergePackets,
133
+
134
+ // Function composition
135
+ pipe,
136
+ compose as composeF,
137
+ identity,
138
+ constant,
139
+ noop,
140
+
141
+ // Async helpers
142
+ sleep,
143
+ withTimeout,
144
+ withRetry,
145
+
146
+ // Collection helpers
147
+ chunk,
148
+ flatten,
149
+ unique,
150
+ groupBy,
151
+ partition,
152
+
153
+ // Object helpers
154
+ deepClone,
155
+ deepMerge,
156
+ pick,
157
+ omit,
158
+
159
+ // Debug helpers
160
+ createLogger,
161
+ measure,
162
+ assert,
163
+ } from './src/utils.js';
164
+
165
+
166
+ // ─────────────────────────────────────────────────────────────
167
+ // Default Export
168
+ // ─────────────────────────────────────────────────────────────
169
+
170
+ import { flow as flowFn } from './src/flow.js';
171
+ export default flowFn;