@brika/blocks-builtin 0.2.0 → 0.3.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.
@@ -161,23 +161,94 @@
161
161
  "description": "Final data"
162
162
  }
163
163
  }
164
+ },
165
+ "spark-receiver": {
166
+ "name": "Spark Receiver",
167
+ "description": "Receives typed spark events from plugins",
168
+ "ports": {
169
+ "out": {
170
+ "name": "Payload",
171
+ "description": "The spark event payload"
172
+ }
173
+ }
164
174
  }
165
175
  },
166
176
  "fields": {
167
- "field": "Field path to check",
168
- "operator": "Comparison operator",
169
- "value": "Value to compare against",
170
- "duration": "Wait duration",
171
- "message": "Message to log",
172
- "level": "Log level",
173
- "status": "End status",
174
- "template": "Output template",
175
- "interval": "Tick interval in milliseconds",
176
- "emitOnStart": "Emit an initial tick immediately",
177
- "url": "Request URL",
178
- "method": "HTTP method",
179
- "headers": "Request headers",
180
- "body": "Request body",
181
- "timeout": "Request timeout in milliseconds"
177
+ "field": {
178
+ "label": "Field",
179
+ "description": "Field path to check (e.g., \"value\", \"data.status\")"
180
+ },
181
+ "operator": {
182
+ "label": "Operator",
183
+ "description": "Comparison operator"
184
+ },
185
+ "value": {
186
+ "label": "Value",
187
+ "description": "Value to compare against"
188
+ },
189
+ "case1": {
190
+ "label": "Case 1",
191
+ "description": "Value for case 1"
192
+ },
193
+ "case2": {
194
+ "label": "Case 2",
195
+ "description": "Value for case 2"
196
+ },
197
+ "case3": {
198
+ "label": "Case 3",
199
+ "description": "Value for case 3"
200
+ },
201
+ "duration": {
202
+ "label": "Duration",
203
+ "description": "How long to wait"
204
+ },
205
+ "message": {
206
+ "label": "Message",
207
+ "description": "Message template with {{inputs.in.field}} expressions"
208
+ },
209
+ "level": {
210
+ "label": "Log Level",
211
+ "description": "Severity level for the log entry"
212
+ },
213
+ "status": {
214
+ "label": "Status",
215
+ "description": "End status of the workflow"
216
+ },
217
+ "template": {
218
+ "label": "Template",
219
+ "description": "Template to build output object"
220
+ },
221
+ "interval": {
222
+ "label": "Interval",
223
+ "description": "Time between ticks"
224
+ },
225
+ "emitOnStart": {
226
+ "label": "Emit on Start",
227
+ "description": "Emit an initial tick immediately when started"
228
+ },
229
+ "url": {
230
+ "label": "URL",
231
+ "description": "Request URL"
232
+ },
233
+ "method": {
234
+ "label": "Method",
235
+ "description": "HTTP method (GET, POST, etc.)"
236
+ },
237
+ "headers": {
238
+ "label": "Headers",
239
+ "description": "HTTP request headers"
240
+ },
241
+ "body": {
242
+ "label": "Body",
243
+ "description": "Request body (for POST/PUT/PATCH)"
244
+ },
245
+ "timeout": {
246
+ "label": "Timeout",
247
+ "description": "Request timeout"
248
+ },
249
+ "sparkType": {
250
+ "label": "Spark Type",
251
+ "description": "The spark type to listen for (e.g., timer:tick)"
252
+ }
182
253
  }
183
254
  }
@@ -3,181 +3,252 @@
3
3
  "description": "Blocs de workflow de base pour les automatisations BRIKA",
4
4
  "blocks": {
5
5
  "clock": {
6
- "name": "Horloge",
6
+ "name": "Clock",
7
7
  "description": "Émettre des ticks périodiques à un intervalle configurable",
8
8
  "ports": {
9
9
  "tick": {
10
10
  "name": "Tick",
11
- "description": "Émis à chaque intervalle avec compteur et horodatage"
11
+ "description": "Émis à chaque intervalle avec compteur et timestamp"
12
12
  }
13
13
  }
14
14
  },
15
15
  "http-request": {
16
- "name": "Requête HTTP",
16
+ "name": "HTTP Request",
17
17
  "description": "Effectuer des requêtes HTTP vers des APIs externes",
18
18
  "ports": {
19
19
  "trigger": {
20
- "name": "Déclencheur",
20
+ "name": "Trigger",
21
21
  "description": "Déclencher la requête HTTP"
22
22
  },
23
23
  "response": {
24
- "name": "Réponse",
25
- "description": "Réponse HTTP avec statut, en-têtes et corps"
24
+ "name": "Response",
25
+ "description": "Réponse HTTP avec status, headers et body"
26
26
  },
27
27
  "error": {
28
- "name": "Erreur",
28
+ "name": "Error",
29
29
  "description": "Détails de l'erreur si la requête échoue"
30
30
  }
31
31
  }
32
32
  },
33
33
  "condition": {
34
34
  "name": "Condition",
35
- "description": "Brancher selon une condition",
35
+ "description": "Brancher le flux selon une condition",
36
36
  "ports": {
37
37
  "in": {
38
- "name": "Entrée",
39
- "description": "Valeur à vérifier"
38
+ "name": "In",
39
+ "description": "Valeur à évaluer"
40
40
  },
41
41
  "then": {
42
- "name": "Alors",
42
+ "name": "Then",
43
43
  "description": "Sortie si la condition est vraie"
44
44
  },
45
45
  "else": {
46
- "name": "Sinon",
46
+ "name": "Else",
47
47
  "description": "Sortie si la condition est fausse"
48
48
  }
49
49
  }
50
50
  },
51
51
  "switch": {
52
- "name": "Aiguillage",
52
+ "name": "Switch",
53
53
  "description": "Branchement multiple selon une valeur",
54
54
  "ports": {
55
55
  "in": {
56
- "name": "Entrée",
57
- "description": "Valeur à vérifier"
56
+ "name": "In",
57
+ "description": "Valeur à évaluer"
58
58
  },
59
59
  "case1": {
60
- "name": "Cas 1",
60
+ "name": "Case 1",
61
61
  "description": "Première sortie"
62
62
  },
63
63
  "case2": {
64
- "name": "Cas 2",
64
+ "name": "Case 2",
65
65
  "description": "Deuxième sortie"
66
66
  },
67
67
  "case3": {
68
- "name": "Cas 3",
68
+ "name": "Case 3",
69
69
  "description": "Troisième sortie"
70
70
  },
71
71
  "default": {
72
- "name": "Défaut",
73
- "description": "Sortie si aucun cas ne correspond"
72
+ "name": "Default",
73
+ "description": "Sortie par défaut si aucun cas ne correspond"
74
74
  }
75
75
  }
76
76
  },
77
77
  "delay": {
78
- "name": "Délai",
79
- "description": "Attendre une durée spécifiée",
78
+ "name": "Delay",
79
+ "description": "Attendre une durée spécifiée avant de continuer",
80
80
  "ports": {
81
81
  "in": {
82
- "name": "Entrée",
82
+ "name": "In",
83
83
  "description": "Données à retarder"
84
84
  },
85
85
  "out": {
86
- "name": "Sortie",
87
- "description": "Données retardées"
86
+ "name": "Out",
87
+ "description": "Données après le délai"
88
88
  }
89
89
  }
90
90
  },
91
91
  "transform": {
92
- "name": "Transformer",
92
+ "name": "Transform",
93
93
  "description": "Transformer ou extraire des données",
94
94
  "ports": {
95
95
  "in": {
96
- "name": "Entrée",
97
- "description": "Données à transformer"
96
+ "name": "In",
97
+ "description": "Données d'entrée"
98
98
  },
99
99
  "out": {
100
- "name": "Sortie",
100
+ "name": "Out",
101
101
  "description": "Données transformées"
102
102
  }
103
103
  }
104
104
  },
105
105
  "log": {
106
- "name": "Journal",
107
- "description": "Enregistrer un message",
106
+ "name": "Log",
107
+ "description": "Enregistrer un message dans les logs",
108
108
  "ports": {
109
109
  "in": {
110
- "name": "Entrée",
111
- "description": "Données à enregistrer"
110
+ "name": "In",
111
+ "description": "Données à logger"
112
112
  },
113
113
  "out": {
114
- "name": "Sortie",
115
- "description": "Données passantes"
114
+ "name": "Out",
115
+ "description": "Données transmises sans modification"
116
116
  }
117
117
  }
118
118
  },
119
119
  "merge": {
120
- "name": "Fusionner",
121
- "description": "Attendre et fusionner plusieurs entrées",
120
+ "name": "Merge",
121
+ "description": "Attendre et combiner plusieurs entrées",
122
122
  "ports": {
123
123
  "a": {
124
- "name": "Entrée A",
124
+ "name": "Input A",
125
125
  "description": "Première entrée"
126
126
  },
127
127
  "b": {
128
- "name": "Entrée B",
128
+ "name": "Input B",
129
129
  "description": "Deuxième entrée"
130
130
  },
131
131
  "out": {
132
- "name": "Sortie",
132
+ "name": "Out",
133
133
  "description": "Entrées combinées"
134
134
  }
135
135
  }
136
136
  },
137
137
  "split": {
138
- "name": "Diviser",
139
- "description": "Diviser l'exécution en branches parallèles",
138
+ "name": "Split",
139
+ "description": "Diviser le flux en branches parallèles",
140
140
  "ports": {
141
141
  "in": {
142
- "name": "Entrée",
142
+ "name": "In",
143
143
  "description": "Données à diviser"
144
144
  },
145
145
  "a": {
146
- "name": "Branche A",
146
+ "name": "Branch A",
147
147
  "description": "Première branche"
148
148
  },
149
149
  "b": {
150
- "name": "Branche B",
150
+ "name": "Branch B",
151
151
  "description": "Deuxième branche"
152
152
  }
153
153
  }
154
154
  },
155
155
  "end": {
156
- "name": "Fin",
157
- "description": "Terminer le workflow",
156
+ "name": "End",
157
+ "description": "Terminer l'exécution du workflow",
158
158
  "ports": {
159
159
  "in": {
160
- "name": "Entrée",
160
+ "name": "In",
161
161
  "description": "Données finales"
162
162
  }
163
163
  }
164
+ },
165
+ "spark-receiver": {
166
+ "name": "Récepteur Spark",
167
+ "description": "Reçoit des événements spark typés des plugins",
168
+ "ports": {
169
+ "out": {
170
+ "name": "Payload",
171
+ "description": "Le payload de l'événement spark"
172
+ }
173
+ }
164
174
  }
165
175
  },
166
176
  "fields": {
167
- "field": "Chemin du champ à vérifier",
168
- "operator": "Opérateur de comparaison",
169
- "value": "Valeur à comparer",
170
- "duration": "Durée d'attente",
171
- "message": "Message à enregistrer",
172
- "level": "Niveau de journal",
173
- "status": "Statut de fin",
174
- "template": "Modèle de sortie",
175
- "interval": "Intervalle en millisecondes",
176
- "emitOnStart": "Émettre un tick initial immédiatement",
177
- "url": "URL de la requête",
178
- "method": "Méthode HTTP",
179
- "headers": "En-têtes de la requête",
180
- "body": "Corps de la requête",
181
- "timeout": "Délai d'expiration en millisecondes"
177
+ "field": {
178
+ "label": "Field",
179
+ "description": "Chemin du champ à évaluer (ex: \"value\", \"data.status\")"
180
+ },
181
+ "operator": {
182
+ "label": "Operator",
183
+ "description": "Opérateur de comparaison"
184
+ },
185
+ "value": {
186
+ "label": "Value",
187
+ "description": "Valeur à comparer"
188
+ },
189
+ "case1": {
190
+ "label": "Case 1",
191
+ "description": "Valeur pour le cas 1"
192
+ },
193
+ "case2": {
194
+ "label": "Case 2",
195
+ "description": "Valeur pour le cas 2"
196
+ },
197
+ "case3": {
198
+ "label": "Case 3",
199
+ "description": "Valeur pour le cas 3"
200
+ },
201
+ "duration": {
202
+ "label": "Duration",
203
+ "description": "Durée d'attente"
204
+ },
205
+ "message": {
206
+ "label": "Message",
207
+ "description": "Template du message avec expressions {{inputs.in.field}}"
208
+ },
209
+ "level": {
210
+ "label": "Level",
211
+ "description": "Niveau de log"
212
+ },
213
+ "status": {
214
+ "label": "Status",
215
+ "description": "Status de fin du workflow"
216
+ },
217
+ "template": {
218
+ "label": "Template",
219
+ "description": "Template pour construire l'objet de sortie"
220
+ },
221
+ "interval": {
222
+ "label": "Interval",
223
+ "description": "Temps entre les ticks"
224
+ },
225
+ "emitOnStart": {
226
+ "label": "Emit on Start",
227
+ "description": "Émettre un tick immédiatement au démarrage"
228
+ },
229
+ "url": {
230
+ "label": "URL",
231
+ "description": "URL de la requête"
232
+ },
233
+ "method": {
234
+ "label": "Method",
235
+ "description": "Méthode HTTP (GET, POST, PUT, PATCH, DELETE)"
236
+ },
237
+ "headers": {
238
+ "label": "Headers",
239
+ "description": "Headers HTTP de la requête"
240
+ },
241
+ "body": {
242
+ "label": "Body",
243
+ "description": "Corps de la requête (pour POST/PUT/PATCH)"
244
+ },
245
+ "timeout": {
246
+ "label": "Timeout",
247
+ "description": "Délai d'expiration de la requête"
248
+ },
249
+ "sparkType": {
250
+ "label": "Type de Spark",
251
+ "description": "Le type de spark à écouter (ex: timer:tick)"
252
+ }
182
253
  }
183
254
  }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "$schema": "https://schema.brika.dev/plugin.schema.json",
3
3
  "name": "@brika/blocks-builtin",
4
- "version": "0.2.0",
4
+ "displayName": "Core Blocks",
5
+ "version": "0.3.0",
5
6
  "description": "Core workflow blocks for BRIKA automations",
6
7
  "author": "BRIKA Team",
7
8
  "license": "MIT",
@@ -23,10 +24,11 @@
23
24
  "condition",
24
25
  "delay",
25
26
  "home-automation",
26
- "visual-programming"
27
+ "visual-programming",
28
+ "brika-plugin"
27
29
  ],
28
30
  "engines": {
29
- "brika": "^0.2.0"
31
+ "brika": "^0.3.0"
30
32
  },
31
33
  "type": "module",
32
34
  "main": "./src/main.ts",
@@ -42,7 +44,7 @@
42
44
  "scripts": {
43
45
  "link": "bun link",
44
46
  "tsc": "bunx --biome tsc --noEmit",
45
- "prepublishOnly": "bun run tsc"
47
+ "prepublishOnly": "brika-verify-plugin"
46
48
  },
47
49
  "blocks": [
48
50
  {
@@ -124,10 +126,18 @@
124
126
  "category": "action",
125
127
  "icon": "square",
126
128
  "color": "#dc2626"
129
+ },
130
+ {
131
+ "id": "spark-receiver",
132
+ "name": "Spark Receiver",
133
+ "description": "Receives typed spark events",
134
+ "category": "trigger",
135
+ "icon": "zap",
136
+ "color": "#f59e0b"
127
137
  }
128
138
  ],
129
139
  "dependencies": {
130
- "@brika/sdk": "0.1.1",
140
+ "@brika/sdk": "0.3.0",
131
141
  "zod": "^4.3.4"
132
142
  }
133
143
  }
package/src/main.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  log,
15
15
  map,
16
16
  output,
17
- type Serializable,
17
+ subscribeSpark,
18
18
  z,
19
19
  } from '@brika/sdk';
20
20
 
@@ -52,16 +52,25 @@ export const httpRequest = defineReactiveBlock(
52
52
  body: z.string().optional().describe('Request body (for POST/PUT/PATCH)'),
53
53
  }),
54
54
  },
55
- ({ inputs, outputs, config, log }) => {
55
+ ({ inputs, outputs, config }) => {
56
56
  inputs.trigger.on(async () => {
57
- log('debug', `HTTP ${config.method ?? 'GET'} ${config.url}`);
57
+ log.debug(`HTTP ${config.method ?? 'GET'} ${config.url}`);
58
58
  try {
59
59
  const res = await fetch(config.url, {
60
60
  method: config.method ?? 'GET',
61
61
  headers: config.headers,
62
62
  body: config.body,
63
63
  });
64
- const body = await res.json().catch(() => res.text());
64
+
65
+ // Read body as text first, then try to parse as JSON
66
+ const text = await res.text();
67
+ let body: unknown;
68
+ try {
69
+ body = JSON.parse(text);
70
+ } catch {
71
+ body = text;
72
+ }
73
+
65
74
  outputs.response.emit({
66
75
  status: res.status,
67
76
  statusText: res.statusText,
@@ -69,7 +78,7 @@ export const httpRequest = defineReactiveBlock(
69
78
  body,
70
79
  });
71
80
  } catch (err) {
72
- log('error', `HTTP request failed: ${err}`);
81
+ log.error(`HTTP request failed: ${err instanceof Error ? err.message : String(err)}`);
73
82
  outputs.error.emit({ message: String(err) });
74
83
  }
75
84
  });
@@ -92,8 +101,8 @@ export const condition = defineReactiveBlock(
92
101
  in: input(z.generic(), { name: 'Input' }),
93
102
  },
94
103
  outputs: {
95
- then: output(z.passthrough('in'), { name: 'Then' }),
96
- else: output(z.passthrough('in'), { name: 'Else' }),
104
+ pass: output(z.passthrough('in'), { name: 'Then' }),
105
+ fail: output(z.passthrough('in'), { name: 'Else' }),
97
106
  },
98
107
  config: z.object({
99
108
  field: z.string().describe('Field path to check (e.g., "value", "data.status")'),
@@ -103,16 +112,16 @@ export const condition = defineReactiveBlock(
103
112
  value: z.any().optional().describe('Value to compare against'),
104
113
  }),
105
114
  },
106
- ({ inputs, outputs, config, log }) => {
115
+ ({ inputs, outputs, config }) => {
107
116
  inputs.in.on((data) => {
108
117
  const fieldValue = getFieldValue(data, config.field);
109
118
  const result = evaluate(fieldValue, config.operator, config.value);
110
- log('debug', `Condition: ${config.field} ${config.operator} ${config.value} = ${result}`);
119
+ log.debug(`Condition: ${config.field} ${config.operator} ${JSON.stringify(config.value)} = ${result}`);
111
120
 
112
121
  if (result) {
113
- outputs.then.emit(data);
122
+ outputs.pass.emit(data);
114
123
  } else {
115
- outputs.else.emit(data);
124
+ outputs.fail.emit(data);
116
125
  }
117
126
  });
118
127
  }
@@ -181,10 +190,10 @@ export const switchBlock = defineReactiveBlock(
181
190
  case3: z.any().optional().describe('Value for case 3'),
182
191
  }),
183
192
  },
184
- ({ inputs, outputs, config, log }) => {
193
+ ({ inputs, outputs, config }) => {
185
194
  inputs.in.on((data) => {
186
195
  const value = getFieldValue(data, config.field);
187
- log('debug', `Switch value: ${JSON.stringify(value)}`);
196
+ log.debug(`Switch value: ${JSON.stringify(value)}`);
188
197
 
189
198
  if (value === config.case1) {
190
199
  outputs.case1.emit(data);
@@ -221,8 +230,8 @@ export const delay = defineReactiveBlock(
221
230
  duration: z.duration(undefined, 'Duration to wait'),
222
231
  }),
223
232
  },
224
- ({ inputs, outputs, config, log }) => {
225
- log('debug', `Delay configured: ${config.duration}ms`);
233
+ ({ inputs, outputs, config }) => {
234
+ log.debug(`Delay configured: ${config.duration}ms`);
226
235
 
227
236
  // Use delay operator to wait before emitting
228
237
  inputs.in.pipe(delayOp(config.duration)).to(outputs.out);
@@ -249,7 +258,7 @@ export const clock = defineReactiveBlock(
249
258
  interval: z.duration(undefined, 'Interval between ticks'),
250
259
  }),
251
260
  },
252
- ({ outputs, config, log, start }) => {
261
+ ({ outputs, config, start }) => {
253
262
  start(interval(config.interval))
254
263
  .pipe(
255
264
  map((count) => {
@@ -257,7 +266,7 @@ export const clock = defineReactiveBlock(
257
266
  })
258
267
  )
259
268
  .to(outputs.tick);
260
- log('info', `Clock started with interval: ${config.interval}ms`);
269
+ log.info(`Clock started with interval: ${config.interval}ms`);
261
270
  }
262
271
  );
263
272
 
@@ -284,7 +293,7 @@ export const transform = defineReactiveBlock(
284
293
  template: z.record(z.string(), z.string()).optional().describe('Template to build output'),
285
294
  }),
286
295
  },
287
- ({ inputs, outputs, config, log }) => {
296
+ ({ inputs, outputs, config }) => {
288
297
  inputs.in
289
298
  .pipe(
290
299
  map((data) => {
@@ -294,14 +303,14 @@ export const transform = defineReactiveBlock(
294
303
  for (const [key, path] of Object.entries(config.template)) {
295
304
  result[key] = getFieldValue(data, path);
296
305
  }
297
- log('debug', `Transformed with template: ${JSON.stringify(result)}`);
306
+ log.debug(`Transformed with template: ${JSON.stringify(result)}`);
298
307
  return result;
299
308
  }
300
309
 
301
310
  // If field is provided, extract it
302
311
  if (config.field) {
303
312
  const value = getFieldValue(data, config.field);
304
- log('debug', `Extracted field ${config.field}: ${JSON.stringify(value)}`);
313
+ log.debug(`Extracted field ${config.field}: ${JSON.stringify(value)}`);
305
314
  return value;
306
315
  }
307
316
 
@@ -327,7 +336,7 @@ function interpolate(
327
336
  template: string,
328
337
  context: { inputs: Record<string, unknown>; config: Record<string, unknown> }
329
338
  ): string {
330
- return template.replace(/\{\{([^}]+)\}\}/g, (_, expr: string) => {
339
+ return template.replaceAll(/\{\{([^{}]+)}}/g, (_, expr: string) => {
331
340
  const path = expr.trim().split('.');
332
341
  let value: unknown = context;
333
342
 
@@ -339,7 +348,8 @@ function interpolate(
339
348
 
340
349
  if (value === undefined || value === null) return '';
341
350
  if (typeof value === 'object') return JSON.stringify(value);
342
- return String(value);
351
+ if (typeof value === 'string') return value;
352
+ return JSON.stringify(value);
343
353
  });
344
354
  }
345
355
 
@@ -366,18 +376,18 @@ export const logBlock = defineReactiveBlock(
366
376
  level: z.enum(['debug', 'info', 'warn', 'error']).default('info').describe('Log level'),
367
377
  }),
368
378
  },
369
- ({ inputs, outputs, config, log }) => {
379
+ ({ inputs, outputs, config }) => {
370
380
  inputs.in.on((data) => {
371
- const context = {
381
+ const c = {
372
382
  inputs: { in: data },
373
- config: config as Record<string, unknown>,
383
+ config,
374
384
  };
375
385
 
376
386
  const message = config.message
377
- ? interpolate(config.message, context)
387
+ ? interpolate(config.message, c)
378
388
  : JSON.stringify(data);
379
389
 
380
- log(config.level, message);
390
+ log[config.level](message);
381
391
  outputs.out.emit(data);
382
392
  });
383
393
  }
@@ -410,12 +420,12 @@ export const merge = defineReactiveBlock(
410
420
  },
411
421
  config: z.object({}),
412
422
  },
413
- ({ inputs, outputs, log }) => {
423
+ ({ inputs, outputs }) => {
414
424
  // Combine waits for both inputs to have values
415
425
  combine(inputs.a, inputs.b)
416
426
  .pipe(
417
427
  map(([a, b]) => {
418
- log('debug', 'Merged inputs');
428
+ log.debug('Merged inputs');
419
429
  return { a, b };
420
430
  })
421
431
  )
@@ -444,9 +454,9 @@ export const split = defineReactiveBlock(
444
454
  },
445
455
  config: z.object({}),
446
456
  },
447
- ({ inputs, outputs, log }) => {
457
+ ({ inputs, outputs }) => {
448
458
  inputs.in.on((data) => {
449
- log('debug', 'Splitting to parallel branches');
459
+ log.debug('Splitting to parallel branches');
450
460
  outputs.a.emit(data);
451
461
  outputs.b.emit(data);
452
462
  });
@@ -473,14 +483,64 @@ export const end = defineReactiveBlock(
473
483
  status: z.enum(['success', 'failure']).default('success').describe('End status'),
474
484
  }),
475
485
  },
476
- ({ inputs, config, log }) => {
486
+ ({ inputs, config }) => {
477
487
  inputs.in.on((data) => {
478
- log('info', `Workflow ended with status: ${config.status}`);
479
- log('debug', `Final data: ${JSON.stringify(data)}`);
488
+ log.info(`Workflow ended with status: ${config.status}`);
489
+ log.debug(`Final data: ${JSON.stringify(data)}`);
480
490
  });
481
491
  }
482
492
  );
483
493
 
494
+ // ─────────────────────────────────────────────────────────────────────────────
495
+ // Spark Receiver Block - Receive typed events
496
+ // ─────────────────────────────────────────────────────────────────────────────
497
+
498
+ /**
499
+ * Spark Receiver Block
500
+ *
501
+ * This is a trigger block that receives typed spark events from the hub.
502
+ * The hub subscribes to the configured spark type and emits data directly
503
+ * to the output port via blockEmit IPC.
504
+ *
505
+ * Configuration:
506
+ * - sparkType: Full spark type to listen for (e.g., "timer:timer-started")
507
+ *
508
+ * The output type is resolved dynamically from the selected spark's schema
509
+ * using the z.resolved() type marker.
510
+ */
511
+ export const sparkReceiver = defineReactiveBlock(
512
+ {
513
+ id: 'spark-receiver',
514
+ name: 'Spark Receiver',
515
+ description: 'Receives typed spark events',
516
+ category: 'trigger',
517
+ icon: 'zap',
518
+ color: '#f59e0b',
519
+ inputs: {},
520
+ outputs: {
521
+ // Output type is resolved from spark's schema via config.sparkType
522
+ out: output(z.resolved('spark', 'sparkType'), { name: 'Payload' }),
523
+ },
524
+ config: z.object({
525
+ sparkType: z.sparkType('Spark type to listen for'),
526
+ }),
527
+ },
528
+ ({ config, outputs, start }) => {
529
+ if (!config.sparkType) {
530
+ log.warn('No spark type configured');
531
+ return;
532
+ }
533
+
534
+ log.info(`Subscribing to spark: ${config.sparkType}`);
535
+
536
+ // Subscribe to sparks and emit payload to output
537
+ // Cleanup is automatic when block stops (via flow system)
538
+ start(subscribeSpark(config.sparkType))
539
+ .pipe(map((event) => event.payload))
540
+ .to(outputs.out);
541
+ }
542
+ );
543
+
484
544
  // ─────────────────────────────────────────────────────────────────────────────
485
545
 
486
- log('info', 'Built-in blocks plugin loaded');
546
+ log.info('Built-in blocks plugin loaded');