lulz 1.0.3 → 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/README.md CHANGED
@@ -1,6 +1,28 @@
1
1
  # lulz
2
2
 
3
- A reactive dataflow system inspired by FFmpeg filtergraph notation and Node-RED.
3
+ A reactive dataflow system that makes coders happy. 🎉
4
+
5
+ Inspired by FFmpeg filtergraph notation, Node-RED, and RxJS.
6
+
7
+ ```javascript
8
+ import { flow, inject, debug } from 'lulz';
9
+
10
+ const app = flow([
11
+ [inject({ payload: 'Hello!' }), debug({ name: 'out' })],
12
+ ]);
13
+
14
+ app.start();
15
+ ```
16
+
17
+ ## Features
18
+
19
+ - **EventEmitter API** - Natural `emit`/`on` for packet injection and interception
20
+ - **Series by Default** - `['in', a, b, c, 'out']` processes sequentially
21
+ - **Explicit Parallel** - `['in', [a, b, c], 'out']` fans out to all
22
+ - **Auto-compose** - Embed flows within flows seamlessly
23
+ - **Worker Pool** - CPU-bound tasks with Worker Threads/Web Workers
24
+ - **RxJS Operators** - `map`, `filter`, `scan`, `combineLatest`, and more
25
+ - **Node-RED Style** - `inject`, `debug`, `func`, `switch`, `template`
4
26
 
5
27
  ## Installation
6
28
 
@@ -11,275 +33,355 @@ npm install lulz
11
33
  ## Quick Start
12
34
 
13
35
  ```javascript
14
- import { flow, Inject, Debug } from 'lulz';
36
+ import { flow, inject, debug, func } from 'lulz';
15
37
 
38
+ // Create a flow
16
39
  const app = flow([
17
- [Inject({ payload: 'Hello World!' }), Debug({ name: 'output' })],
40
+ [inject({ payload: 'Hello World!' }), 'input'],
41
+ ['input', func({ func: msg => ({ ...msg, payload: msg.payload.toUpperCase() }) }), 'output'],
42
+ ['output', debug({ name: 'result' })],
18
43
  ]);
19
44
 
45
+ // Start producers
20
46
  app.start();
21
- ```
22
47
 
23
- ## Core Concepts
48
+ // Or inject manually via EventEmitter API
49
+ app.emit('input', { payload: 'Manual injection!' });
24
50
 
25
- ### Flow Syntax
51
+ // Listen to any pipe
52
+ app.on('output', (packet) => {
53
+ console.log('Received:', packet.payload);
54
+ });
55
+ ```
56
+
57
+ ## Processing Modes
26
58
 
27
- Each line in a flow is an array: `[source, ...transforms, destination]`
59
+ ### Series (Default)
28
60
 
29
- - **Source**: A string (pipe name) or producer function
30
- - **Transforms**: Functions that process packets
31
- - **Destination**: A string (pipe name) or consumer function
61
+ Functions process sequentially. Output of one becomes input of next.
32
62
 
33
63
  ```javascript
34
- const app = flow([
35
- [producer, 'pipeName'], // Producer → pipe
36
- ['pipeName', transform, 'out'], // Pipe transformpipe
37
- ['out', consumer], // Pipe → consumer
38
- ]);
64
+ ['input', validate, transform, save, 'output']
65
+ // ↓ ↓ ↓
66
+ // packet packetpacketoutput
39
67
  ```
40
68
 
41
- ### Function Types
69
+ ### Parallel (Explicit)
42
70
 
43
- **Outer functions** (factories) are regular functions that return inner functions:
71
+ Use `[]` or `parallel()` to fan out. All receive the same packet.
44
72
 
45
73
  ```javascript
46
- function myTransform(options) { // Outer - receives config
47
- return (send, packet) => { // Inner - processes packets
48
- send({ ...packet, modified: true });
49
- };
50
- }
74
+ ['input', [processA, processB, processC], 'output']
75
+ // ↓ ↓ ↓
76
+ // packet packet packet
77
+ // ↓ ↓ ↓
78
+ // └─────────┴─────────┘
79
+ // ↓
80
+ // output (3 packets)
51
81
  ```
52
82
 
53
- **Inner functions** are arrow functions that do the actual processing:
83
+ ### Helper Functions
54
84
 
55
85
  ```javascript
56
- const passthrough = (send, packet) => send(packet);
86
+ import { series, parallel } from 'lulz';
87
+
88
+ // Explicit series (same as default, but clearer)
89
+ ['input', series(a, b, c), 'output']
90
+
91
+ // Explicit parallel
92
+ ['input', parallel(a, b, c), 'output']
57
93
  ```
58
94
 
59
- The system auto-detects which is which using `fn.hasOwnProperty('prototype')`:
60
- - Regular functions have `prototype` → outer
61
- - Arrow functions don't → inner
95
+ ## EventEmitter API
62
96
 
63
- ### Pre-configured vs Auto-configured
97
+ Every flow is an EventEmitter. Pipes are events.
64
98
 
65
99
  ```javascript
66
- ['input', myTransform, 'output'] // Auto-config: called with {}
67
- ['input', myTransform({ option: 1 }), 'output'] // Pre-configured
100
+ const app = flow([
101
+ ['data', transform, 'result'],
102
+ ]);
103
+
104
+ // Inject packets
105
+ app.emit('data', { payload: 42 });
106
+
107
+ // Listen to pipes
108
+ app.on('result', (packet) => {
109
+ console.log(packet.payload);
110
+ });
111
+
112
+ // Also works
113
+ app.inject('data', { payload: 42 });
68
114
  ```
69
115
 
70
- ## Processing Modes
116
+ ## Function Types
71
117
 
72
- ### Fan-out (Parallel)
118
+ ### Outer Functions (Factories)
73
119
 
74
- All transforms receive the same packet simultaneously:
120
+ Regular functions that return inner functions. Called with options.
75
121
 
76
122
  ```javascript
77
- ['input', transformA, transformB, transformC, 'output']
78
- // ↓ ↓ ↓
79
- // packet packet packet
80
- // ↓ ↓ ↓
81
- // └───────────┴───────────┘
82
- // ↓
83
- // output (receives 3 packets)
123
+ function myTransform(options) {
124
+ const { multiplier = 1 } = options;
125
+ return (send, packet) => {
126
+ send({ ...packet, payload: packet.payload * multiplier });
127
+ };
128
+ }
129
+
130
+ // Usage
131
+ ['input', myTransform({ multiplier: 2 }), 'output'] // Pre-configured
132
+ ['input', myTransform, 'output'] // Auto-configured with {}
84
133
  ```
85
134
 
86
- ### Series (Sequential)
135
+ ### Inner Functions (Processors)
87
136
 
88
- Use `[a, b, c]` syntax for sequential processing:
137
+ Arrow functions that process packets. Used directly.
89
138
 
90
139
  ```javascript
91
- ['input', [transformA, transformB, transformC], 'output']
92
- // ↓
93
- // packet
94
- // ↓
95
- // transformA
96
- // ↓
97
- // transformB
98
- // ↓
99
- // transformC
100
- // ↓
101
- // output (receives 1 packet)
140
+ const double = (send, packet) => {
141
+ send({ ...packet, payload: packet.payload * 2 });
142
+ };
143
+
144
+ ['input', double, 'output']
102
145
  ```
103
146
 
104
- ### Mixed
147
+ ## Subflows
105
148
 
106
- Combine both in a single line:
149
+ Create reusable flow components.
107
150
 
108
151
  ```javascript
109
- ['input', validate, [enrich, format], notify, 'output']
110
- // ↓ ↓ ↓
111
- // fan-out series fan-out
152
+ import { subflow, compose } from 'lulz';
153
+
154
+ // Create reusable component
155
+ const sanitizer = subflow([
156
+ ['in', func({ func: msg => ({
157
+ ...msg,
158
+ payload: String(msg.payload).trim().toLowerCase()
159
+ })}), 'out'],
160
+ ]);
161
+
162
+ // Use via compose
163
+ const pipeline = compose(sanitizer, validator, enricher);
164
+ pipeline.emit('in', { payload: ' HELLO ' });
165
+
166
+ // Or embed in flow (auto-compose)
167
+ const app = flow([
168
+ ['input', sanitizer, 'output'],
169
+ ]);
112
170
  ```
113
171
 
114
- ## Built-in Nodes
172
+ ## Node-RED Style Nodes
115
173
 
116
- ### Inject
174
+ ### inject
117
175
 
118
- Produces packets on schedule or trigger:
176
+ Produce packets on schedule.
119
177
 
120
178
  ```javascript
121
- Inject({
122
- payload: 'hello', // or () => Date.now() for dynamic
179
+ inject({
180
+ payload: 'hello', // or () => Date.now()
123
181
  topic: 'greeting',
124
- once: true, // Emit once on start
125
- onceDelay: 100, // Delay before first emit (ms)
126
- interval: 1000, // Repeat interval (ms)
182
+ once: true, // Emit once on start
183
+ onceDelay: 100, // Delay before first
184
+ interval: 1000, // Repeat interval (ms)
127
185
  })
128
186
  ```
129
187
 
130
- ### Debug
188
+ ### debug
131
189
 
132
- Logs packets:
190
+ Log packets.
133
191
 
134
192
  ```javascript
135
- Debug({
193
+ debug({
136
194
  name: 'my-debug',
137
195
  active: true,
138
- complete: false, // true = show full msg, false = payload only
139
- logger: console.log, // Custom logger function
196
+ complete: false, // true = full packet
197
+ logger: console.log,
140
198
  })
141
199
  ```
142
200
 
143
- ### Function
201
+ ### func
144
202
 
145
- Execute custom JavaScript:
203
+ Execute custom code.
146
204
 
147
205
  ```javascript
148
- Function({
206
+ func({
149
207
  func: (msg, context) => {
150
208
  return { ...msg, payload: msg.payload.toUpperCase() };
151
209
  },
152
210
  })
153
211
  ```
154
212
 
155
- Return `null` to drop the message.
156
-
157
- ### Change
213
+ ### change
158
214
 
159
- Modify message properties:
215
+ Modify properties.
160
216
 
161
217
  ```javascript
162
- Change({
218
+ change({
163
219
  rules: [
164
220
  { type: 'set', prop: 'payload', to: 'new value' },
165
- { type: 'set', prop: 'topic', to: (msg) => msg.payload.type },
166
221
  { type: 'change', prop: 'payload', from: /old/, to: 'new' },
167
- { type: 'delete', prop: 'unwanted' },
168
- { type: 'move', prop: 'payload', to: 'data' },
222
+ { type: 'delete', prop: 'temp' },
223
+ { type: 'move', prop: 'data', to: 'payload' },
169
224
  ]
170
225
  })
171
226
  ```
172
227
 
173
- ### Switch
228
+ ### switchNode
174
229
 
175
- Route messages based on conditions:
230
+ Route by conditions.
176
231
 
177
232
  ```javascript
178
- Switch({
233
+ switchNode({
179
234
  property: 'payload',
180
235
  rules: [
181
- { type: 'eq', value: 'hello' },
182
236
  { type: 'gt', value: 100 },
183
237
  { type: 'regex', value: /^test/ },
184
238
  { type: 'else' },
185
239
  ],
186
- checkall: false, // Stop at first match
187
240
  })
188
241
  ```
189
242
 
190
- Rule types: `eq`, `neq`, `lt`, `gt`, `lte`, `gte`, `regex`, `true`, `false`, `null`, `nnull`, `else`
243
+ ### template
191
244
 
192
- ### Template
193
-
194
- Render mustache templates:
245
+ Render templates.
195
246
 
196
247
  ```javascript
197
- Template({
198
- template: 'Hello {{name}}, you have {{count}} messages!',
199
- field: 'payload', // Output field
248
+ template({
249
+ template: 'Hello {{name}}!',
250
+ field: 'payload',
200
251
  })
201
252
  ```
202
253
 
203
- ### Delay
254
+ ## RxJS-Style Operators
204
255
 
205
- Delay or rate-limit messages:
256
+ ### Transformation
206
257
 
207
258
  ```javascript
208
- Delay({
209
- delay: 1000, // Delay in ms
210
- rate: 10, // Or rate limit (msgs/sec)
211
- drop: false, // Drop vs queue when rate limited
212
- })
213
- ```
259
+ import { map, scan, pluck, pairwise, buffer } from 'lulz';
214
260
 
215
- ### Split
261
+ map({ fn: x => x * 2 })
262
+ scan({ reducer: (acc, x) => acc + x, initial: 0 })
263
+ pluck({ path: 'data.value' })
264
+ pairwise()
265
+ buffer({ count: 5 })
266
+ ```
216
267
 
217
- Split arrays/strings into sequences:
268
+ ### Filtering
218
269
 
219
270
  ```javascript
220
- Split({
221
- property: 'payload',
222
- delimiter: ',', // For strings, or 'array' for arrays
223
- })
271
+ import { filter, take, skip, distinct, distinctUntilChanged } from 'lulz';
272
+
273
+ filter({ predicate: x => x > 0 })
274
+ take({ count: 5 })
275
+ skip({ count: 2 })
276
+ distinct()
277
+ distinctUntilChanged()
224
278
  ```
225
279
 
226
- ### Join
280
+ ### Timing
227
281
 
228
- Join sequences back together:
282
+ ```javascript
283
+ import { debounce, throttle, delay, timeout } from 'lulz';
284
+
285
+ debounce({ time: 300 })
286
+ throttle({ time: 1000 })
287
+ delay({ time: 500 })
288
+ timeout({ time: 5000 })
289
+ ```
290
+
291
+ ### Combination
229
292
 
230
293
  ```javascript
231
- Join({
232
- count: 5, // Emit after N messages
233
- property: 'payload',
234
- mode: 'manual', // 'auto', 'manual', 'reduce'
235
- })
294
+ import { combineLatest, merge, zip, withLatestFrom } from 'lulz';
295
+
296
+ combineLatest({ pipes: ['temp', 'humidity'] })
297
+ merge()
298
+ zip({ sources: 2 })
236
299
  ```
237
300
 
238
- ## Subflows
301
+ ## Worker Pool
239
302
 
240
- Create reusable flow components:
303
+ Process CPU-bound tasks in parallel using Worker Threads.
241
304
 
242
305
  ```javascript
243
- const sanitizer = subflow([
244
- ['in', Function({ func: (msg) => ({
245
- ...msg,
246
- payload: String(msg.payload).trim().toLowerCase()
247
- })}), 'out'],
306
+ import { taskQueue, worker, parallelMap } from 'lulz';
307
+
308
+ // Standalone task queue
309
+ const queue = taskQueue({
310
+ workers: 4,
311
+ handler: (data) => heavyComputation(data),
312
+ });
313
+
314
+ queue.on('result', ({ id, result }) => console.log(result));
315
+ queue.submit({ data: 42 });
316
+ await queue.drain();
317
+
318
+ // In a flow
319
+ const app = flow([
320
+ ['input', worker({
321
+ workers: 4,
322
+ handler: (data) => data * data,
323
+ }), 'output'],
248
324
  ]);
325
+ ```
249
326
 
250
- // Wire into main flow
251
- mainFlow.pipes['input'].connect(sanitizer._input);
252
- sanitizer._output.connect(mainFlow.pipes['process']);
327
+ ## API Reference
328
+
329
+ ### flow(graph, context?)
330
+
331
+ Create a new flow.
332
+
333
+ ```javascript
334
+ const app = flow([...], { username: 'alice' });
335
+
336
+ app.start(); // Start producers
337
+ app.stop(); // Stop producers
338
+ app.emit(pipe, packet); // Inject packet
339
+ app.on(pipe, handler); // Listen to pipe
340
+ app.pipe(name); // Get pipe node
253
341
  ```
254
342
 
255
- ## API
343
+ ### subflow(graph, context?)
256
344
 
257
- ### `flow(graph, context)`
345
+ Create a reusable flow with `in`/`out` pipes.
258
346
 
259
- Create a new flow from a graph definition.
347
+ ### compose(...flows)
260
348
 
261
- Returns:
262
- - `start()` - Start all producers
263
- - `stop()` - Stop all producers
264
- - `inject(pipeName, packet)` - Inject a packet into a pipe
265
- - `getPipe(name)` - Get a pipe by name
266
- - `pipes` - Object of all pipes
349
+ Connect flows in sequence.
267
350
 
268
- ### `subflow(graph, context)`
351
+ ### series(...fns) / parallel(...fns)
269
352
 
270
- Create a subflow with `in` and `out` pipes.
353
+ Explicit processing mode markers.
271
354
 
272
- ### `compose(...flows)`
355
+ ## Project Structure
273
356
 
274
- Connect multiple flows in sequence.
357
+ ```
358
+ lulz/
359
+ ├── index.js # Main exports
360
+ ├── src/
361
+ │ ├── flow.js # Core engine
362
+ │ ├── red-lib.js # Node-RED style nodes
363
+ │ ├── rx-lib.js # RxJS operators
364
+ │ ├── workers.js # Worker pool
365
+ │ └── utils.js # Utilities
366
+ ├── test.js
367
+ ├── examples.js
368
+ ├── TODO.md # Future operators
369
+ └── README.md
370
+ ```
275
371
 
276
372
  ## Running
277
373
 
278
374
  ```bash
279
- npm test # Run tests
280
- npm run examples # Run examples
375
+ npm test # Run tests
376
+ npm run examples # Run examples
281
377
  ```
282
378
 
283
379
  ## License
284
380
 
285
381
  MIT
382
+
383
+ ## Links
384
+
385
+ - [GitHub](https://github.com/catpea/lulz)
386
+ - [Node-RED](https://nodered.org/)
387
+ - [RxJS](https://rxjs.dev/)
package/TODO.md ADDED
@@ -0,0 +1,132 @@
1
+ # lulz - TODO
2
+
3
+ ## RxJS Operators to Add Later
4
+
5
+ These operators are less commonly used but might be useful in specific scenarios.
6
+
7
+ ### Combination Operators
8
+
9
+ - [ ] `forkJoin` - Wait for all observables to complete, emit last values
10
+ - [ ] `race` - First to emit wins
11
+ - [ ] `combineAll` - Combine all inner observables
12
+ - [ ] `concatAll` - Concat all inner observables
13
+ - [ ] `mergeAll` - Merge all inner observables
14
+ - [ ] `switchAll` - Switch to latest inner observable
15
+ - [ ] `startWith` - Prepend values
16
+ - [ ] `endWith` - Append values
17
+
18
+ ### Transformation Operators
19
+
20
+ - [ ] `bufferCount` - Buffer by count
21
+ - [ ] `bufferTime` - Buffer by time
22
+ - [ ] `bufferToggle` - Buffer between open/close signals
23
+ - [ ] `bufferWhen` - Buffer until closing selector
24
+ - [ ] `concatMap` - Map and concat
25
+ - [ ] `exhaustMap` - Ignore new while processing
26
+ - [ ] `expand` - Recursively project
27
+ - [ ] `groupBy` - Group by key
28
+ - [ ] `mapTo` - Map to constant
29
+ - [ ] `mergeMap` / `flatMap` - Map and merge
30
+ - [ ] `mergeScan` - Scan with merge
31
+ - [ ] `partition` - Split into two streams
32
+ - [ ] `switchMap` - Switch to new on each emission
33
+ - [ ] `windowCount` - Window by count
34
+ - [ ] `windowTime` - Window by time
35
+ - [ ] `windowToggle` - Window between signals
36
+ - [ ] `windowWhen` - Window by closing selector
37
+
38
+ ### Filtering Operators
39
+
40
+ - [ ] `audit` - Ignore during duration selector
41
+ - [ ] `auditTime` - Ignore during time window
42
+ - [ ] `debounceTime` - (alias for debounce with time)
43
+ - [ ] `elementAt` - Emit only Nth element
44
+ - [ ] `first` - Emit first (or first matching)
45
+ - [ ] `ignoreElements` - Ignore all, only complete/error
46
+ - [ ] `last` - Emit last (or last matching)
47
+ - [ ] `sample` - Sample on signal
48
+ - [ ] `sampleTime` - Sample at interval
49
+ - [ ] `single` - Emit only if single match
50
+ - [ ] `skipLast` - Skip last N
51
+ - [ ] `skipUntil` - Skip until signal
52
+ - [ ] `takeLast` - Take last N
53
+ - [ ] `takeUntil` - Take until signal
54
+ - [ ] `throttleTime` - (alias for throttle)
55
+
56
+ ### Utility Operators
57
+
58
+ - [ ] `dematerialize` - Convert notification objects
59
+ - [ ] `materialize` - Wrap in notification objects
60
+ - [ ] `observeOn` - Schedule emissions
61
+ - [ ] `subscribeOn` - Schedule subscription
62
+ - [ ] `timeInterval` - Emit time between values
63
+ - [ ] `finalize` - Execute on complete/error
64
+ - [ ] `repeat` - Repeat N times
65
+ - [ ] `repeatWhen` - Repeat based on notifier
66
+
67
+ ### Error Handling
68
+
69
+ - [ ] `retryWhen` - Retry based on notifier
70
+ - [ ] `onErrorResumeNext` - Continue with next on error
71
+
72
+ ### Multicasting
73
+
74
+ - [ ] `multicast` - Share with subject
75
+ - [ ] `publish` - Share via publish subject
76
+ - [ ] `publishBehavior` - Share via behavior subject
77
+ - [ ] `publishLast` - Share via async subject
78
+ - [ ] `publishReplay` - Share via replay subject
79
+ - [ ] `refCount` - Auto connect/disconnect
80
+ - [ ] `shareReplay` - Share and replay
81
+
82
+ ## Node-RED Nodes to Add
83
+
84
+ ### Network
85
+
86
+ - [ ] `http in` - HTTP endpoint
87
+ - [ ] `http request` - HTTP client
88
+ - [ ] `http response` - HTTP response
89
+ - [ ] `websocket in` - WebSocket listener
90
+ - [ ] `websocket out` - WebSocket sender
91
+ - [ ] `tcp in` / `tcp out` / `tcp request` - TCP nodes
92
+ - [ ] `udp in` / `udp out` - UDP nodes
93
+ - [ ] `mqtt in` / `mqtt out` - MQTT nodes
94
+
95
+ ### Storage
96
+
97
+ - [ ] `file in` - Read file
98
+ - [ ] `file out` - Write file
99
+ - [ ] `watch` - Watch file/directory
100
+
101
+ ### Parsers
102
+
103
+ - [ ] `csv` - Parse/generate CSV
104
+ - [ ] `html` - Parse HTML
105
+ - [ ] `json` - Parse/stringify JSON
106
+ - [ ] `xml` - Parse/generate XML
107
+ - [ ] `yaml` - Parse/generate YAML
108
+
109
+ ### Sequence
110
+
111
+ - [ ] `batch` - Batch messages
112
+ - [ ] `sort` - Sort messages
113
+
114
+ ## Worker Enhancements
115
+
116
+ - [ ] Browser Web Worker support
117
+ - [ ] SharedArrayBuffer for zero-copy data transfer
118
+ - [ ] Worker pool auto-scaling
119
+ - [ ] Priority queue
120
+ - [ ] Task cancellation
121
+ - [ ] Progress reporting
122
+
123
+ ## Core Improvements
124
+
125
+ - [ ] Better TypeScript definitions
126
+ - [ ] Flow visualization / debugging
127
+ - [ ] Hot reloading of flows
128
+ - [ ] Persistence / checkpointing
129
+ - [ ] Metrics / monitoring
130
+ - [ ] Backpressure handling
131
+ - [ ] Dead letter queue for failed messages
132
+ - [ ] Circuit breaker pattern