lulz 1.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/CHICKEN.md +156 -0
- package/README.md +285 -0
- package/examples.js +317 -0
- package/index.js +21 -0
- package/package.json +20 -0
- package/src/flow.js +313 -0
- package/src/nodes.js +520 -0
- package/test.js +430 -0
package/CHICKEN.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
This is the original chiken scratch that started this project
|
|
2
|
+
|
|
3
|
+
```js
|
|
4
|
+
|
|
5
|
+
const fakeServer = new EventEmitter();
|
|
6
|
+
let counter = 1;
|
|
7
|
+
setInterval(()=>{
|
|
8
|
+
fakeServer.emit('fakeData', counter++);
|
|
9
|
+
},1_000)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// the actual program
|
|
13
|
+
|
|
14
|
+
const context = {
|
|
15
|
+
username: 'alice',
|
|
16
|
+
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const pipes = {};
|
|
20
|
+
|
|
21
|
+
// PROGRAM EXAMPLE
|
|
22
|
+
const blogBuilder = [
|
|
23
|
+
|
|
24
|
+
[ socket('post'), 'post' ], // monitor web socket for new post announcement
|
|
25
|
+
[ watch('assets'), 'asset' ], // watch assets folder for changes
|
|
26
|
+
|
|
27
|
+
[ 'post', log ], // log whan a new post arrives on post pipe
|
|
28
|
+
|
|
29
|
+
[ 'asset', assets ], // listen to asset pipe and call the asset funcion
|
|
30
|
+
[ 'post', cover, audio, post, 'updated' ], // listen to post pipe and call cover+aufio+post with same data (fan not series like in ffmpeg, series would requre array ['in', [a,b,c], 'out'], where a gets in, sends a out to b, b sends b out to c, and c sends to out pipe (or funcion if a funcion follows the array) this is a todo)
|
|
31
|
+
[ 'updated', pagerizer, log ],
|
|
32
|
+
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
const app = flow(blogBuilder, context);
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
// User Functions (DO NOT IMPLEMENT YET)
|
|
40
|
+
|
|
41
|
+
function log(options){ // outer declaration: allows user to configure the funcion in program array
|
|
42
|
+
|
|
43
|
+
return (send, packet) => { // inner funcion must be arrow as .arguments are used to set inner/outer apart
|
|
44
|
+
console.log( options, this.context, packet );
|
|
45
|
+
send(packet); /* send through */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// examples of producers
|
|
50
|
+
|
|
51
|
+
function socket(options){
|
|
52
|
+
return (send)=>{
|
|
53
|
+
fakeServer.on('fakeData', counter=>send(counter))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function watch(options){
|
|
58
|
+
return (send)=>{
|
|
59
|
+
fakeServer.on('fakeData', counter=>send(counter))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
// example sink/tranducer, just pass through
|
|
66
|
+
function assets(options){ return (send, packet)=>send(packet)}
|
|
67
|
+
function cover(options){ return (send, packet)=>send(packet)}
|
|
68
|
+
function audio(options){ return (send, packet)=>send(packet)}
|
|
69
|
+
function post(options){ return (send, packet)=>send(packet)}
|
|
70
|
+
function pagerizer(options){ return (send, packet)=>send(packet)}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
// System Functions
|
|
78
|
+
|
|
79
|
+
function flow(graph, context){
|
|
80
|
+
|
|
81
|
+
const directions = {from:0, to:2};
|
|
82
|
+
|
|
83
|
+
const [ inputs, transforms, outputs ] = graph.reduce((a, c)=>{
|
|
84
|
+
let direction = directions.from;
|
|
85
|
+
if(typeof c === 'string'){
|
|
86
|
+
a[direction].push(makePipe(c));
|
|
87
|
+
}else{
|
|
88
|
+
direction = directions.to; // flip direction
|
|
89
|
+
const isOuter = c?.arguments;
|
|
90
|
+
let fn;
|
|
91
|
+
if(isOuter){
|
|
92
|
+
// bind outer funcion
|
|
93
|
+
const bound = c.bind(context)
|
|
94
|
+
fn = bound({}); // apply empty config and retreive inner funcion
|
|
95
|
+
}else{
|
|
96
|
+
// already inner funcion
|
|
97
|
+
// set context to an already configured inner function
|
|
98
|
+
fn.context = context;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
a[1].push(makeNode(fn)); // store transform
|
|
103
|
+
|
|
104
|
+
}
|
|
105
|
+
},[[],[],[]]);
|
|
106
|
+
|
|
107
|
+
if(inputs.length){
|
|
108
|
+
for (const input of inputs){
|
|
109
|
+
for( const transform of transforms ){
|
|
110
|
+
input.connect(transform);
|
|
111
|
+
for (const output of outputs){
|
|
112
|
+
transform.connect(output);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}else{
|
|
117
|
+
for( const transform of transforms ){
|
|
118
|
+
input.connect(transform);
|
|
119
|
+
for (const output of outputs){
|
|
120
|
+
transform.connect(output);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
function makeNode(name, fn) {
|
|
136
|
+
const outputs = new Set()
|
|
137
|
+
function send(packet) {
|
|
138
|
+
for (const out of outputs) out(packet)
|
|
139
|
+
}
|
|
140
|
+
function input(packet) {
|
|
141
|
+
fn(send, packet)
|
|
142
|
+
}
|
|
143
|
+
input.connect = next => {
|
|
144
|
+
outputs.add(next)
|
|
145
|
+
return next
|
|
146
|
+
}
|
|
147
|
+
input.name = name
|
|
148
|
+
return input
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
makePipe(name){
|
|
152
|
+
if(!pipes[name]) pipes[name] = {type:'pipe', name, through: makeNode(through)};
|
|
153
|
+
return pipes[name];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# lulz
|
|
2
|
+
|
|
3
|
+
A reactive dataflow system inspired by FFmpeg filtergraph notation and Node-RED.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install lulz
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const { flow, Inject, Debug } = require('./index');
|
|
15
|
+
|
|
16
|
+
const app = flow([
|
|
17
|
+
[Inject({ payload: 'Hello World!' }), Debug({ name: 'output' })],
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
app.start();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Core Concepts
|
|
24
|
+
|
|
25
|
+
### Flow Syntax
|
|
26
|
+
|
|
27
|
+
Each line in a flow is an array: `[source, ...transforms, destination]`
|
|
28
|
+
|
|
29
|
+
- **Source**: A string (pipe name) or producer function
|
|
30
|
+
- **Transforms**: Functions that process packets
|
|
31
|
+
- **Destination**: A string (pipe name) or consumer function
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
const app = flow([
|
|
35
|
+
[producer, 'pipeName'], // Producer → pipe
|
|
36
|
+
['pipeName', transform, 'out'], // Pipe → transform → pipe
|
|
37
|
+
['out', consumer], // Pipe → consumer
|
|
38
|
+
]);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Function Types
|
|
42
|
+
|
|
43
|
+
**Outer functions** (factories) are regular functions that return inner functions:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
function myTransform(options) { // Outer - receives config
|
|
47
|
+
return (send, packet) => { // Inner - processes packets
|
|
48
|
+
send({ ...packet, modified: true });
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Inner functions** are arrow functions that do the actual processing:
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
const passthrough = (send, packet) => send(packet);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The system auto-detects which is which using `fn.hasOwnProperty('prototype')`:
|
|
60
|
+
- Regular functions have `prototype` → outer
|
|
61
|
+
- Arrow functions don't → inner
|
|
62
|
+
|
|
63
|
+
### Pre-configured vs Auto-configured
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
['input', myTransform, 'output'] // Auto-config: called with {}
|
|
67
|
+
['input', myTransform({ option: 1 }), 'output'] // Pre-configured
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Processing Modes
|
|
71
|
+
|
|
72
|
+
### Fan-out (Parallel)
|
|
73
|
+
|
|
74
|
+
All transforms receive the same packet simultaneously:
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
['input', transformA, transformB, transformC, 'output']
|
|
78
|
+
// ↓ ↓ ↓
|
|
79
|
+
// packet packet packet
|
|
80
|
+
// ↓ ↓ ↓
|
|
81
|
+
// └───────────┴───────────┘
|
|
82
|
+
// ↓
|
|
83
|
+
// output (receives 3 packets)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Series (Sequential)
|
|
87
|
+
|
|
88
|
+
Use `[a, b, c]` syntax for sequential processing:
|
|
89
|
+
|
|
90
|
+
```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)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Mixed
|
|
105
|
+
|
|
106
|
+
Combine both in a single line:
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
['input', validate, [enrich, format], notify, 'output']
|
|
110
|
+
// ↓ ↓ ↓
|
|
111
|
+
// fan-out series fan-out
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Built-in Nodes
|
|
115
|
+
|
|
116
|
+
### Inject
|
|
117
|
+
|
|
118
|
+
Produces packets on schedule or trigger:
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
Inject({
|
|
122
|
+
payload: 'hello', // or () => Date.now() for dynamic
|
|
123
|
+
topic: 'greeting',
|
|
124
|
+
once: true, // Emit once on start
|
|
125
|
+
onceDelay: 100, // Delay before first emit (ms)
|
|
126
|
+
interval: 1000, // Repeat interval (ms)
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Debug
|
|
131
|
+
|
|
132
|
+
Logs packets:
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
Debug({
|
|
136
|
+
name: 'my-debug',
|
|
137
|
+
active: true,
|
|
138
|
+
complete: false, // true = show full msg, false = payload only
|
|
139
|
+
logger: console.log, // Custom logger function
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Function
|
|
144
|
+
|
|
145
|
+
Execute custom JavaScript:
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
Function({
|
|
149
|
+
func: (msg, context) => {
|
|
150
|
+
return { ...msg, payload: msg.payload.toUpperCase() };
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Return `null` to drop the message.
|
|
156
|
+
|
|
157
|
+
### Change
|
|
158
|
+
|
|
159
|
+
Modify message properties:
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
Change({
|
|
163
|
+
rules: [
|
|
164
|
+
{ type: 'set', prop: 'payload', to: 'new value' },
|
|
165
|
+
{ type: 'set', prop: 'topic', to: (msg) => msg.payload.type },
|
|
166
|
+
{ type: 'change', prop: 'payload', from: /old/, to: 'new' },
|
|
167
|
+
{ type: 'delete', prop: 'unwanted' },
|
|
168
|
+
{ type: 'move', prop: 'payload', to: 'data' },
|
|
169
|
+
]
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Switch
|
|
174
|
+
|
|
175
|
+
Route messages based on conditions:
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
Switch({
|
|
179
|
+
property: 'payload',
|
|
180
|
+
rules: [
|
|
181
|
+
{ type: 'eq', value: 'hello' },
|
|
182
|
+
{ type: 'gt', value: 100 },
|
|
183
|
+
{ type: 'regex', value: /^test/ },
|
|
184
|
+
{ type: 'else' },
|
|
185
|
+
],
|
|
186
|
+
checkall: false, // Stop at first match
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Rule types: `eq`, `neq`, `lt`, `gt`, `lte`, `gte`, `regex`, `true`, `false`, `null`, `nnull`, `else`
|
|
191
|
+
|
|
192
|
+
### Template
|
|
193
|
+
|
|
194
|
+
Render mustache templates:
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
Template({
|
|
198
|
+
template: 'Hello {{name}}, you have {{count}} messages!',
|
|
199
|
+
field: 'payload', // Output field
|
|
200
|
+
})
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Delay
|
|
204
|
+
|
|
205
|
+
Delay or rate-limit messages:
|
|
206
|
+
|
|
207
|
+
```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
|
+
```
|
|
214
|
+
|
|
215
|
+
### Split
|
|
216
|
+
|
|
217
|
+
Split arrays/strings into sequences:
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
Split({
|
|
221
|
+
property: 'payload',
|
|
222
|
+
delimiter: ',', // For strings, or 'array' for arrays
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Join
|
|
227
|
+
|
|
228
|
+
Join sequences back together:
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
Join({
|
|
232
|
+
count: 5, // Emit after N messages
|
|
233
|
+
property: 'payload',
|
|
234
|
+
mode: 'manual', // 'auto', 'manual', 'reduce'
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Subflows
|
|
239
|
+
|
|
240
|
+
Create reusable flow components:
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
const sanitizer = subflow([
|
|
244
|
+
['in', Function({ func: (msg) => ({
|
|
245
|
+
...msg,
|
|
246
|
+
payload: String(msg.payload).trim().toLowerCase()
|
|
247
|
+
})}), 'out'],
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
// Wire into main flow
|
|
251
|
+
mainFlow.pipes['input'].connect(sanitizer._input);
|
|
252
|
+
sanitizer._output.connect(mainFlow.pipes['process']);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## API
|
|
256
|
+
|
|
257
|
+
### `flow(graph, context)`
|
|
258
|
+
|
|
259
|
+
Create a new flow from a graph definition.
|
|
260
|
+
|
|
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
|
|
267
|
+
|
|
268
|
+
### `subflow(graph, context)`
|
|
269
|
+
|
|
270
|
+
Create a subflow with `in` and `out` pipes.
|
|
271
|
+
|
|
272
|
+
### `compose(...flows)`
|
|
273
|
+
|
|
274
|
+
Connect multiple flows in sequence.
|
|
275
|
+
|
|
276
|
+
## Running
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
npm test # Run tests
|
|
280
|
+
npm run examples # Run examples
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
MIT
|