@fugood/bricks-ctor 2.25.0-beta.51 → 2.25.0-beta.53

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.
@@ -171,6 +171,50 @@ describe('compile animations', () => {
171
171
  })
172
172
  })
173
173
 
174
+ describe('compile error collection', () => {
175
+ test('reports every entity error in a single failure instead of bailing on the first', async () => {
176
+ const app = makeApp()
177
+ // Invalid ids across three independent entity loops — a fail-fast compile would only
178
+ // surface the first; collection must report all three in one run.
179
+ app.rootSubspace.bricks = [{ id: 'bad-brick', templateKey: 'BRICK_TEXT' }]
180
+ app.rootSubspace.generators = [{ id: 'bad-generator', templateKey: 'GENERATOR_HTTP' }]
181
+ app.rootSubspace.data = [{ __typename: 'Data', id: 'bad-data', type: 'string' }]
182
+
183
+ let error
184
+ try {
185
+ await compile(app)
186
+ } catch (caught) {
187
+ error = caught
188
+ }
189
+
190
+ expect(error).toBeInstanceOf(Error)
191
+ expect(error.message).toContain('Compile failed with 3 error(s)')
192
+ expect(error.message).toContain('Invalid BRICK id (brick index: 0')
193
+ expect(error.message).toContain('Invalid GENERATOR id (generator index: 0')
194
+ expect(error.message).toContain('Invalid PROPERTY_BANK_DATA_NODE id (data index: 0')
195
+ })
196
+
197
+ test('skipping a bad entity does not block compiling its siblings', async () => {
198
+ const app = makeApp()
199
+ app.rootSubspace.data = [
200
+ { __typename: 'Data', id: 'bad-data', type: 'string' },
201
+ makeData(undefined), // valid DATA_ID sibling
202
+ ]
203
+
204
+ let error
205
+ try {
206
+ await compile(app)
207
+ } catch (caught) {
208
+ error = caught
209
+ }
210
+
211
+ // Only the bad sibling is reported; the valid one compiled past it in the same pass.
212
+ expect(error.message).toContain('Compile failed with 1 error(s)')
213
+ expect(error.message).toContain('bad-data')
214
+ expect(error.message).not.toContain(DATA_ID)
215
+ })
216
+ })
217
+
174
218
  describe('compile event handlers', () => {
175
219
  const BRICK_ID = 'BRICK_00000000-0000-0000-0000-000000000002'
176
220
 
package/compile/index.ts CHANGED
@@ -75,6 +75,31 @@ const assertEntryId = (
75
75
  return id
76
76
  }
77
77
 
78
+ // Per-compile error collection. Instead of throwing on the first bad entity, record the
79
+ // message and skip that entity, so a single compile surfaces every entity's first error
80
+ // (e.g. an invalid brick id, generator id and data-calc id all in one run). `errors` is
81
+ // local to each compile() call — no shared state — and compile throws an aggregated error
82
+ // at the end when any were collected, so a failed compile still rejects and never returns
83
+ // or writes a partial config.
84
+ const collect = <T>(errors: string[], fn: () => T, fallback: T): T => {
85
+ try {
86
+ return fn()
87
+ } catch (error) {
88
+ errors.push(error instanceof Error ? error.message : String(error))
89
+ return fallback
90
+ }
91
+ }
92
+
93
+ // reduce() into an id-keyed map, recording a per-item compile error and skipping that item
94
+ // instead of aborting the whole compile. The callback is the last argument (and the init is
95
+ // always {}) so the formatter keeps its body inline rather than re-indenting it.
96
+ const collectReduce = <T>(
97
+ errors: string[],
98
+ items: T[],
99
+ fn: (acc: Record<string, unknown>, item: T, index: number) => Record<string, unknown>,
100
+ ): Record<string, unknown> =>
101
+ items.reduce((acc, item, index) => collect(errors, () => fn(acc, item, index), acc), {})
102
+
78
103
  const compileProperty = (property, errorReference: string, result = {}) => {
79
104
  if (Array.isArray(property)) {
80
105
  return property.map((p) => compileProperty(p, errorReference))
@@ -744,13 +769,20 @@ const recordConfigChange = async (previousConfig: unknown, config: unknown) => {
744
769
 
745
770
  export const compile = async (app: Application) => {
746
771
  await new Promise((resolve) => setImmediate(resolve, 0))
772
+ // Collected entity-level compile errors (see collect/collectReduce). Aggregated and
773
+ // thrown at the end so one compile reports every entity's first error.
774
+ const errors: string[] = []
747
775
  // Snapshot the prior build artifact before the caller's compile.ts overwrites it, so
748
776
  // the config change introduced by this compile can be recorded on return.
749
777
  const previousConfig = await readBuildConfig(process.cwd())
750
778
  const timestamp = Date.now()
751
779
  // Pre-index subspace ids so the canvas-item validation below stays O(1).
752
780
  const subspaceIdSet = new Set(app.subspaces.map((s) => s.id))
753
- const compiledAutomationMap = app.automationMap ? compileAutomation(app.automationMap) : null
781
+ let compiledAutomationMap: ReturnType<typeof compileAutomation> | null = null
782
+ if (app.automationMap) {
783
+ const { automationMap } = app
784
+ compiledAutomationMap = collect(errors, () => compileAutomation(automationMap), null)
785
+ }
754
786
  const config = {
755
787
  title: `${app.name || 'Unknown'}(${timestamp})`,
756
788
  subspace_map: app.subspaces.reduce((subspaceMap, subspace, subspaceIndex) => {
@@ -836,74 +868,78 @@ export const compile = async (app: Application) => {
836
868
  change_canvas: subspace.localSyncChangeCanvas,
837
869
  }
838
870
  : undefined,
839
- animation_map: subspace.animations.reduce((map, animation, animationIndex) => {
840
- const animationId = assertEntryId(
841
- animation?.id,
842
- 'ANIMATION',
843
- `(animation index: ${animationIndex}, subspace: ${subspaceId})`,
844
- )
871
+ animation_map: collectReduce(
872
+ errors,
873
+ subspace.animations,
874
+ (map, animation, animationIndex) => {
875
+ const animationId = assertEntryId(
876
+ animation?.id,
877
+ 'ANIMATION',
878
+ `(animation index: ${animationIndex}, subspace: ${subspaceId})`,
879
+ )
845
880
 
846
- const animationTypename = animation.__typename
847
- if (animationTypename === 'Animation') {
848
- const animationDef = animation as AnimationDef
849
- const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
850
- const animationWarningMetadata = {
851
- animationIndex,
852
- animationTitle: animationDef.title,
853
- animationAlias: animationDef.alias,
854
- animationProperty: animationDef.property,
855
- subspaceIndex,
856
- subspaceTitle: subspace.title,
857
- }
858
- const animationType = getAnimationType(animationDef.config, animationErrorReference)
859
- map[animationId] = {
860
- alias: animationDef.alias,
861
- title: animationDef.title,
862
- description: animationDef.description,
863
- hide_short_ref: animationDef.hideShortRef,
864
- animationRunType: animationDef.runType,
865
- property: assertAnimationProperty(animationDef.property, animationErrorReference),
866
- type: animationType,
867
- config: compileAnimationConfig(
868
- animationType,
869
- animationDef.config,
870
- animationErrorReference,
871
- animationWarningMetadata,
872
- ),
873
- }
874
- } else if (animationTypename === 'AnimationCompose') {
875
- const animationDef = animation as AnimationComposeDef
876
- const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
877
- map[animationId] = {
878
- alias: animationDef.alias,
879
- title: animationDef.title,
880
- description: animationDef.description,
881
- hide_short_ref: animationDef.hideShortRef,
882
- animationRunType: animationDef.runType,
883
- compose_type: assertAnimationComposeType(
884
- animationDef.composeType,
885
- animationErrorReference,
886
- ),
887
- item_list: animationDef.items.map((item, index) => {
888
- const innerAnimation = item()
889
- const innerAnimationId = assertEntryId(
890
- innerAnimation?.id,
891
- 'ANIMATION',
892
- `(animation item index: ${index}, animation: ${animationId}, subspace ${subspaceId})`,
893
- )
894
- return { animation_id: innerAnimationId }
895
- }),
881
+ const animationTypename = animation.__typename
882
+ if (animationTypename === 'Animation') {
883
+ const animationDef = animation as AnimationDef
884
+ const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
885
+ const animationWarningMetadata = {
886
+ animationIndex,
887
+ animationTitle: animationDef.title,
888
+ animationAlias: animationDef.alias,
889
+ animationProperty: animationDef.property,
890
+ subspaceIndex,
891
+ subspaceTitle: subspace.title,
892
+ }
893
+ const animationType = getAnimationType(animationDef.config, animationErrorReference)
894
+ map[animationId] = {
895
+ alias: animationDef.alias,
896
+ title: animationDef.title,
897
+ description: animationDef.description,
898
+ hide_short_ref: animationDef.hideShortRef,
899
+ animationRunType: animationDef.runType,
900
+ property: assertAnimationProperty(animationDef.property, animationErrorReference),
901
+ type: animationType,
902
+ config: compileAnimationConfig(
903
+ animationType,
904
+ animationDef.config,
905
+ animationErrorReference,
906
+ animationWarningMetadata,
907
+ ),
908
+ }
909
+ } else if (animationTypename === 'AnimationCompose') {
910
+ const animationDef = animation as AnimationComposeDef
911
+ const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
912
+ map[animationId] = {
913
+ alias: animationDef.alias,
914
+ title: animationDef.title,
915
+ description: animationDef.description,
916
+ hide_short_ref: animationDef.hideShortRef,
917
+ animationRunType: animationDef.runType,
918
+ compose_type: assertAnimationComposeType(
919
+ animationDef.composeType,
920
+ animationErrorReference,
921
+ ),
922
+ item_list: animationDef.items.map((item, index) => {
923
+ const innerAnimation = item()
924
+ const innerAnimationId = assertEntryId(
925
+ innerAnimation?.id,
926
+ 'ANIMATION',
927
+ `(animation item index: ${index}, animation: ${animationId}, subspace ${subspaceId})`,
928
+ )
929
+ return { animation_id: innerAnimationId }
930
+ }),
931
+ }
932
+ } else {
933
+ throw new Error(
934
+ `Invalid animation typename (animation: ${animationId}, subspace ${subspaceId}): ${String(
935
+ animationTypename,
936
+ )}`,
937
+ )
896
938
  }
897
- } else {
898
- throw new Error(
899
- `Invalid animation typename (animation: ${animationId}, subspace ${subspaceId}): ${String(
900
- animationTypename,
901
- )}`,
902
- )
903
- }
904
- return map
905
- }, {}),
906
- brick_map: subspace.bricks.reduce((map, brick, brickIndex) => {
939
+ return map
940
+ },
941
+ ),
942
+ brick_map: collectReduce(errors, subspace.bricks, (map, brick, brickIndex) => {
907
943
  const brickId = assertEntryId(
908
944
  brick.id,
909
945
  'BRICK',
@@ -1068,9 +1104,9 @@ export const compile = async (app: Application) => {
1068
1104
  }, {}),
1069
1105
  }
1070
1106
  return map
1071
- }, {}),
1107
+ }),
1072
1108
  root_canvas_id: rootCanvasId,
1073
- canvas_map: subspace.canvases.reduce((map, canvas, canvasIndex) => {
1109
+ canvas_map: collectReduce(errors, subspace.canvases, (map, canvas, canvasIndex) => {
1074
1110
  const canvasId = assertEntryId(
1075
1111
  canvas.id,
1076
1112
  'CANVAS',
@@ -1150,81 +1186,85 @@ export const compile = async (app: Application) => {
1150
1186
  }),
1151
1187
  }
1152
1188
  return map
1153
- }, {}),
1154
- generator_map: subspace.generators.reduce((map, generator, generatorIndex) => {
1155
- const generatorId = assertEntryId(
1156
- generator.id,
1157
- 'GENERATOR',
1158
- `(generator index: ${generatorIndex}, subspace: ${subspaceId})`,
1159
- )
1189
+ }),
1190
+ generator_map: collectReduce(
1191
+ errors,
1192
+ subspace.generators,
1193
+ (map, generator, generatorIndex) => {
1194
+ const generatorId = assertEntryId(
1195
+ generator.id,
1196
+ 'GENERATOR',
1197
+ `(generator index: ${generatorIndex}, subspace: ${subspaceId})`,
1198
+ )
1160
1199
 
1161
- map[generatorId] = {
1162
- template_key: generator.templateKey,
1163
- alias: generator.alias,
1164
- title: generator.title,
1165
- description: generator.description,
1166
- hide_short_ref: generator.hideShortRef,
1167
- local_sync: generator.localSyncRunMode
1168
- ? {
1169
- run_mode: generator.localSyncRunMode,
1170
- }
1171
- : undefined,
1172
- property: compileProperty(
1173
- generator.property || {},
1174
- `(generator: ${generatorId}, subspace ${subspaceId})`,
1175
- ),
1176
- event_map: compileEvents(generator.templateKey, generator.events || {}, {
1177
- camelCase: false,
1178
- errorReference: `(generator: ${generatorId}, subspace ${subspaceId})`,
1179
- }),
1180
- outlet: compileOutlets(
1181
- generator.templateKey,
1182
- generator.outlets || {},
1183
- `(generator: ${generatorId}, subspace ${subspaceId})`,
1184
- ),
1185
- state_group: generator.switches?.reduce((acc, switchCase, switchIndex) => {
1186
- const switchId = assertEntryId(
1187
- switchCase.id,
1188
- 'BRICK_STATE_GROUP',
1189
- `(generator: ${generatorId}, switch index: ${switchIndex}, subspace ${subspaceId})`,
1190
- )
1200
+ map[generatorId] = {
1201
+ template_key: generator.templateKey,
1202
+ alias: generator.alias,
1203
+ title: generator.title,
1204
+ description: generator.description,
1205
+ hide_short_ref: generator.hideShortRef,
1206
+ local_sync: generator.localSyncRunMode
1207
+ ? {
1208
+ run_mode: generator.localSyncRunMode,
1209
+ }
1210
+ : undefined,
1211
+ property: compileProperty(
1212
+ generator.property || {},
1213
+ `(generator: ${generatorId}, subspace ${subspaceId})`,
1214
+ ),
1215
+ event_map: compileEvents(generator.templateKey, generator.events || {}, {
1216
+ camelCase: false,
1217
+ errorReference: `(generator: ${generatorId}, subspace ${subspaceId})`,
1218
+ }),
1219
+ outlet: compileOutlets(
1220
+ generator.templateKey,
1221
+ generator.outlets || {},
1222
+ `(generator: ${generatorId}, subspace ${subspaceId})`,
1223
+ ),
1224
+ state_group: generator.switches?.reduce((acc, switchCase, switchIndex) => {
1225
+ const switchId = assertEntryId(
1226
+ switchCase.id,
1227
+ 'BRICK_STATE_GROUP',
1228
+ `(generator: ${generatorId}, switch index: ${switchIndex}, subspace ${subspaceId})`,
1229
+ )
1191
1230
 
1192
- acc[switchId] = {
1193
- title: switchCase.title,
1194
- description: switchCase.description,
1195
- break: switchCase.break,
1196
- override: switchCase.override,
1197
- commented: switchCase.disabled,
1198
- conds: compileSwitchConds(
1199
- generator.templateKey,
1200
- switchCase.conds || [],
1201
- `(generator: ${generatorId}, switch: ${switchId}, subspace ${subspaceId})`,
1202
- ),
1203
- property: compileProperty(
1204
- switchCase.property,
1205
- `(generator: ${generatorId}, switch: ${switchId}, subspace ${subspaceId})`,
1206
- ),
1207
- outlet: compileOutlets(
1208
- generator.templateKey,
1209
- switchCase.outlets || {},
1210
- `(generator: ${generatorId}, switch: ${switchId}, subspace ${subspaceId})`,
1211
- ),
1212
- event_map: compileEvents(generator.templateKey, switchCase.events || {}, {
1213
- camelCase: false,
1214
- errorReference: `(generator: ${generatorId}, switch: ${switchId}, subspace ${subspaceId})`,
1215
- }),
1216
- animation: compileAnimations(
1217
- generator.templateKey,
1218
- switchCase.animation || {},
1219
- `(generator: ${generatorId}, switch: ${switchId}, subspace ${subspaceId})`,
1220
- ),
1221
- }
1222
- return acc
1223
- }, {}),
1224
- }
1225
- return map
1226
- }, {}),
1227
- property_bank_map: subspace.data.reduce((map, data, dataIndex) => {
1231
+ acc[switchId] = {
1232
+ title: switchCase.title,
1233
+ description: switchCase.description,
1234
+ break: switchCase.break,
1235
+ override: switchCase.override,
1236
+ commented: switchCase.disabled,
1237
+ conds: compileSwitchConds(
1238
+ generator.templateKey,
1239
+ switchCase.conds || [],
1240
+ `(generator: ${generatorId}, switch: ${switchId}, subspace ${subspaceId})`,
1241
+ ),
1242
+ property: compileProperty(
1243
+ switchCase.property,
1244
+ `(generator: ${generatorId}, switch: ${switchId}, subspace ${subspaceId})`,
1245
+ ),
1246
+ outlet: compileOutlets(
1247
+ generator.templateKey,
1248
+ switchCase.outlets || {},
1249
+ `(generator: ${generatorId}, switch: ${switchId}, subspace ${subspaceId})`,
1250
+ ),
1251
+ event_map: compileEvents(generator.templateKey, switchCase.events || {}, {
1252
+ camelCase: false,
1253
+ errorReference: `(generator: ${generatorId}, switch: ${switchId}, subspace ${subspaceId})`,
1254
+ }),
1255
+ animation: compileAnimations(
1256
+ generator.templateKey,
1257
+ switchCase.animation || {},
1258
+ `(generator: ${generatorId}, switch: ${switchId}, subspace ${subspaceId})`,
1259
+ ),
1260
+ }
1261
+ return acc
1262
+ }, {}),
1263
+ }
1264
+ return map
1265
+ },
1266
+ ),
1267
+ property_bank_map: collectReduce(errors, subspace.data, (map, data, dataIndex) => {
1228
1268
  const dataId = assertEntryId(
1229
1269
  data.id,
1230
1270
  'PROPERTY_BANK_DATA_NODE',
@@ -1258,225 +1298,229 @@ export const compile = async (app: Application) => {
1258
1298
  hit_regex: data.hit_regex,
1259
1299
  }
1260
1300
  return map
1261
- }, {}),
1262
- property_bank_calc_map: subspace.dataCalculation.reduce((map, dataCalc, dataCalcIndex) => {
1263
- const dataCalcId = assertEntryId(
1264
- dataCalc.id,
1265
- 'PROPERTY_BANK_COMMAND_MAP',
1266
- `(data calc index: ${dataCalcIndex}, subspace: ${subspaceId})`,
1267
- )
1301
+ }),
1302
+ property_bank_calc_map: collectReduce(
1303
+ errors,
1304
+ subspace.dataCalculation,
1305
+ (map, dataCalc, dataCalcIndex) => {
1306
+ const dataCalcId = assertEntryId(
1307
+ dataCalc.id,
1308
+ 'PROPERTY_BANK_COMMAND_MAP',
1309
+ `(data calc index: ${dataCalcIndex}, subspace: ${subspaceId})`,
1310
+ )
1268
1311
 
1269
- const calc: any = {
1270
- alias: dataCalc.alias,
1271
- title: dataCalc.title,
1272
- description: dataCalc.description,
1273
- hide_short_ref: dataCalc.hideShortRef,
1274
- }
1275
- if (dataCalc.triggerMode) calc.trigger_type = dataCalc.triggerMode
1276
- if (dataCalc.__typename === 'DataCalculationMap') {
1277
- calc.type = 'general'
1278
- const mapCalc = dataCalc as DataCalculationMap
1279
-
1280
- const getNodeId = (
1281
- node: DataCalculationData | DataCommand,
1282
- reference = '',
1283
- nodeIndex?: number,
1284
- ) => {
1285
- const nodeReference = [
1286
- `data calc: ${dataCalcId}`,
1287
- `subspace: ${subspaceId}`,
1288
- typeof nodeIndex === 'number' ? `node index: ${nodeIndex}` : undefined,
1289
- reference || undefined,
1290
- ]
1291
- .filter(Boolean)
1292
- .join(', ')
1293
-
1294
- if (node.__typename === 'DataCalculationData') {
1295
- return assertEntryId(
1296
- node.data()?.id,
1297
- 'PROPERTY_BANK_DATA_NODE',
1298
- `(${nodeReference})`,
1299
- )
1300
- }
1301
- if (node.__typename === 'DataCommand') {
1302
- return assertEntryId(node.id, 'PROPERTY_BANK_COMMAND_NODE', `(${nodeReference})`)
1303
- }
1304
- throw new Error(`Invalid node: ${JSON.stringify(node)}`)
1312
+ const calc: any = {
1313
+ alias: dataCalc.alias,
1314
+ title: dataCalc.title,
1315
+ description: dataCalc.description,
1316
+ hide_short_ref: dataCalc.hideShortRef,
1305
1317
  }
1306
-
1307
- const generateInputPorts = (inputs) =>
1308
- inputs.reduce((acc, port, portIndex) => {
1309
- if (!acc[port.key]) acc[port.key] = null
1310
-
1311
- let sourceId
1312
- const sourceNode = port.source()
1313
- if (
1314
- sourceNode?.__typename === 'DataCalculationData' ||
1315
- sourceNode?.__typename === 'DataCommand'
1316
- ) {
1317
- sourceId = getNodeId(sourceNode, `input port index: ${portIndex}`)
1318
+ if (dataCalc.triggerMode) calc.trigger_type = dataCalc.triggerMode
1319
+ if (dataCalc.__typename === 'DataCalculationMap') {
1320
+ calc.type = 'general'
1321
+ const mapCalc = dataCalc as DataCalculationMap
1322
+
1323
+ const getNodeId = (
1324
+ node: DataCalculationData | DataCommand,
1325
+ reference = '',
1326
+ nodeIndex?: number,
1327
+ ) => {
1328
+ const nodeReference = [
1329
+ `data calc: ${dataCalcId}`,
1330
+ `subspace: ${subspaceId}`,
1331
+ typeof nodeIndex === 'number' ? `node index: ${nodeIndex}` : undefined,
1332
+ reference || undefined,
1333
+ ]
1334
+ .filter(Boolean)
1335
+ .join(', ')
1336
+
1337
+ if (node.__typename === 'DataCalculationData') {
1338
+ return assertEntryId(
1339
+ node.data()?.id,
1340
+ 'PROPERTY_BANK_DATA_NODE',
1341
+ `(${nodeReference})`,
1342
+ )
1318
1343
  }
1344
+ if (node.__typename === 'DataCommand') {
1345
+ return assertEntryId(node.id, 'PROPERTY_BANK_COMMAND_NODE', `(${nodeReference})`)
1346
+ }
1347
+ throw new Error(`Invalid node: ${JSON.stringify(node)}`)
1348
+ }
1319
1349
 
1320
- if (!sourceId) return acc
1321
- if (!acc[port.key]) acc[port.key] = []
1322
-
1323
- acc[port.key].push({
1324
- id: sourceId,
1325
- port: port.sourceKey,
1326
- disable_trigger_command: !port.trigger ? true : undefined,
1327
- })
1350
+ const generateInputPorts = (inputs) =>
1351
+ inputs.reduce((acc, port, portIndex) => {
1352
+ if (!acc[port.key]) acc[port.key] = null
1353
+
1354
+ let sourceId
1355
+ const sourceNode = port.source()
1356
+ if (
1357
+ sourceNode?.__typename === 'DataCalculationData' ||
1358
+ sourceNode?.__typename === 'DataCommand'
1359
+ ) {
1360
+ sourceId = getNodeId(sourceNode, `input port index: ${portIndex}`)
1361
+ }
1362
+
1363
+ if (!sourceId) return acc
1364
+ if (!acc[port.key]) acc[port.key] = []
1365
+
1366
+ acc[port.key].push({
1367
+ id: sourceId,
1368
+ port: port.sourceKey,
1369
+ disable_trigger_command: !port.trigger ? true : undefined,
1370
+ })
1371
+ return acc
1372
+ }, {})
1373
+
1374
+ const generateOutputPorts = (outputs) =>
1375
+ outputs.reduce((acc, port, portIndex) => {
1376
+ if (!acc[port.key]) acc[port.key] = null
1377
+
1378
+ let targetId
1379
+ const targetNode = port.target()
1380
+ if (
1381
+ targetNode?.__typename === 'DataCalculationData' ||
1382
+ targetNode?.__typename === 'DataCommand'
1383
+ ) {
1384
+ targetId = getNodeId(targetNode, `output port index: ${portIndex}`)
1385
+ }
1386
+
1387
+ if (!targetId) return acc
1388
+ if (!acc[port.key]) acc[port.key] = []
1389
+
1390
+ acc[port.key].push({
1391
+ id: targetId,
1392
+ port: port.targetKey,
1393
+ })
1394
+ return acc
1395
+ }, {})
1396
+
1397
+ calc.map = mapCalc.nodes.reduce((acc, node, nodeIndex) => {
1398
+ if (node.__typename === 'DataCalculationData') {
1399
+ const dataNode = node as DataCalculationData
1400
+ acc[getNodeId(dataNode, 'data node', nodeIndex)] = {
1401
+ title: dataNode.title,
1402
+ description: dataNode.description,
1403
+ hide_short_ref: dataNode.hideShortRef,
1404
+ type: 'data-node',
1405
+ properties: {},
1406
+ in: generateInputPorts(dataNode.inputs),
1407
+ out: generateOutputPorts(dataNode.outputs),
1408
+ }
1409
+ } else if (node.__typename === 'DataCommand') {
1410
+ const commandNode = node as DataCommand
1411
+ const commandName = commandNode.__commandName
1412
+ const type = commandName.split('_')[0].toLowerCase()
1413
+
1414
+ const args = commandNode.inputs.filter(
1415
+ (input) =>
1416
+ typeof input.source !== 'function' ||
1417
+ (input.source()?.__typename !== 'DataCalculationData' &&
1418
+ input.source()?.__typename !== 'DataCommand'),
1419
+ )
1420
+ const inputs = commandNode.inputs.filter(
1421
+ (input) =>
1422
+ typeof input.source === 'function' &&
1423
+ (input.source()?.__typename === 'DataCalculationData' ||
1424
+ input.source()?.__typename === 'DataCommand'),
1425
+ )
1426
+ acc[getNodeId(commandNode, 'command node', nodeIndex)] = {
1427
+ title: commandNode.title,
1428
+ description: commandNode.description,
1429
+ hide_short_ref: commandNode.hideShortRef,
1430
+ type: `command-node-${type}`,
1431
+ properties: {
1432
+ command: commandNode.__commandName,
1433
+ args: args.reduce((argsAcc, input) => {
1434
+ argsAcc[input.key] = input.source
1435
+ return argsAcc
1436
+ }, {}),
1437
+ },
1438
+ in: generateInputPorts(inputs),
1439
+ out: generateOutputPorts(commandNode.outputs),
1440
+ }
1441
+ }
1328
1442
  return acc
1329
1443
  }, {})
1330
-
1331
- const generateOutputPorts = (outputs) =>
1332
- outputs.reduce((acc, port, portIndex) => {
1333
- if (!acc[port.key]) acc[port.key] = null
1334
-
1335
- let targetId
1336
- const targetNode = port.target()
1337
- if (
1338
- targetNode?.__typename === 'DataCalculationData' ||
1339
- targetNode?.__typename === 'DataCommand'
1340
- ) {
1341
- targetId = getNodeId(targetNode, `output port index: ${portIndex}`)
1444
+ calc.editor_info = mapCalc.editorInfo.reduce((acc, editorInfo) => {
1445
+ acc[getNodeId(editorInfo.node, 'editor info node')] = {
1446
+ position: editorInfo.position,
1447
+ points: editorInfo.points.reduce((pointsAcc, point) => {
1448
+ const sourceId = getNodeId(point.source, 'editor info point source')
1449
+ const targetId = getNodeId(point.target, 'editor info point target')
1450
+ const key = `${sourceId}-${point.sourceOutputKey}-${targetId}-${point.targetInputKey}`
1451
+ pointsAcc[key] = point.positions
1452
+ return pointsAcc
1453
+ }, {}),
1342
1454
  }
1343
-
1344
- if (!targetId) return acc
1345
- if (!acc[port.key]) acc[port.key] = []
1346
-
1347
- acc[port.key].push({
1348
- id: targetId,
1349
- port: port.targetKey,
1350
- })
1351
1455
  return acc
1352
1456
  }, {})
1353
-
1354
- calc.map = mapCalc.nodes.reduce((acc, node, nodeIndex) => {
1355
- if (node.__typename === 'DataCalculationData') {
1356
- const dataNode = node as DataCalculationData
1357
- acc[getNodeId(dataNode, 'data node', nodeIndex)] = {
1358
- title: dataNode.title,
1359
- description: dataNode.description,
1360
- hide_short_ref: dataNode.hideShortRef,
1361
- type: 'data-node',
1362
- properties: {},
1363
- in: generateInputPorts(dataNode.inputs),
1364
- out: generateOutputPorts(dataNode.outputs),
1365
- }
1366
- } else if (node.__typename === 'DataCommand') {
1367
- const commandNode = node as DataCommand
1368
- const commandName = commandNode.__commandName
1369
- const type = commandName.split('_')[0].toLowerCase()
1370
-
1371
- const args = commandNode.inputs.filter(
1372
- (input) =>
1373
- typeof input.source !== 'function' ||
1374
- (input.source()?.__typename !== 'DataCalculationData' &&
1375
- input.source()?.__typename !== 'DataCommand'),
1376
- )
1377
- const inputs = commandNode.inputs.filter(
1378
- (input) =>
1379
- typeof input.source === 'function' &&
1380
- (input.source()?.__typename === 'DataCalculationData' ||
1381
- input.source()?.__typename === 'DataCommand'),
1382
- )
1383
- acc[getNodeId(commandNode, 'command node', nodeIndex)] = {
1384
- title: commandNode.title,
1385
- description: commandNode.description,
1386
- hide_short_ref: commandNode.hideShortRef,
1387
- type: `command-node-${type}`,
1388
- properties: {
1389
- command: commandNode.__commandName,
1390
- args: args.reduce((argsAcc, input) => {
1391
- argsAcc[input.key] = input.source
1392
- return argsAcc
1393
- }, {}),
1394
- },
1395
- in: generateInputPorts(inputs),
1396
- out: generateOutputPorts(commandNode.outputs),
1397
- }
1398
- }
1399
- return acc
1400
- }, {})
1401
- calc.editor_info = mapCalc.editorInfo.reduce((acc, editorInfo) => {
1402
- acc[getNodeId(editorInfo.node, 'editor info node')] = {
1403
- position: editorInfo.position,
1404
- points: editorInfo.points.reduce((pointsAcc, point) => {
1405
- const sourceId = getNodeId(point.source, 'editor info point source')
1406
- const targetId = getNodeId(point.target, 'editor info point target')
1407
- const key = `${sourceId}-${point.sourceOutputKey}-${targetId}-${point.targetInputKey}`
1408
- pointsAcc[key] = point.positions
1409
- return pointsAcc
1457
+ } else if (dataCalc.__typename === 'DataCalculationScript') {
1458
+ const scriptCalc = dataCalc as DataCalculationScript
1459
+ calc.type = 'script'
1460
+
1461
+ const code = compileScriptCalculationCode(scriptCalc.code)
1462
+ calc.script_config = {
1463
+ title: scriptCalc.title ?? '',
1464
+ note: scriptCalc.note ?? '',
1465
+ code,
1466
+ enable_async: scriptCalc.enableAsync,
1467
+ trigger_mode: scriptCalc.triggerMode,
1468
+ inputs: scriptCalc.inputs.reduce((acc, input) => {
1469
+ const inputId = assertEntryId(
1470
+ input.data()?.id,
1471
+ 'PROPERTY_BANK_DATA_NODE',
1472
+ `(data calc: ${dataCalcId}, script input: ${input.key}, subspace: ${subspaceId})`,
1473
+ )
1474
+ acc[inputId] = input.key
1475
+ return acc
1410
1476
  }, {}),
1411
- }
1412
- return acc
1413
- }, {})
1414
- } else if (dataCalc.__typename === 'DataCalculationScript') {
1415
- const scriptCalc = dataCalc as DataCalculationScript
1416
- calc.type = 'script'
1417
-
1418
- const code = compileScriptCalculationCode(scriptCalc.code)
1419
- calc.script_config = {
1420
- title: scriptCalc.title ?? '',
1421
- note: scriptCalc.note ?? '',
1422
- code,
1423
- enable_async: scriptCalc.enableAsync,
1424
- trigger_mode: scriptCalc.triggerMode,
1425
- inputs: scriptCalc.inputs.reduce((acc, input) => {
1426
- const inputId = assertEntryId(
1427
- input.data()?.id,
1428
- 'PROPERTY_BANK_DATA_NODE',
1429
- `(data calc: ${dataCalcId}, script input: ${input.key}, subspace: ${subspaceId})`,
1430
- )
1431
- acc[inputId] = input.key
1432
- return acc
1433
- }, {}),
1434
- disabled_triggers: scriptCalc.inputs.reduce((acc, input) => {
1435
- const inputId = assertEntryId(
1436
- input.data()?.id,
1437
- 'PROPERTY_BANK_DATA_NODE',
1438
- `(data calc: ${dataCalcId}, script trigger input: ${input.key}, subspace: ${subspaceId})`,
1439
- )
1440
- acc[inputId] = !input.trigger
1441
- return acc
1442
- }, {}),
1443
- output: scriptCalc.output
1444
- ? assertEntryId(
1445
- scriptCalc.output()?.id,
1477
+ disabled_triggers: scriptCalc.inputs.reduce((acc, input) => {
1478
+ const inputId = assertEntryId(
1479
+ input.data()?.id,
1446
1480
  'PROPERTY_BANK_DATA_NODE',
1447
- `(data calc: ${dataCalcId}, script output, subspace: ${subspaceId})`,
1481
+ `(data calc: ${dataCalcId}, script trigger input: ${input.key}, subspace: ${subspaceId})`,
1448
1482
  )
1449
- : null,
1450
- outputs: scriptCalc.outputs.reduce((acc, output) => {
1451
- if (!acc[output.key]) acc[output.key] = []
1452
- const outputId = assertEntryId(
1453
- output.data()?.id,
1454
- 'PROPERTY_BANK_DATA_NODE',
1455
- `(data calc: ${dataCalcId}, script outputs key: ${output.key}, subspace: ${subspaceId})`,
1456
- )
1457
- acc[output.key].push(outputId)
1458
- return acc
1459
- }, {}),
1460
- error: scriptCalc.error
1461
- ? assertEntryId(
1462
- scriptCalc.error()?.id,
1483
+ acc[inputId] = !input.trigger
1484
+ return acc
1485
+ }, {}),
1486
+ output: scriptCalc.output
1487
+ ? assertEntryId(
1488
+ scriptCalc.output()?.id,
1489
+ 'PROPERTY_BANK_DATA_NODE',
1490
+ `(data calc: ${dataCalcId}, script output, subspace: ${subspaceId})`,
1491
+ )
1492
+ : null,
1493
+ outputs: scriptCalc.outputs.reduce((acc, output) => {
1494
+ if (!acc[output.key]) acc[output.key] = []
1495
+ const outputId = assertEntryId(
1496
+ output.data()?.id,
1463
1497
  'PROPERTY_BANK_DATA_NODE',
1464
- `(data calc: ${dataCalcId}, script error output, subspace: ${subspaceId})`,
1498
+ `(data calc: ${dataCalcId}, script outputs key: ${output.key}, subspace: ${subspaceId})`,
1465
1499
  )
1466
- : null,
1467
- }
1500
+ acc[output.key].push(outputId)
1501
+ return acc
1502
+ }, {}),
1503
+ error: scriptCalc.error
1504
+ ? assertEntryId(
1505
+ scriptCalc.error()?.id,
1506
+ 'PROPERTY_BANK_DATA_NODE',
1507
+ `(data calc: ${dataCalcId}, script error output, subspace: ${subspaceId})`,
1508
+ )
1509
+ : null,
1510
+ }
1468
1511
 
1469
- Object.assign(calc, generateCalulationMap(calc.script_config, dataCalcId))
1470
- }
1471
- map[dataCalcId] = calc
1472
- return map
1473
- }, {}),
1512
+ Object.assign(calc, generateCalulationMap(calc.script_config, dataCalcId))
1513
+ }
1514
+ map[dataCalcId] = calc
1515
+ return map
1516
+ },
1517
+ ),
1474
1518
  action_map: subspace.actions || undefined,
1475
1519
  event_map: compileEvents('', subspace.events || {}, {
1476
1520
  camelCase: false,
1477
1521
  errorReference: `(subspace ${subspaceId})`,
1478
1522
  }),
1479
- routing: subspace.dataRouting.reduce((acc, data, index) => {
1523
+ routing: collectReduce(errors, subspace.dataRouting, (acc, data, index) => {
1480
1524
  const dataId = assertEntryId(
1481
1525
  data?.id,
1482
1526
  'PROPERTY_BANK_DATA_NODE',
@@ -1484,15 +1528,15 @@ export const compile = async (app: Application) => {
1484
1528
  )
1485
1529
  acc[dataId] = { enabled_routing: true }
1486
1530
  return acc
1487
- }, {}),
1531
+ }),
1488
1532
  ...compileModule(subspace),
1489
1533
  }
1490
1534
  return subspaceMap
1491
1535
  }, {}),
1492
- root_subspace_id: assertEntryId(
1493
- app.rootSubspace?.id,
1494
- 'SUBSPACE',
1495
- '(application root subspace)',
1536
+ root_subspace_id: collect(
1537
+ errors,
1538
+ () => assertEntryId(app.rootSubspace?.id, 'SUBSPACE', '(application root subspace)'),
1539
+ '',
1496
1540
  ),
1497
1541
  fonts: app.fonts,
1498
1542
  ...compileApplicationSettings(app.settings),
@@ -1503,6 +1547,12 @@ export const compile = async (app: Application) => {
1503
1547
  automation_map: compiledAutomationMap || app.metadata?.TEMP_automation_map || {},
1504
1548
  update_timestamp: timestamp,
1505
1549
  }
1550
+ if (errors.length > 0) {
1551
+ throw new Error(
1552
+ `Compile failed with ${errors.length} error(s):\n` +
1553
+ errors.map((message, index) => ` ${index + 1}. ${message}`).join('\n'),
1554
+ )
1555
+ }
1506
1556
  await recordConfigChange(previousConfig, config)
1507
1557
  return config
1508
1558
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fugood/bricks-ctor",
3
- "version": "2.25.0-beta.51",
3
+ "version": "2.25.0-beta.53",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "typecheck": "tsc --noEmit",
@@ -11,7 +11,7 @@
11
11
  "@babel/parser": "7.28.5",
12
12
  "@babel/traverse": "7.28.5",
13
13
  "@babel/types": "7.28.5",
14
- "@fugood/bricks-cli": "^2.25.0-beta.51",
14
+ "@fugood/bricks-cli": "^2.25.0-beta.53",
15
15
  "@huggingface/gguf": "^0.3.2",
16
16
  "@iarna/toml": "^3.0.0",
17
17
  "@modelcontextprotocol/sdk": "^1.15.0",
@@ -29,5 +29,5 @@
29
29
  "peerDependencies": {
30
30
  "oxfmt": "^0.36.0"
31
31
  },
32
- "gitHead": "9a6ddecabc4a2e11fc6ae9a256de730cd3d744ca"
32
+ "gitHead": "b08f881540787ad817115880ebcee1487a0a81ab"
33
33
  }
@@ -25,6 +25,7 @@ The primary way to orchestrate multi-step flows. A single event can contain an a
25
25
  - Use `waitAsync: true` to await async actions before the next step
26
26
  - Use `dataParams` + `mapping` to pass event data downstream
27
27
  - This is the "glue" that wires generators, state, and UI together
28
+ - For Generator result UI: await Generator, write done/version Data; route/current Data stays identity.
28
29
 
29
30
  Sequential `PROPERTY_BANK` / `PROPERTY_BANK_EXPRESSION` actions in one chain read the data values that existed when the chain started. If a later action needs to read what an earlier action wrote, set `waitAsync: true` on the earlier action.
30
31
 
@@ -87,6 +87,17 @@ const triggerCalc: EventAction = {
87
87
  - When a **later action reads the calc's outputs**, set `waitAsync: true` on the `PROPERTY_BANK_COMMAND` action itself — it awaits the full calc chain including output writes.
88
88
  - A dataParam's `value` acts as an execution gate: `{ input: () => d, value: false }` skips that trigger (combine with `mapping` for conditional runs).
89
89
 
90
+ ## One trigger source per result panel
91
+
92
+ A panel that shows the result of an async action (Generator HTTP/LLM response, request telemetry) reads from a calc whose **trigger should come from a single source**: either the live async outlet, or a completion marker the action chain writes — not both.
93
+
94
+ - **Outlet-triggered** (`{ data: () => dResponse, trigger: true }`): the calc reruns as outlets land. Use when each outlet is only written once its value is final.
95
+ - **Marker-triggered**: run the generator with `waitAsync: true`, then write a `done` Data the calc triggers on. Use when several outlets settle separately and the panel must wait for all of them.
96
+
97
+ Wiring both at once — triggering on the live outlet _and_ gating on a separately-written marker — lets the calc run before the outlets have settled (it renders `undefined`/stale) while the marker path masks it intermittently. A panel that updates "sometimes" usually has two competing triggers: pick one, and order it with `waitAsync` so the trigger fires after the values it reads are written.
98
+
99
+ Scope the panel's calc to the **single value it derives** (the formatted display string), and write sibling status labels — running/done, progress, selected route — imperatively in the event chain with `PROPERTY_BANK`. A calc rewrites _every_ one of its outputs on each run, so a return that omits a key writes `undefined` to that output Data (see [Field Rules](#field-rules-defaults-and-constraints)): folding status labels into a multi-output result calc resets them to `undefined` on any run that returns only the derived value. If a calc genuinely must drive several outputs, return all of their keys every run.
100
+
90
101
  ## Script Sandbox
91
102
 
92
103
  Scripts run in `use strict` mode as a function body — top-level `return` returns the calc result. No `fetch`, `XMLHttpRequest`, or `require` in any mode: I/O belongs to Generators.
@@ -220,6 +231,8 @@ const appendHistory: DataCalculationScript = {
220
231
  | `PROPERTY_BANK_COMMAND` does nothing | Auto calc + `trigger: false` input, or `input` doesn't reference an input Data of the calc | Command a `trigger: true` input (auto) or any input (manual) |
221
232
  | Compile error `Not allow duplicate set property id...` | Auto mode with same Data as input and output | Use `triggerMode: 'manual'`, or split into separate Data |
222
233
  | Calc reads stale value written earlier in the same chain | Missing `waitAsync: true` on the preceding write | Set `waitAsync: true` on the write action |
234
+ | Result/telemetry panel shows `undefined` or stale data intermittently | Calc triggered by two sources at once (live outlet + a separately-written marker) | Trigger from one source — see [One trigger source per result panel](#one-trigger-source-per-result-panel) |
235
+ | Sibling status Data (selected/progress) resets to `undefined` after a calc runs | Multi-output result calc whose return omitted those keys on that run | Scope the calc to one output and write status labels imperatively, or return every output key each run — see [One trigger source per result panel](#one-trigger-source-per-result-panel) |
223
236
  | Works in Simulator, fails on device | V8 vs Hermes/JSC engine difference | Verify on device (Path 2); avoid engine-sensitive parsing |
224
237
  | `console.log` shows nothing | Console only emits during DevTools debug sessions | Attach DevTools, or write debug values to an output Data |
225
238
  | `Promise`/`setTimeout` undefined, or `Async mode is required` error | `enableAsync: false` | Set `enableAsync: true` |
@@ -191,9 +191,11 @@ Default property:
191
191
  /* A stroke was just committed (drawn or added programmatically) */
192
192
  onStrokeEnd?: Array<EventAction<string & keyof TemplateEventPropsMap['Sketch']['onStrokeEnd']>>
193
193
  /* The canvas was cleared */
194
- onClear?: Array<EventAction>
194
+ onClear?: Array<EventAction<string & keyof TemplateEventPropsMap['Sketch']['onClear']>>
195
195
  /* Sketch state changed (any commit, undo, redo, clear, or import) */
196
- onStateChange?: Array<EventAction>
196
+ onStateChange?: Array<
197
+ EventAction<string & keyof TemplateEventPropsMap['Sketch']['onStateChange']>
198
+ >
197
199
  /* Active tool changed */
198
200
  onToolChange?: Array<
199
201
  EventAction<string & keyof TemplateEventPropsMap['Sketch']['onToolChange']>
@@ -1,6 +1,6 @@
1
1
  /* Auto generated by build script
2
2
  *
3
- * Embedded HTTP/HTTPS server with route matching, CORS, auth (Basic/Bearer), SSE streaming, file upload, and async response mode
3
+ * Embedded HTTP/HTTPS server with route matching, CORS, auth (Basic/Bearer), SSE streaming, file upload, async response mode, and JS Sandbox route handlers
4
4
  */
5
5
  import type { SwitchCondInnerStateCurrentCanvas, SwitchCondData, SwitchDef } from '../switch'
6
6
  import type { Data, DataLink } from '../data'
@@ -42,6 +42,7 @@ Default property:
42
42
  "init": false,
43
43
  "method": "GET",
44
44
  "path": "/",
45
+ "methods": [],
45
46
  "idleTimeout": 10000,
46
47
  "authType": "none",
47
48
  "asyncMode": false,
@@ -70,6 +71,59 @@ Default property:
70
71
  | DataLink
71
72
  /* Path of HTTP request */
72
73
  path?: string | DataLink
74
+ /* Additional route methods handled by JS Sandbox scripts. The script receives `inputs.request`, `inputs.query`, `inputs.headers`, `inputs.body`, and configured `additionalParams`. `scriptConfig.members` can expose generator or brick script-member functions, same as MCP Server. */
75
+ methods?:
76
+ | Array<
77
+ | DataLink
78
+ | {
79
+ enabled?: boolean | DataLink
80
+ name?: string | DataLink
81
+ method?:
82
+ | 'GET'
83
+ | 'POST'
84
+ | 'PUT'
85
+ | 'DELETE'
86
+ | 'HEAD'
87
+ | 'PATCH'
88
+ | 'OPTIONS'
89
+ | 'CONNECT'
90
+ | 'TRACE'
91
+ | DataLink
92
+ path?: string | DataLink
93
+ resStatusCode?: number | DataLink
94
+ resContentType?:
95
+ | 'text/plain'
96
+ | 'text/html'
97
+ | 'text/javascript'
98
+ | 'text/css'
99
+ | 'text/xml'
100
+ | 'application/xml'
101
+ | 'application/json'
102
+ | 'application/octet-stream'
103
+ | 'text/event-stream'
104
+ | DataLink
105
+ resHeader?: {} | DataLink
106
+ resBody?: any
107
+ sseEvent?: string | DataLink
108
+ scriptConfig?:
109
+ | DataLink
110
+ | {
111
+ code?: string | DataLink
112
+ timeout?: number | DataLink
113
+ members?:
114
+ | Array<
115
+ | DataLink
116
+ | {
117
+ handler?: string | DataLink
118
+ varName?: string | DataLink
119
+ }
120
+ >
121
+ | DataLink
122
+ additionalParams?: {} | DataLink
123
+ }
124
+ }
125
+ >
126
+ | DataLink
73
127
  /* Max connection idle time, 0 is disable */
74
128
  idleTimeout?: number | DataLink
75
129
  /* HTTP request body limit, 0 is unlimited */
@@ -153,7 +207,7 @@ Default property:
153
207
  }
154
208
  }
155
209
 
156
- /* Embedded HTTP/HTTPS server with route matching, CORS, auth (Basic/Bearer), SSE streaming, file upload, and async response mode */
210
+ /* Embedded HTTP/HTTPS server with route matching, CORS, auth (Basic/Bearer), SSE streaming, file upload, async response mode, and JS Sandbox route handlers */
157
211
  export type GeneratorHTTPServer = Generator &
158
212
  GeneratorHTTPServerDef & {
159
213
  templateKey: 'GENERATOR_HTTP_SERVER'
@@ -33,7 +33,7 @@ Default property:
33
33
  property?: {
34
34
  /* Start tick on generator initialized */
35
35
  init?: boolean | DataLink
36
- /* Interval of second for countdown */
36
+ /* Tick interval in milliseconds for countdown */
37
37
  interval?: number | DataLink
38
38
  /* Initial value of countdown */
39
39
  countdownStartValue?: number | DataLink
@@ -142,7 +142,9 @@ export const templateEventPropsMap = {
142
142
  },
143
143
  },
144
144
  Sketch: {
145
- onStrokeEnd: { BRICK_SKETCH_STROKE_COUNT: 'number' },
145
+ onStrokeEnd: { BRICK_SKETCH_STROKE_COUNT: 'number', BRICK_SKETCH_STATE: '{}' },
146
+ onClear: { BRICK_SKETCH_STATE: '{}' },
147
+ onStateChange: { BRICK_SKETCH_STATE: '{}' },
146
148
  onToolChange: { BRICK_SKETCH_TOOL: 'string' },
147
149
  onExportImage: { BRICK_SKETCH_IMAGE_URI: 'string' },
148
150
  },