diff --git a/src/engine/variable.js b/src/engine/variable.js index 7617f9551a..a1d5e63cc2 100644 --- a/src/engine/variable.js +++ b/src/engine/variable.js @@ -19,6 +19,7 @@ class Variable { this.name = name; this.type = type; this.isCloud = isCloud; + this.isPersistent = false; switch (this.type) { case Variable.SCALAR_TYPE: this.value = 0; diff --git a/src/extensions/scratch3_mesh_v2/mesh-service.js b/src/extensions/scratch3_mesh_v2/mesh-service.js index fec9e7a8ad..95b98fc638 100644 --- a/src/extensions/scratch3_mesh_v2/mesh-service.js +++ b/src/extensions/scratch3_mesh_v2/mesh-service.js @@ -615,10 +615,22 @@ class MeshV2Service { broadcastEvent (event) { log.info(`Mesh V2: Executing broadcastEvent for: ${event.name}`); try { + const stage = this.runtime.getTargetForStage(); + let broadcastVar = stage.lookupBroadcastByInputValue(event.name); + if (!broadcastVar) { + log.info(`Mesh V2: Creating missing broadcast message: ${event.name}`); + stage.createVariable(null, event.name, Variable.BROADCAST_MESSAGE_TYPE); + broadcastVar = stage.lookupBroadcastByInputValue(event.name); + if (broadcastVar) { + broadcastVar.isPersistent = true; + } + this.runtime.requestBlocksUpdate(); + } + const args = { BROADCAST_OPTION: { - id: null, - name: event.name + id: broadcastVar ? broadcastVar.id : null, + name: broadcastVar ? broadcastVar.name : event.name } }; const util = BlockUtility.lastInstance(); diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 0bbc818202..53def47af3 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -1400,6 +1400,9 @@ class VirtualMachine extends EventEmitter { // Anything left in messageIds is not referenced by a block, so delete it. for (let i = 0; i < messageIds.length; i++) { const id = messageIds[i]; + if (this.runtime.getTargetForStage().variables[id].isPersistent) { + continue; + } delete this.runtime.getTargetForStage().variables[id]; } const globalVarMap = Object.assign({}, this.runtime.getTargetForStage().variables); diff --git a/test/unit/mesh_service_v2_broadcast_creation.js b/test/unit/mesh_service_v2_broadcast_creation.js new file mode 100644 index 0000000000..2a793f948f --- /dev/null +++ b/test/unit/mesh_service_v2_broadcast_creation.js @@ -0,0 +1,122 @@ +const test = require('tap').test; +const MeshV2Service = require('../../src/extensions/scratch3_mesh_v2/mesh-service'); +const BlockUtility = require('../../src/engine/block-utility'); +const Variable = require('../../src/engine/variable'); + +const createMockBlocks = stage => ({ + runtime: { + sequencer: {}, + emit: () => {}, + on: () => {}, + off: () => {}, + getTargetForStage: () => stage, + requestBlocksUpdate: () => {} + }, + opcodeFunctions: { + event_broadcast: () => {} + } +}); + +const createMockStage = () => ({ + variables: {}, + lookupBroadcastMsg: function (id, name) { + for (const varId in this.variables) { + const v = this.variables[varId]; + if (v.type === Variable.BROADCAST_MESSAGE_TYPE && v.name === name) { + return v; + } + } + return null; + }, + lookupBroadcastByInputValue: function (name) { + for (const varId in this.variables) { + const v = this.variables[varId]; + if (v.type === Variable.BROADCAST_MESSAGE_TYPE && v.name.toLowerCase() === name.toLowerCase()) { + return v; + } + } + return null; + }, + createVariable: function (id, name, type) { + const varId = id || `id-${name}`; + this.variables[varId] = new Variable(varId, name, type); + } +}); + +// Mock BlockUtility.lastInstance() +const originalLastInstance = BlockUtility.lastInstance; +const mockUtil = { + sequencer: {} +}; +BlockUtility.lastInstance = () => mockUtil; + +test('MeshV2Service Broadcast Creation', t => { + t.test('broadcastEvent creates missing broadcast message', st => { + const stage = createMockStage(); + const blocks = createMockBlocks(stage); + let broadcastArgs = null; + blocks.opcodeFunctions.event_broadcast = args => { + broadcastArgs = args; + }; + + const service = new MeshV2Service(blocks, 'node1', 'domain1'); + service.groupId = 'group1'; + + const event = { + name: 'new message', + firedByNodeId: 'node2' + }; + + service.broadcastEvent(event); + + // Check if message was created + const broadcastVar = stage.lookupBroadcastByInputValue('new message'); + st.ok(broadcastVar, 'Broadcast message should be created'); + st.equal(broadcastVar.name, 'new message'); + st.equal(broadcastVar.type, Variable.BROADCAST_MESSAGE_TYPE); + st.equal(broadcastVar.isPersistent, true, 'Broadcast message should be persistent'); + + // Check if opcode was called with the new ID + st.ok(broadcastArgs, 'event_broadcast should be called'); + st.equal(broadcastArgs.BROADCAST_OPTION.name, 'new message'); + st.equal(broadcastArgs.BROADCAST_OPTION.id, broadcastVar.id); + + st.end(); + }); + + t.test('broadcastEvent uses existing broadcast message (case-insensitive)', st => { + const stage = createMockStage(); + const existingVar = new Variable('existing-id', 'Existing Message', Variable.BROADCAST_MESSAGE_TYPE); + stage.variables['existing-id'] = existingVar; + + const blocks = createMockBlocks(stage); + let broadcastArgs = null; + blocks.opcodeFunctions.event_broadcast = args => { + broadcastArgs = args; + }; + + const service = new MeshV2Service(blocks, 'node1', 'domain1'); + service.groupId = 'group1'; + + const event = { + name: 'existing message', + firedByNodeId: 'node2' + }; + + service.broadcastEvent(event); + + // Check if opcode was called with the existing ID and canonical name + st.ok(broadcastArgs, 'event_broadcast should be called'); + st.equal(broadcastArgs.BROADCAST_OPTION.id, 'existing-id', 'Should use existing variable ID'); + st.equal(broadcastArgs.BROADCAST_OPTION.name, 'Existing Message', 'Should use canonical name'); + st.equal(Object.keys(stage.variables).length, 1, 'Should not create duplicate variable'); + + st.end(); + }); + + t.tearDown(() => { + BlockUtility.lastInstance = originalLastInstance; + }); + + t.end(); +}); diff --git a/test/unit/mesh_service_v2_integration.js b/test/unit/mesh_service_v2_integration.js index 889a5ff167..7e4c8715e7 100644 --- a/test/unit/mesh_service_v2_integration.js +++ b/test/unit/mesh_service_v2_integration.js @@ -12,7 +12,13 @@ const createMockBlocks = broadcastCallback => ({ sequencer: {}, emit: () => {}, on: () => {}, - off: () => {} + off: () => {}, + getTargetForStage: () => ({ + lookupBroadcastMsg: (id, name) => ({id: `id-${name}`, name: name}), + lookupBroadcastByInputValue: name => ({id: `id-${name}`, name: name}), + createVariable: () => {} + }), + requestBlocksUpdate: () => {} }, opcodeFunctions: { event_broadcast: args => {