From 57c86aa7b84440313c36b9b5f0bba290ef9f5b3e Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Thu, 1 Jan 2026 18:08:01 +0900 Subject: [PATCH] feat: use last modified value for meshV2 sensor value block - Update remoteData structure to include timestamps for each data key - Modify handleDataUpdate and fetchAllNodesData to store timestamps - Update getRemoteVariable to return most recently modified value - Add unit tests for timestamp-based variable retrieval - Update integration tests to match new remoteData structure Resolves: smalruby/smalruby3-gui#492 Co-Authored-By: Gemini --- .../scratch3_mesh_v2/mesh-service.js | 23 +++-- .../extensions/mesh-v2-variable-sync.test.js | 2 +- test/unit/mesh_service_v2_timestamp.js | 84 +++++++++++++++++++ 3 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 test/unit/mesh_service_v2_timestamp.js diff --git a/src/extensions/scratch3_mesh_v2/mesh-service.js b/src/extensions/scratch3_mesh_v2/mesh-service.js index 51ef500572..fc558a0d0e 100644 --- a/src/extensions/scratch3_mesh_v2/mesh-service.js +++ b/src/extensions/scratch3_mesh_v2/mesh-service.js @@ -52,7 +52,7 @@ class MeshV2Service { this.dataSyncTimer = null; this.memberHeartbeatInterval = 120; // Default 2 min - // Data from other nodes: { nodeId: { key: value } } + // Data from other nodes: { nodeId: { key: { value: string, timestamp: number } } } this.remoteData = {}; // Rate limiters @@ -365,7 +365,10 @@ class MeshV2Service { } nodeStatus.data.forEach(item => { - this.remoteData[nodeId][item.key] = item.value; + this.remoteData[nodeId][item.key] = { + value: item.value, + timestamp: Date.now() // Add timestamp + }; }); } @@ -775,7 +778,10 @@ class MeshV2Service { this.remoteData[status.nodeId] = {}; } status.data.forEach(item => { - this.remoteData[status.nodeId][item.key] = item.value; + this.remoteData[status.nodeId][item.key] = { + value: item.value, + timestamp: Date.now() + }; }); }); @@ -848,13 +854,20 @@ class MeshV2Service { } getRemoteVariable (name) { + let latestValue = null; + let latestTimestamp = 0; + // Search across all nodes for the variable name for (const nodeId in this.remoteData) { if (Object.prototype.hasOwnProperty.call(this.remoteData[nodeId], name)) { - return this.remoteData[nodeId][name]; + const data = this.remoteData[nodeId][name]; + if (data.timestamp > latestTimestamp) { + latestTimestamp = data.timestamp; + latestValue = data.value; + } } } - return null; + return latestValue; } } diff --git a/test/integration/extensions/mesh-v2-variable-sync.test.js b/test/integration/extensions/mesh-v2-variable-sync.test.js index c60df2e10f..0d7da4936e 100644 --- a/test/integration/extensions/mesh-v2-variable-sync.test.js +++ b/test/integration/extensions/mesh-v2-variable-sync.test.js @@ -165,7 +165,7 @@ test('MeshV2Service fetch existing nodes data on joinGroup', async t => { await service.joinGroup('group1', 'domain1', 'groupName'); t.ok(service.remoteData['host-node'], 'Should have data from host-node'); - t.equal(service.remoteData['host-node'].hostVar, '100', 'Should have correct variable value from host'); + t.equal(service.remoteData['host-node'].hostVar.value, '100', 'Should have correct variable value from host'); service.cleanup(); t.end(); diff --git a/test/unit/mesh_service_v2_timestamp.js b/test/unit/mesh_service_v2_timestamp.js new file mode 100644 index 0000000000..e7abb6886c --- /dev/null +++ b/test/unit/mesh_service_v2_timestamp.js @@ -0,0 +1,84 @@ +const test = require('tap').test; +const MeshV2Service = require('../../src/extensions/scratch3_mesh_v2/mesh-service'); + +const createMockBlocks = () => ({ + runtime: { + sequencer: {}, + emit: () => {}, + on: () => {}, + off: () => {} + } +}); + +test('MeshV2Service Timestamp-based getRemoteVariable', t => { + const blocks = createMockBlocks(); + const service = new MeshV2Service(blocks, 'node-self', 'domain1'); + service.groupId = 'group1'; + + t.test('should return the latest value based on timestamp', st => { + // Setup remoteData with multiple nodes having the same key + const now = Date.now(); + service.remoteData = { + node1: { + 'my var': {value: 'value-old', timestamp: now - 1000} + }, + node2: { + 'my var': {value: 'value-newest', timestamp: now} + }, + node3: { + 'my var': {value: 'value-middle', timestamp: now - 500} + } + }; + + const result = service.getRemoteVariable('my var'); + st.equal(result, 'value-newest', 'Should return the value with the largest timestamp'); + st.end(); + }); + + t.test('handleDataUpdate should add timestamp', st => { + const nodeStatus = { + nodeId: 'node4', + data: [ + {key: 'var1', value: '100'} + ] + }; + + const beforeUpdate = Date.now(); + service.handleDataUpdate(nodeStatus); + const afterUpdate = Date.now(); + + st.ok(service.remoteData.node4, 'Node 4 should be added'); + st.ok(service.remoteData.node4.var1, 'var1 should be added'); + st.equal(service.remoteData.node4.var1.value, '100'); + st.ok(service.remoteData.node4.var1.timestamp >= beforeUpdate); + st.ok(service.remoteData.node4.var1.timestamp <= afterUpdate); + st.end(); + }); + + t.test('fetchAllNodesData should add timestamp', async st => { + service.client = { + query: () => Promise.resolve({ + data: { + listGroupStatuses: [ + { + nodeId: 'node5', + data: [{key: 'var2', value: '200'}] + } + ] + } + }) + }; + + const beforeFetch = Date.now(); + await service.fetchAllNodesData(); + const afterFetch = Date.now(); + + st.ok(service.remoteData.node5, 'Node 5 should be added'); + st.equal(service.remoteData.node5.var2.value, '200'); + st.ok(service.remoteData.node5.var2.timestamp >= beforeFetch); + st.ok(service.remoteData.node5.var2.timestamp <= afterFetch); + st.end(); + }); + + t.end(); +});