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/examples.js ADDED
@@ -0,0 +1,317 @@
1
+ /**
2
+ * lulz Examples
3
+ *
4
+ * Various patterns and use cases
5
+ */
6
+
7
+ import {
8
+ flow,
9
+ subflow,
10
+ compose,
11
+ Inject,
12
+ Debug,
13
+ Function as Fn,
14
+ Change,
15
+ Switch,
16
+ Template,
17
+ Delay,
18
+ Join,
19
+ Split,
20
+ } from './index.js';
21
+
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
+
29
+ // ============================================================
30
+ // EXAMPLE 1: Basic pipe connection
31
+ // ============================================================
32
+
33
+ console.log('\n=== Example 1: Basic Inject → Debug ===\n');
34
+
35
+ const example1 = flow([
36
+ [Inject({ payload: 'Hello FlowGraph!', once: true }), Debug({ name: 'output', logger })],
37
+ ]);
38
+
39
+ example1.start();
40
+
41
+ // ============================================================
42
+ // EXAMPLE 2: Series processing with [a, b, c] syntax
43
+ // ============================================================
44
+
45
+ console.log('\n=== Example 2: Series Processing ===\n');
46
+
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
+ }
57
+
58
+ const example2 = flow([
59
+ ['input', [addStep('validate'), addStep('transform'), addStep('enrich')], Debug({ name: 'result', logger })],
60
+ ]);
61
+
62
+ example2.inject('input', { payload: { data: 'test' } });
63
+
64
+ // ============================================================
65
+ // EXAMPLE 3: Fan-out (parallel processing)
66
+ // ============================================================
67
+
68
+ console.log('\n=== Example 3: Fan-out Processing ===\n');
69
+
70
+ function processA(options) {
71
+ return (send, packet) => {
72
+ logger('[A] Fast processing');
73
+ send({ ...packet, processedBy: 'A' });
74
+ };
75
+ }
76
+
77
+ function processB(options) {
78
+ return (send, packet) => {
79
+ logger('[B] Detailed processing');
80
+ send({ ...packet, processedBy: 'B', extra: 'details' });
81
+ };
82
+ }
83
+
84
+ const example3 = flow([
85
+ ['input', processA, processB, 'output'], // Both receive same input
86
+ ['output', Debug({ name: 'fan-out-result', logger })],
87
+ ]);
88
+
89
+ example3.inject('input', { payload: 'parallel test' });
90
+
91
+ // ============================================================
92
+ // EXAMPLE 4: Mixed pre-configured and auto-configured functions
93
+ // ============================================================
94
+
95
+ console.log('\n=== Example 4: Pre-configured Functions ===\n');
96
+
97
+ function multiplier(options) {
98
+ const factor = options.factor || 1;
99
+ return (send, packet) => {
100
+ send({ ...packet, payload: packet.payload * factor });
101
+ };
102
+ }
103
+
104
+ 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 })],
110
+ ]);
111
+
112
+ example4.inject('input', { payload: 10 }); // 10 → 20 → 20 → 100
113
+
114
+ // ============================================================
115
+ // EXAMPLE 5: Conditional routing with Switch
116
+ // ============================================================
117
+
118
+ console.log('\n=== Example 5: Conditional Routing ===\n');
119
+
120
+ 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 })],
137
+ ]);
138
+
139
+ example5.inject('input', { payload: { temperature: 35, city: 'Phoenix' } });
140
+ example5.inject('input', { payload: { temperature: 15, city: 'Seattle' } });
141
+
142
+ // ============================================================
143
+ // EXAMPLE 6: Template rendering
144
+ // ============================================================
145
+
146
+ console.log('\n=== Example 6: Template Rendering ===\n');
147
+
148
+ const example6 = flow([
149
+ ['input', Template({
150
+ template: '{{payload.name}} from {{payload.city}} says: "{{payload.message}}"'
151
+ }), Debug({ name: 'message', logger })],
152
+ ]);
153
+
154
+ example6.inject('input', {
155
+ payload: {
156
+ name: 'Alice',
157
+ city: 'Wonderland',
158
+ message: 'Down the rabbit hole!'
159
+ }
160
+ });
161
+
162
+ // ============================================================
163
+ // EXAMPLE 7: Subflow embedding
164
+ // ============================================================
165
+
166
+ console.log('\n=== Example 7: Subflow (Reusable Component) ===\n');
167
+
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
+ ]);
177
+
178
+ // Use it in main flow
179
+ const example7 = flow([
180
+ ['input', 'process'],
181
+ ['process', Debug({ name: 'sanitized', logger })],
182
+ ]);
183
+
184
+ // Wire up subflow
185
+ example7.pipes['input'].connect(sanitizer._input);
186
+ sanitizer._output.connect(example7.pipes['process']);
187
+
188
+ example7.inject('input', { payload: ' HELLO WORLD ' });
189
+
190
+ // ============================================================
191
+ // EXAMPLE 8: Blog builder (original use case)
192
+ // ============================================================
193
+
194
+ console.log('\n=== Example 8: Blog Builder Pattern ===\n');
195
+
196
+ // Simulated producer functions
197
+ function socket(channel) {
198
+ return (send) => {
199
+ logger(`[socket] Listening on: ${channel}`);
200
+ // Simulate receiving data
201
+ setTimeout(() => {
202
+ send({ payload: { type: 'new-post', id: 123 }, channel });
203
+ }, 50);
204
+ };
205
+ }
206
+
207
+ function watch(folder) {
208
+ return (send) => {
209
+ logger(`[watch] Watching: ${folder}`);
210
+ // Simulate file change
211
+ setTimeout(() => {
212
+ send({ payload: { type: 'file-changed', path: `${folder}/image.png` }, folder });
213
+ }, 75);
214
+ };
215
+ }
216
+
217
+ // Processing functions
218
+ function cover(options) {
219
+ return (send, packet) => {
220
+ logger('[cover] Generating cover image...');
221
+ send({ ...packet, cover: true });
222
+ };
223
+ }
224
+
225
+ function audio(options) {
226
+ return (send, packet) => {
227
+ logger('[audio] Processing audio...');
228
+ send({ ...packet, audio: true });
229
+ };
230
+ }
231
+
232
+ function post(options) {
233
+ return (send, packet) => {
234
+ logger('[post] Building post...');
235
+ send({ ...packet, built: true });
236
+ };
237
+ }
238
+
239
+ function assets(options) {
240
+ return (send, packet) => {
241
+ logger('[assets] Processing asset:', packet.payload?.path);
242
+ send(packet);
243
+ };
244
+ }
245
+
246
+ function pagerizer(options) {
247
+ return (send, packet) => {
248
+ logger('[pagerizer] Updating pagination...');
249
+ send({ ...packet, paginated: true });
250
+ };
251
+ }
252
+
253
+ 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 })],
260
+ ], { username: 'alice' });
261
+
262
+ blogBuilder.start();
263
+
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
+
288
+ function enrichB(options) {
289
+ return (send, packet) => {
290
+ logger('[enrichB] Adding metadata B');
291
+ send({ ...packet, metaB: true });
292
+ };
293
+ }
294
+
295
+ function finalize(options) {
296
+ return (send, packet) => {
297
+ logger('[finalize] Completing...');
298
+ send({ ...packet, finalized: true });
299
+ };
300
+ }
301
+
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
307
+ ]);
308
+
309
+ example9.inject('input', { payload: { data: 'important' } });
310
+
311
+ // ============================================================
312
+ // Summary
313
+ // ============================================================
314
+
315
+ setTimeout(() => {
316
+ console.log('\n=== All Examples Complete ===\n');
317
+ }, 200);
package/index.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * lulz - A reactive dataflow system
3
+ *
4
+ * Inspired by FFmpeg filtergraph notation and Node-RED
5
+ */
6
+
7
+ export { flow, subflow, compose, makeNode, makePipe, isOuter, isInner } from './src/flow.js';
8
+ export {
9
+ Inject,
10
+ Debug,
11
+ Function,
12
+ Change,
13
+ Switch,
14
+ Template,
15
+ Delay,
16
+ Join,
17
+ Split,
18
+ getProperty,
19
+ setProperty,
20
+ deleteProperty,
21
+ } from './src/nodes.js';
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "lulz",
3
+ "version": "1.0.0",
4
+ "description": "A reactive dataflow system inspired by Visual Programming, FFmpeg filtergraph and Node-RED",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "test": "node --test",
9
+ "examples": "node examples.js"
10
+ },
11
+ "keywords": [
12
+ "dataflow",
13
+ "reactive",
14
+ "flow",
15
+ "pipeline",
16
+ "node-red"
17
+ ],
18
+ "author": "",
19
+ "license": "MIT"
20
+ }
package/src/flow.js ADDED
@@ -0,0 +1,313 @@
1
+ /**
2
+ * lulz - A reactive dataflow system inspired by FFmpeg filtergraph
3
+ *
4
+ * Syntax:
5
+ * [source, ...transforms, destination]
6
+ *
7
+ * Where:
8
+ * - source: string (pipe name) or producer function
9
+ * - transforms: functions (outer or inner) or arrays for series
10
+ * - destination: string (pipe name) or consumer function
11
+ *
12
+ * Series processing:
13
+ * ['in', [a, b, c], 'out'] - a→b→c in sequence
14
+ *
15
+ * Fan-out (parallel):
16
+ * ['in', a, b, c, 'out'] - all receive same packet, all send to out
17
+ */
18
+
19
+ // Distinguish outer (regular fn) from inner (arrow fn)
20
+ function isOuter(fn) {
21
+ return typeof fn === 'function' && fn.hasOwnProperty('prototype');
22
+ }
23
+
24
+ function isInner(fn) {
25
+ return typeof fn === 'function' && !fn.hasOwnProperty('prototype');
26
+ }
27
+
28
+ function isSeriesArray(item) {
29
+ return Array.isArray(item) && item.every(el => typeof el === 'function');
30
+ }
31
+
32
+ /**
33
+ * Create a processing node
34
+ */
35
+ function makeNode(name, fn) {
36
+ const outputs = new Set();
37
+
38
+ function send(packet) {
39
+ for (const out of outputs) {
40
+ out.receive(packet);
41
+ }
42
+ }
43
+
44
+ function receive(packet) {
45
+ fn(send, packet);
46
+ }
47
+
48
+ receive.connect = (next) => {
49
+ outputs.add(next);
50
+ return next;
51
+ };
52
+
53
+ receive.disconnect = (next) => {
54
+ outputs.delete(next);
55
+ };
56
+
57
+ receive.receive = receive;
58
+ Object.defineProperty(receive, 'name', { value: name, writable: false });
59
+ receive.outputs = outputs;
60
+ receive._isNode = true;
61
+
62
+ return receive;
63
+ }
64
+
65
+ /**
66
+ * Create a pass-through pipe
67
+ */
68
+ function makePipe(name) {
69
+ return makeNode(name, (send, packet) => send(packet));
70
+ }
71
+
72
+ /**
73
+ * Build a series chain: a→b→c
74
+ * Returns { input, output } nodes
75
+ */
76
+ function buildSeries(fns, context, prepareFunction) {
77
+ if (fns.length === 0) {
78
+ const passthrough = makeNode('passthrough', (send, packet) => send(packet));
79
+ return { input: passthrough, output: passthrough };
80
+ }
81
+
82
+ const nodes = fns.map((fn, i) => {
83
+ const inner = prepareFunction(fn);
84
+ return makeNode(fn.name || `series-${i}`, inner);
85
+ });
86
+
87
+ // Chain them: node[0] → node[1] → ... → node[n-1]
88
+ for (let i = 0; i < nodes.length - 1; i++) {
89
+ nodes[i].connect(nodes[i + 1]);
90
+ }
91
+
92
+ return {
93
+ input: nodes[0],
94
+ output: nodes[nodes.length - 1]
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Main flow builder
100
+ */
101
+ function flow(graph, context = {}) {
102
+ const pipes = {};
103
+ const nodes = [];
104
+ const producers = [];
105
+
106
+ function getPipe(name) {
107
+ if (!pipes[name]) {
108
+ pipes[name] = makePipe(name);
109
+ }
110
+ return pipes[name];
111
+ }
112
+
113
+ // Convert any function to a ready-to-use inner function
114
+ function prepareFunction(fn) {
115
+ if (isOuter(fn)) {
116
+ // Factory function: call with empty options, bind context
117
+ const bound = fn.bind({ context });
118
+ return bound({});
119
+ } else {
120
+ // Arrow (inner) function - wrap to provide context
121
+ return (send, packet) => {
122
+ // Create a this-like object for arrow functions via closure
123
+ fn(send, packet);
124
+ };
125
+ }
126
+ }
127
+
128
+ // Process each line in the graph
129
+ for (const line of graph) {
130
+ if (!Array.isArray(line) || line.length < 2) continue;
131
+
132
+ const elements = [...line];
133
+ const first = elements[0];
134
+ const last = elements[elements.length - 1];
135
+ const middle = elements.slice(1, -1);
136
+
137
+ // === SOURCE ===
138
+ let source;
139
+ if (typeof first === 'string') {
140
+ source = getPipe(first);
141
+ } else if (typeof first === 'function') {
142
+ // Producer function
143
+ const producerFn = isOuter(first) ? first.bind({ context })({}) : first;
144
+ const node = makeNode(first.name || 'producer', (send, packet) => send(packet));
145
+
146
+ // Store producer for later activation
147
+ producers.push({ fn: producerFn, node });
148
+ source = node;
149
+ } else if (first?._isFlow) {
150
+ // Subflow as source
151
+ source = first._output || getPipe('__subflow_out__');
152
+ }
153
+
154
+ // === DESTINATION ===
155
+ let dest;
156
+ if (typeof last === 'string') {
157
+ dest = getPipe(last);
158
+ } else if (typeof last === 'function') {
159
+ const inner = prepareFunction(last);
160
+ dest = makeNode(last.name || 'sink', inner);
161
+ nodes.push(dest);
162
+ } else if (last?._isFlow) {
163
+ // Subflow as destination
164
+ dest = last._input || getPipe('__subflow_in__');
165
+ }
166
+
167
+ // === MIDDLE (transforms) ===
168
+ if (middle.length === 0) {
169
+ // Direct connection
170
+ source.connect(dest);
171
+ } else {
172
+ // Check if we have series arrays or fan-out
173
+ const hasSeries = middle.some(isSeriesArray);
174
+
175
+ if (middle.length === 1 && isSeriesArray(middle[0])) {
176
+ // Pure series: ['in', [a, b, c], 'out']
177
+ const { input, output } = buildSeries(middle[0], context, prepareFunction);
178
+ source.connect(input);
179
+ output.connect(dest);
180
+ nodes.push(input);
181
+ } else {
182
+ // Fan-out or mixed
183
+ for (const item of middle) {
184
+ if (isSeriesArray(item)) {
185
+ // Series within fan-out: ['in', a, [b, c, d], e, 'out']
186
+ const { input, output } = buildSeries(item, context, prepareFunction);
187
+ source.connect(input);
188
+ output.connect(dest);
189
+ nodes.push(input);
190
+ } else if (typeof item === 'function') {
191
+ // Regular transform in fan-out
192
+ const inner = prepareFunction(item);
193
+ const node = makeNode(item.name || 'transform', inner);
194
+ source.connect(node);
195
+ node.connect(dest);
196
+ nodes.push(node);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ // Create the flow object
204
+ const flowObj = {
205
+ pipes,
206
+ nodes,
207
+ producers,
208
+ _isFlow: true,
209
+
210
+ // Start all producers
211
+ start() {
212
+ for (const { fn, node } of producers) {
213
+ fn((packet) => node.receive(packet));
214
+ }
215
+ return this;
216
+ },
217
+
218
+ // Stop all producers (if they return cleanup functions)
219
+ stop() {
220
+ // TODO: implement cleanup
221
+ return this;
222
+ },
223
+
224
+ // Inject a packet into a pipe
225
+ inject(pipeName, packet) {
226
+ const pipe = pipes[pipeName];
227
+ if (pipe) {
228
+ pipe.receive(packet);
229
+ } else {
230
+ console.warn(`Pipe "${pipeName}" not found`);
231
+ }
232
+ return this;
233
+ },
234
+
235
+ // Get a pipe for external connection
236
+ getPipe(name) {
237
+ return getPipe(name);
238
+ },
239
+
240
+ // For subflow embedding
241
+ _input: pipes['in'],
242
+ _output: pipes['out'],
243
+ };
244
+
245
+ return flowObj;
246
+ }
247
+
248
+ /**
249
+ * Create a subflow that can be embedded in other flows
250
+ */
251
+ function subflow(graph, context = {}) {
252
+ // Ensure the subflow has 'in' and 'out' pipes
253
+ const flowObj = flow(graph, context);
254
+
255
+ // Mark as subflow
256
+ flowObj._isSubflow = true;
257
+ flowObj._input = flowObj.getPipe('in');
258
+ flowObj._output = flowObj.getPipe('out');
259
+
260
+ return flowObj;
261
+ }
262
+
263
+ /**
264
+ * Compose multiple flows
265
+ */
266
+ function compose(...flows) {
267
+ // Connect flows in sequence
268
+ for (let i = 0; i < flows.length - 1; i++) {
269
+ const current = flows[i];
270
+ const next = flows[i + 1];
271
+
272
+ if (current._output && next._input) {
273
+ current._output.connect(next._input);
274
+ }
275
+ }
276
+
277
+ return {
278
+ _isFlow: true,
279
+ _isComposed: true,
280
+ flows,
281
+ _input: flows[0]?._input,
282
+ _output: flows[flows.length - 1]?._output,
283
+
284
+ start() {
285
+ for (const f of flows) {
286
+ if (f.start) f.start();
287
+ }
288
+ return this;
289
+ },
290
+
291
+ stop() {
292
+ for (const f of flows) {
293
+ if (f.stop) f.stop();
294
+ }
295
+ return this;
296
+ },
297
+
298
+ inject(pipeName, packet) {
299
+ flows[0]?.inject(pipeName, packet);
300
+ return this;
301
+ }
302
+ };
303
+ }
304
+
305
+ export {
306
+ flow,
307
+ subflow,
308
+ compose,
309
+ makeNode,
310
+ makePipe,
311
+ isOuter,
312
+ isInner,
313
+ };