From 5bf7d591e418c9d5eb9db740d0e3338033cfee2a Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 3 Jan 2026 18:13:51 +0900 Subject: [PATCH 1/8] feat: implement configurable mesh v2 parameters - Added environment variables for connection timeout, data update interval, event batch interval. - Added environment variable for host heartbeat interval. - Updated mesh-service.js to use these variables with default fallbacks. - Updated webpack.config.js to inject these variables. --- .../scratch3_mesh_v2/mesh-service.js | 18 ++++++++++++++---- webpack.config.js | 6 +++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/extensions/scratch3_mesh_v2/mesh-service.js b/src/extensions/scratch3_mesh_v2/mesh-service.js index ceda5b9f899..528e8f686ec 100644 --- a/src/extensions/scratch3_mesh_v2/mesh-service.js +++ b/src/extensions/scratch3_mesh_v2/mesh-service.js @@ -18,7 +18,9 @@ const { LIST_GROUP_STATUSES } = require('./gql-operations'); -const CONNECTION_TIMEOUT = 50 * 60 * 1000; // 50 minutes in milliseconds +const CONNECTION_TIMEOUT = process.env.MESH_CONNECTION_TIMEOUT_MS ? + parseInt(process.env.MESH_CONNECTION_TIMEOUT_MS, 10) : + 50 * 60 * 1000; // Default 50 minutes in milliseconds /** * GraphQL error types that indicate the connection is no longer valid. @@ -58,14 +60,19 @@ class MeshV2Service { this.remoteData = {}; // Rate limiters - this.dataRateLimiter = new RateLimiter(4, 250, { + const dataInterval = process.env.MESH_DATA_UPDATE_INTERVAL_MS ? + parseInt(process.env.MESH_DATA_UPDATE_INTERVAL_MS, 10) : + 250; + this.dataRateLimiter = new RateLimiter(4, dataInterval, { enableMerge: true, mergeKeyField: 'key' }); // Event queue for batch sending: { eventName, payload, firedAt } の配列 this.eventQueue = []; - this.eventBatchInterval = 250; + this.eventBatchInterval = process.env.MESH_EVENT_BATCH_INTERVAL_MS ? + parseInt(process.env.MESH_EVENT_BATCH_INTERVAL_MS, 10) : + 250; this.eventBatchTimer = null; // Event queue limits @@ -657,7 +664,10 @@ class MeshV2Service { log.info(`Mesh V2: Starting heartbeat timer (Role: ${this.isHost ? 'Host' : 'Member'})`); // Use 15s for host, memberHeartbeatInterval for member (default 120s) - const interval = this.isHost ? 15 * 1000 : this.memberHeartbeatInterval * 1000; + const hostInterval = process.env.MESH_HOST_HEARTBEAT_INTERVAL_MS ? + parseInt(process.env.MESH_HOST_HEARTBEAT_INTERVAL_MS, 10) : + 15 * 1000; + const interval = this.isHost ? hostInterval : this.memberHeartbeatInterval * 1000; this.heartbeatTimer = setInterval(() => { if (this.isHost) { diff --git a/webpack.config.js b/webpack.config.js index faf0579148d..83ceec217e3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -76,7 +76,11 @@ const webBuilder = new ScratchWebpackConfigBuilder(common) .addPlugin(new webpack.DefinePlugin({ 'process.env.MESH_GRAPHQL_ENDPOINT': JSON.stringify(process.env.MESH_GRAPHQL_ENDPOINT), 'process.env.MESH_API_KEY': JSON.stringify(process.env.MESH_API_KEY), - 'process.env.MESH_AWS_REGION': JSON.stringify(process.env.MESH_AWS_REGION) + 'process.env.MESH_AWS_REGION': JSON.stringify(process.env.MESH_AWS_REGION), + 'process.env.MESH_CONNECTION_TIMEOUT_MS': JSON.stringify(process.env.MESH_CONNECTION_TIMEOUT_MS), + 'process.env.MESH_DATA_UPDATE_INTERVAL_MS': JSON.stringify(process.env.MESH_DATA_UPDATE_INTERVAL_MS), + 'process.env.MESH_EVENT_BATCH_INTERVAL_MS': JSON.stringify(process.env.MESH_EVENT_BATCH_INTERVAL_MS), + 'process.env.MESH_HOST_HEARTBEAT_INTERVAL_MS': JSON.stringify(process.env.MESH_HOST_HEARTBEAT_INTERVAL_MS) })); const playgroundBuilder = webBuilder.clone() From 2bd170a05246dca49579e4c21060440cc504496f Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 3 Jan 2026 19:55:45 +0900 Subject: [PATCH 2/8] refactor: improve environment variable parsing in MeshV2Service - Added parseEnvInt helper for unified parsing and validation - Added descriptive comments and default values for Mesh v2 configuration - Updated .env.example with missing Mesh v2 parameters --- .env.example | 25 +++++++++ .../scratch3_mesh_v2/mesh-service.js | 54 ++++++++++++++----- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 90de5ea03e2..cef283630cf 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,28 @@ MESH_API_KEY=da2-your-api-key # AWS Region (Production) # Example: ap-northeast-1 MESH_AWS_REGION=ap-northeast-1 + +# Connection timeout in milliseconds +# Default: 3000000 (50 minutes) +# Production: 1500000 (25 minutes) +MESH_CONNECTION_TIMEOUT_MS=3000000 + +# Data update interval in milliseconds +# Default: 250 +# Production: 1000 +MESH_DATA_UPDATE_INTERVAL_MS=250 + +# Event batch interval in milliseconds +# Default: 250 +# Production: 1000 +MESH_EVENT_BATCH_INTERVAL_MS=250 + +# Host heartbeat interval in milliseconds +# Default: 15000 (15 seconds) +MESH_HOST_HEARTBEAT_INTERVAL_MS=15000 + +# Debug logging (comma-separated categories, e.g., scratch-vm:*) +DEBUG= + +# Development server port +PORT=8073 diff --git a/src/extensions/scratch3_mesh_v2/mesh-service.js b/src/extensions/scratch3_mesh_v2/mesh-service.js index 528e8f686ec..7f0f9a99bd5 100644 --- a/src/extensions/scratch3_mesh_v2/mesh-service.js +++ b/src/extensions/scratch3_mesh_v2/mesh-service.js @@ -18,9 +18,28 @@ const { LIST_GROUP_STATUSES } = require('./gql-operations'); -const CONNECTION_TIMEOUT = process.env.MESH_CONNECTION_TIMEOUT_MS ? - parseInt(process.env.MESH_CONNECTION_TIMEOUT_MS, 10) : - 50 * 60 * 1000; // Default 50 minutes in milliseconds +/** + * Parses an environment variable as an integer with validation. + * @param {string} envVar - The environment variable value. + * @param {number} defaultValue - The default value if parsing fails or is out of range. + * @param {number} min - Minimum allowed value (inclusive). + * @param {number} max - Maximum allowed value (inclusive). + * @returns {number} The parsed integer or default value. + */ +const parseEnvInt = (envVar, defaultValue, min = 0, max = Infinity) => { + if (!envVar) return defaultValue; + const parsed = parseInt(envVar, 10); + if (isNaN(parsed) || parsed < min || parsed > max) return defaultValue; + return parsed; +}; + +// Mesh v2 configuration parameters +const CONNECTION_TIMEOUT = parseEnvInt( + process.env.MESH_CONNECTION_TIMEOUT_MS, + 50 * 60 * 1000, // Default: 50 minutes (production: 25 minutes = 1500000ms) + 60 * 1000, // min: 1 minute + 120 * 60 * 1000 // max: 2 hours +); /** * GraphQL error types that indicate the connection is no longer valid. @@ -60,9 +79,13 @@ class MeshV2Service { this.remoteData = {}; // Rate limiters - const dataInterval = process.env.MESH_DATA_UPDATE_INTERVAL_MS ? - parseInt(process.env.MESH_DATA_UPDATE_INTERVAL_MS, 10) : - 250; + // Data update interval (default: 250ms, production: 1000ms) + const dataInterval = parseEnvInt( + process.env.MESH_DATA_UPDATE_INTERVAL_MS, + 250, // default + 100, // min: 100ms + 10000 // max: 10 seconds + ); this.dataRateLimiter = new RateLimiter(4, dataInterval, { enableMerge: true, mergeKeyField: 'key' @@ -70,9 +93,13 @@ class MeshV2Service { // Event queue for batch sending: { eventName, payload, firedAt } の配列 this.eventQueue = []; - this.eventBatchInterval = process.env.MESH_EVENT_BATCH_INTERVAL_MS ? - parseInt(process.env.MESH_EVENT_BATCH_INTERVAL_MS, 10) : - 250; + // Event batch interval (default: 250ms, production: 1000ms) + this.eventBatchInterval = parseEnvInt( + process.env.MESH_EVENT_BATCH_INTERVAL_MS, + 250, // default + 100, // min: 100ms + 10000 // max: 10 seconds + ); this.eventBatchTimer = null; // Event queue limits @@ -664,9 +691,12 @@ class MeshV2Service { log.info(`Mesh V2: Starting heartbeat timer (Role: ${this.isHost ? 'Host' : 'Member'})`); // Use 15s for host, memberHeartbeatInterval for member (default 120s) - const hostInterval = process.env.MESH_HOST_HEARTBEAT_INTERVAL_MS ? - parseInt(process.env.MESH_HOST_HEARTBEAT_INTERVAL_MS, 10) : - 15 * 1000; + const hostInterval = parseEnvInt( + process.env.MESH_HOST_HEARTBEAT_INTERVAL_MS, + 15 * 1000, // default: 15 seconds + 1000, // min: 1 second + 300 * 1000 // max: 5 minutes + ); const interval = this.isHost ? hostInterval : this.memberHeartbeatInterval * 1000; this.heartbeatTimer = setInterval(() => { From af019ded6269d153b5da0337373313850650c678 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 3 Jan 2026 21:03:31 +0900 Subject: [PATCH 3/8] refactor: align Mesh v2 environment variables with backend naming - Renamed MESH_CONNECTION_TIMEOUT_MS to MESH_MAX_CONNECTION_TIME_SECONDS (unit: seconds) - Renamed MESH_HOST_HEARTBEAT_INTERVAL_MS to MESH_HOST_HEARTBEAT_INTERVAL_SECONDS (unit: seconds) - Added MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS (unit: seconds) - Updated implementation to handle seconds and convert to ms where needed - Updated .env.example and webpack.config.js --- .env | 45 +++++++++++++++++++ .env.example | 17 ++++--- .../scratch3_mesh_v2/mesh-service.js | 32 ++++++++----- webpack.config.js | 7 ++- 4 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 00000000000..ed86e9026e8 --- /dev/null +++ b/.env @@ -0,0 +1,45 @@ +# Smalruby3 scratch-vm Environment Variables +# Copy this file to .env and adjust values for local development + +# Mesh V2 Extension Configuration + +# AppSync GraphQL API Endpoint (Production) +# Example: https://xxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql +# stg +MESH_GRAPHQL_ENDPOINT=https://mpe3yhgk6zdxfhjbay2vqcq5qe.appsync-api.ap-northeast-1.amazonaws.com/graphql +# stg2 +# MESH_GRAPHQL_ENDPOINT=https://n4toi6es3fchzhp2ccxyg2qjqy.appsync-api.ap-northeast-1.amazonaws.com/graphql + +# AppSync API Key (Production) +# Example: da2-xxxxx +# stg +MESH_API_KEY=AIzaSyDBKkJfMiFfps8zEXubMu5XZmDGJ_AuLw8 +# stg2 +# MESH_API_KEY=da2-hpyr3rgsd5ewjoa56thwxmyjmm + +# AWS Region (Production) +# Example: ap-northeast-1 +# stg, stg2, prod +MESH_AWS_REGION=ap-northeast-1 + +# Maximum connection time in seconds +# Default: 300 (5 minutes) +MESH_MAX_CONNECTION_TIME_SECONDS=300 + +# Data update interval in milliseconds +# Default, Production: 1000 +MESH_DATA_UPDATE_INTERVAL_MS=1000 + +# Event batch interval in milliseconds +# Default, Production: 1000 +MESH_EVENT_BATCH_INTERVAL_MS=1000 + +# Host heartbeat interval in milliseconds +# Default: 60000 (60 seconds) +MESH_HOST_HEARTBEAT_INTERVAL_MS=60000 + +# Debug logging (comma-separated categories, e.g., scratch-vm:*) +DEBUG= + +# Development server port +PORT=8073 diff --git a/.env.example b/.env.example index cef283630cf..c37588e7b11 100644 --- a/.env.example +++ b/.env.example @@ -15,10 +15,9 @@ MESH_API_KEY=da2-your-api-key # Example: ap-northeast-1 MESH_AWS_REGION=ap-northeast-1 -# Connection timeout in milliseconds -# Default: 3000000 (50 minutes) -# Production: 1500000 (25 minutes) -MESH_CONNECTION_TIMEOUT_MS=3000000 +# Maximum connection time in seconds +# Default: 300 (5 minutes) +MESH_MAX_CONNECTION_TIME_SECONDS=300 # Data update interval in milliseconds # Default: 250 @@ -30,9 +29,13 @@ MESH_DATA_UPDATE_INTERVAL_MS=250 # Production: 1000 MESH_EVENT_BATCH_INTERVAL_MS=250 -# Host heartbeat interval in milliseconds -# Default: 15000 (15 seconds) -MESH_HOST_HEARTBEAT_INTERVAL_MS=15000 +# Host heartbeat interval in seconds +# Default: 15 (15 seconds) +MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=15 + +# Member heartbeat interval in seconds +# Default: 15 (15 seconds) +MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS=15 # Debug logging (comma-separated categories, e.g., scratch-vm:*) DEBUG= diff --git a/src/extensions/scratch3_mesh_v2/mesh-service.js b/src/extensions/scratch3_mesh_v2/mesh-service.js index 7f0f9a99bd5..8ef9f7f795f 100644 --- a/src/extensions/scratch3_mesh_v2/mesh-service.js +++ b/src/extensions/scratch3_mesh_v2/mesh-service.js @@ -35,11 +35,11 @@ const parseEnvInt = (envVar, defaultValue, min = 0, max = Infinity) => { // Mesh v2 configuration parameters const CONNECTION_TIMEOUT = parseEnvInt( - process.env.MESH_CONNECTION_TIMEOUT_MS, - 50 * 60 * 1000, // Default: 50 minutes (production: 25 minutes = 1500000ms) - 60 * 1000, // min: 1 minute - 120 * 60 * 1000 // max: 2 hours -); + process.env.MESH_MAX_CONNECTION_TIME_SECONDS, + 300, // Default: 5 minutes (300 seconds) + 60, // min: 1 minute + 7200 // max: 2 hours +) * 1000; // Convert to milliseconds /** * GraphQL error types that indicate the connection is no longer valid. @@ -690,14 +690,22 @@ class MeshV2Service { if (!this.groupId) return; log.info(`Mesh V2: Starting heartbeat timer (Role: ${this.isHost ? 'Host' : 'Member'})`); - // Use 15s for host, memberHeartbeatInterval for member (default 120s) + // Use 15s for host, memberHeartbeatInterval for member (default 15s) const hostInterval = parseEnvInt( - process.env.MESH_HOST_HEARTBEAT_INTERVAL_MS, - 15 * 1000, // default: 15 seconds - 1000, // min: 1 second - 300 * 1000 // max: 5 minutes - ); - const interval = this.isHost ? hostInterval : this.memberHeartbeatInterval * 1000; + process.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS, + 15, // default: 15 seconds + 1, // min: 1 second + 300 // max: 5 minutes + ) * 1000; + + const memberDefaultInterval = parseEnvInt( + process.env.MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS, + 15, // default: 15 seconds + 1, // min: 1 second + 300 // max: 5 minutes + ) * 1000; + + const interval = this.isHost ? hostInterval : (this.memberHeartbeatInterval * 1000 || memberDefaultInterval); this.heartbeatTimer = setInterval(() => { if (this.isHost) { diff --git a/webpack.config.js b/webpack.config.js index 83ceec217e3..1df678e99dc 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -77,10 +77,13 @@ const webBuilder = new ScratchWebpackConfigBuilder(common) 'process.env.MESH_GRAPHQL_ENDPOINT': JSON.stringify(process.env.MESH_GRAPHQL_ENDPOINT), 'process.env.MESH_API_KEY': JSON.stringify(process.env.MESH_API_KEY), 'process.env.MESH_AWS_REGION': JSON.stringify(process.env.MESH_AWS_REGION), - 'process.env.MESH_CONNECTION_TIMEOUT_MS': JSON.stringify(process.env.MESH_CONNECTION_TIMEOUT_MS), + 'process.env.MESH_MAX_CONNECTION_TIME_SECONDS': JSON.stringify(process.env.MESH_MAX_CONNECTION_TIME_SECONDS), 'process.env.MESH_DATA_UPDATE_INTERVAL_MS': JSON.stringify(process.env.MESH_DATA_UPDATE_INTERVAL_MS), 'process.env.MESH_EVENT_BATCH_INTERVAL_MS': JSON.stringify(process.env.MESH_EVENT_BATCH_INTERVAL_MS), - 'process.env.MESH_HOST_HEARTBEAT_INTERVAL_MS': JSON.stringify(process.env.MESH_HOST_HEARTBEAT_INTERVAL_MS) + 'process.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS': + JSON.stringify(process.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS), + 'process.env.MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS': + JSON.stringify(process.env.MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS) })); const playgroundBuilder = webBuilder.clone() From 412319b566f25726726e09c4f547b1f5615c7bfa Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sat, 3 Jan 2026 21:23:06 +0900 Subject: [PATCH 4/8] refactor: update default values for Mesh v2 configuration and ignore .env - Updated default intervals to match .env.example (1000ms for data/event, 60s/120s for heartbeats) - Added .env to .gitignore and untracked existing .env file --- .env | 45 ------------------- .env.example | 18 ++++---- .gitignore | 3 ++ .../scratch3_mesh_v2/mesh-service.js | 16 +++---- 4 files changed, 19 insertions(+), 63 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index ed86e9026e8..00000000000 --- a/.env +++ /dev/null @@ -1,45 +0,0 @@ -# Smalruby3 scratch-vm Environment Variables -# Copy this file to .env and adjust values for local development - -# Mesh V2 Extension Configuration - -# AppSync GraphQL API Endpoint (Production) -# Example: https://xxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql -# stg -MESH_GRAPHQL_ENDPOINT=https://mpe3yhgk6zdxfhjbay2vqcq5qe.appsync-api.ap-northeast-1.amazonaws.com/graphql -# stg2 -# MESH_GRAPHQL_ENDPOINT=https://n4toi6es3fchzhp2ccxyg2qjqy.appsync-api.ap-northeast-1.amazonaws.com/graphql - -# AppSync API Key (Production) -# Example: da2-xxxxx -# stg -MESH_API_KEY=AIzaSyDBKkJfMiFfps8zEXubMu5XZmDGJ_AuLw8 -# stg2 -# MESH_API_KEY=da2-hpyr3rgsd5ewjoa56thwxmyjmm - -# AWS Region (Production) -# Example: ap-northeast-1 -# stg, stg2, prod -MESH_AWS_REGION=ap-northeast-1 - -# Maximum connection time in seconds -# Default: 300 (5 minutes) -MESH_MAX_CONNECTION_TIME_SECONDS=300 - -# Data update interval in milliseconds -# Default, Production: 1000 -MESH_DATA_UPDATE_INTERVAL_MS=1000 - -# Event batch interval in milliseconds -# Default, Production: 1000 -MESH_EVENT_BATCH_INTERVAL_MS=1000 - -# Host heartbeat interval in milliseconds -# Default: 60000 (60 seconds) -MESH_HOST_HEARTBEAT_INTERVAL_MS=60000 - -# Debug logging (comma-separated categories, e.g., scratch-vm:*) -DEBUG= - -# Development server port -PORT=8073 diff --git a/.env.example b/.env.example index c37588e7b11..347d1d09441 100644 --- a/.env.example +++ b/.env.example @@ -20,22 +20,20 @@ MESH_AWS_REGION=ap-northeast-1 MESH_MAX_CONNECTION_TIME_SECONDS=300 # Data update interval in milliseconds -# Default: 250 -# Production: 1000 -MESH_DATA_UPDATE_INTERVAL_MS=250 +# Default: 1000 +MESH_DATA_UPDATE_INTERVAL_MS=1000 # Event batch interval in milliseconds -# Default: 250 -# Production: 1000 -MESH_EVENT_BATCH_INTERVAL_MS=250 +# Default: 1000 +MESH_EVENT_BATCH_INTERVAL_MS=1000 # Host heartbeat interval in seconds -# Default: 15 (15 seconds) -MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=15 +# Default: 60 (60 seconds) +MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=60 # Member heartbeat interval in seconds -# Default: 15 (15 seconds) -MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS=15 +# Default: 120 (120 seconds) +MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS=120 # Debug logging (comma-separated categories, e.g., scratch-vm:*) DEBUG= diff --git a/.gitignore b/.gitignore index 76c05bbafb3..a24cfe2335d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ npm-* # Load Test /test/load-test/node_modules + +# Environment +.env diff --git a/src/extensions/scratch3_mesh_v2/mesh-service.js b/src/extensions/scratch3_mesh_v2/mesh-service.js index 8ef9f7f795f..22542da21d9 100644 --- a/src/extensions/scratch3_mesh_v2/mesh-service.js +++ b/src/extensions/scratch3_mesh_v2/mesh-service.js @@ -79,10 +79,10 @@ class MeshV2Service { this.remoteData = {}; // Rate limiters - // Data update interval (default: 250ms, production: 1000ms) + // Data update interval (default: 1000ms) const dataInterval = parseEnvInt( process.env.MESH_DATA_UPDATE_INTERVAL_MS, - 250, // default + 1000, // default 100, // min: 100ms 10000 // max: 10 seconds ); @@ -93,10 +93,10 @@ class MeshV2Service { // Event queue for batch sending: { eventName, payload, firedAt } の配列 this.eventQueue = []; - // Event batch interval (default: 250ms, production: 1000ms) + // Event batch interval (default: 1000ms) this.eventBatchInterval = parseEnvInt( process.env.MESH_EVENT_BATCH_INTERVAL_MS, - 250, // default + 1000, // default 100, // min: 100ms 10000 // max: 10 seconds ); @@ -690,19 +690,19 @@ class MeshV2Service { if (!this.groupId) return; log.info(`Mesh V2: Starting heartbeat timer (Role: ${this.isHost ? 'Host' : 'Member'})`); - // Use 15s for host, memberHeartbeatInterval for member (default 15s) + // Use 60s for host, memberHeartbeatInterval for member (default 120s) const hostInterval = parseEnvInt( process.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS, - 15, // default: 15 seconds + 60, // default: 60 seconds 1, // min: 1 second 300 // max: 5 minutes ) * 1000; const memberDefaultInterval = parseEnvInt( process.env.MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS, - 15, // default: 15 seconds + 120, // default: 120 seconds 1, // min: 1 second - 300 // max: 5 minutes + 600 // max: 10 minutes ) * 1000; const interval = this.isHost ? hostInterval : (this.memberHeartbeatInterval * 1000 || memberDefaultInterval); From a5ea13f5000a9ff88dd5aefdc20de44d79208ebd Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 4 Jan 2026 00:31:54 +0900 Subject: [PATCH 5/8] refactor: remove redundant MESH_MAX_CONNECTION_TIME_SECONDS - Removed CONNECTION_TIMEOUT fallback in mesh-service.js - startConnectionTimer now relies solely on expiresAt from server - Removed variable from .env.example and webpack.config.js --- .env.example | 4 ---- .../scratch3_mesh_v2/mesh-service.js | 20 ++++++++----------- webpack.config.js | 1 - 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/.env.example b/.env.example index 347d1d09441..4d0a82a3de9 100644 --- a/.env.example +++ b/.env.example @@ -15,10 +15,6 @@ MESH_API_KEY=da2-your-api-key # Example: ap-northeast-1 MESH_AWS_REGION=ap-northeast-1 -# Maximum connection time in seconds -# Default: 300 (5 minutes) -MESH_MAX_CONNECTION_TIME_SECONDS=300 - # Data update interval in milliseconds # Default: 1000 MESH_DATA_UPDATE_INTERVAL_MS=1000 diff --git a/src/extensions/scratch3_mesh_v2/mesh-service.js b/src/extensions/scratch3_mesh_v2/mesh-service.js index 22542da21d9..1ce616c1bb3 100644 --- a/src/extensions/scratch3_mesh_v2/mesh-service.js +++ b/src/extensions/scratch3_mesh_v2/mesh-service.js @@ -34,12 +34,6 @@ const parseEnvInt = (envVar, defaultValue, min = 0, max = Infinity) => { }; // Mesh v2 configuration parameters -const CONNECTION_TIMEOUT = parseEnvInt( - process.env.MESH_MAX_CONNECTION_TIME_SECONDS, - 300, // Default: 5 minutes (300 seconds) - 60, // min: 1 minute - 7200 // max: 2 hours -) * 1000; // Convert to milliseconds /** * GraphQL error types that indicate the connection is no longer valid. @@ -789,13 +783,15 @@ class MeshV2Service { startConnectionTimer () { this.stopConnectionTimer(); - let timeout = CONNECTION_TIMEOUT; - if (this.expiresAt) { - const serverTimeout = new Date(this.expiresAt).getTime() - Date.now(); - if (serverTimeout > 0) { - timeout = serverTimeout; - } + if (!this.expiresAt) return; + + const timeout = new Date(this.expiresAt).getTime() - Date.now(); + if (timeout <= 0) { + log.warn('Mesh V2: Group is already expired'); + this.leaveGroup(); + return; } + const timeoutMinutes = Math.round(timeout / 60000); this.connectionTimer = setTimeout(() => { log.warn(`Mesh V2: Connection timeout (${timeoutMinutes} minutes)`); diff --git a/webpack.config.js b/webpack.config.js index 1df678e99dc..9eeeb6abdac 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -77,7 +77,6 @@ const webBuilder = new ScratchWebpackConfigBuilder(common) 'process.env.MESH_GRAPHQL_ENDPOINT': JSON.stringify(process.env.MESH_GRAPHQL_ENDPOINT), 'process.env.MESH_API_KEY': JSON.stringify(process.env.MESH_API_KEY), 'process.env.MESH_AWS_REGION': JSON.stringify(process.env.MESH_AWS_REGION), - 'process.env.MESH_MAX_CONNECTION_TIME_SECONDS': JSON.stringify(process.env.MESH_MAX_CONNECTION_TIME_SECONDS), 'process.env.MESH_DATA_UPDATE_INTERVAL_MS': JSON.stringify(process.env.MESH_DATA_UPDATE_INTERVAL_MS), 'process.env.MESH_EVENT_BATCH_INTERVAL_MS': JSON.stringify(process.env.MESH_EVENT_BATCH_INTERVAL_MS), 'process.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS': From 96714e068bee3909064a0203e6e5c09b15c30caa Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 4 Jan 2026 00:41:00 +0900 Subject: [PATCH 6/8] refactor: use server-provided heartbeat interval in MeshV2Service - Added heartbeatIntervalSeconds to CREATE_GROUP mutation - Updated createGroup, renewHeartbeat, and joinGroup to capture interval from server - Updated startHeartbeat to use server interval instead of environment variables - Removed MESH_HOST_HEARTBEAT_INTERVAL_SECONDS and MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS --- .env.example | 8 ------ .../scratch3_mesh_v2/gql-operations.js | 1 + .../scratch3_mesh_v2/mesh-service.js | 28 ++++++++----------- webpack.config.js | 6 +--- 4 files changed, 14 insertions(+), 29 deletions(-) diff --git a/.env.example b/.env.example index 4d0a82a3de9..c19992b7e29 100644 --- a/.env.example +++ b/.env.example @@ -23,14 +23,6 @@ MESH_DATA_UPDATE_INTERVAL_MS=1000 # Default: 1000 MESH_EVENT_BATCH_INTERVAL_MS=1000 -# Host heartbeat interval in seconds -# Default: 60 (60 seconds) -MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=60 - -# Member heartbeat interval in seconds -# Default: 120 (120 seconds) -MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS=120 - # Debug logging (comma-separated categories, e.g., scratch-vm:*) DEBUG= diff --git a/src/extensions/scratch3_mesh_v2/gql-operations.js b/src/extensions/scratch3_mesh_v2/gql-operations.js index 9a2b25f5361..19ee27b244d 100644 --- a/src/extensions/scratch3_mesh_v2/gql-operations.js +++ b/src/extensions/scratch3_mesh_v2/gql-operations.js @@ -31,6 +31,7 @@ const CREATE_GROUP = gql` hostId createdAt expiresAt + heartbeatIntervalSeconds } } `; diff --git a/src/extensions/scratch3_mesh_v2/mesh-service.js b/src/extensions/scratch3_mesh_v2/mesh-service.js index 1ce616c1bb3..b7295043ad6 100644 --- a/src/extensions/scratch3_mesh_v2/mesh-service.js +++ b/src/extensions/scratch3_mesh_v2/mesh-service.js @@ -67,6 +67,7 @@ class MeshV2Service { // Last data send promise to track completion of the most recent data transmission this.lastDataSendPromise = Promise.resolve(); + this.hostHeartbeatInterval = 60; // Default 1 min this.memberHeartbeatInterval = 120; // Default 2 min // Data from other nodes: { nodeId: { key: { value: string, timestamp: number } } } @@ -226,6 +227,9 @@ class MeshV2Service { this.domain = group.domain; // Update domain from server this.expiresAt = group.expiresAt; this.isHost = true; + if (group.heartbeatIntervalSeconds) { + this.hostHeartbeatInterval = group.heartbeatIntervalSeconds; + } this.costTracking.connectionStartTime = Date.now(); this.startSubscriptions(); @@ -684,22 +688,7 @@ class MeshV2Service { if (!this.groupId) return; log.info(`Mesh V2: Starting heartbeat timer (Role: ${this.isHost ? 'Host' : 'Member'})`); - // Use 60s for host, memberHeartbeatInterval for member (default 120s) - const hostInterval = parseEnvInt( - process.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS, - 60, // default: 60 seconds - 1, // min: 1 second - 300 // max: 5 minutes - ) * 1000; - - const memberDefaultInterval = parseEnvInt( - process.env.MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS, - 120, // default: 120 seconds - 1, // min: 1 second - 600 // max: 10 minutes - ) * 1000; - - const interval = this.isHost ? hostInterval : (this.memberHeartbeatInterval * 1000 || memberDefaultInterval); + const interval = (this.isHost ? this.hostHeartbeatInterval : this.memberHeartbeatInterval) * 1000; this.heartbeatTimer = setInterval(() => { if (this.isHost) { @@ -734,6 +723,13 @@ class MeshV2Service { }); this.expiresAt = result.data.renewHeartbeat.expiresAt; log.info(`Mesh V2: Heartbeat renewed. Expires at: ${this.expiresAt}`); + if (result.data.renewHeartbeat.heartbeatIntervalSeconds) { + const newInterval = result.data.renewHeartbeat.heartbeatIntervalSeconds; + if (newInterval !== this.hostHeartbeatInterval) { + this.hostHeartbeatInterval = newInterval; + this.startHeartbeat(); // Restart with new interval + } + } this.startConnectionTimer(); return result.data.renewHeartbeat; } catch (error) { diff --git a/webpack.config.js b/webpack.config.js index 9eeeb6abdac..dfc05d55352 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -78,11 +78,7 @@ const webBuilder = new ScratchWebpackConfigBuilder(common) 'process.env.MESH_API_KEY': JSON.stringify(process.env.MESH_API_KEY), 'process.env.MESH_AWS_REGION': JSON.stringify(process.env.MESH_AWS_REGION), 'process.env.MESH_DATA_UPDATE_INTERVAL_MS': JSON.stringify(process.env.MESH_DATA_UPDATE_INTERVAL_MS), - 'process.env.MESH_EVENT_BATCH_INTERVAL_MS': JSON.stringify(process.env.MESH_EVENT_BATCH_INTERVAL_MS), - 'process.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS': - JSON.stringify(process.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS), - 'process.env.MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS': - JSON.stringify(process.env.MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS) + 'process.env.MESH_EVENT_BATCH_INTERVAL_MS': JSON.stringify(process.env.MESH_EVENT_BATCH_INTERVAL_MS) })); const playgroundBuilder = webBuilder.clone() From 5bf166750edcf2a88c43a0dfcbb8932549560b80 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 4 Jan 2026 10:53:29 +0900 Subject: [PATCH 7/8] test: update mock expiresAt to future date to pass CI - Fixed unit tests failing due to immediate group expiration when expiresAt was in the past. - Added missing heartbeatIntervalSeconds to mock responses. Co-Authored-By: Gemini --- test/unit/extension_mesh_v2_service.js | 12 ++++++++---- test/unit/mesh_service_v2_global_vars.js | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/test/unit/extension_mesh_v2_service.js b/test/unit/extension_mesh_v2_service.js index f241d1d74d0..8347edfa0f0 100644 --- a/test/unit/extension_mesh_v2_service.js +++ b/test/unit/extension_mesh_v2_service.js @@ -35,18 +35,22 @@ test('MeshV2Service Cost Tracking', t => { id: 'g1', name: 'G1', domain: 'd1', - expiresAt: '2026-01-01T00:00:00Z' + expiresAt: '2099-01-01T00:00:00Z', + heartbeatIntervalSeconds: 60 }, joinGroup: { id: 'n1', domain: 'd1', - expiresAt: '2026-01-01T00:00:00Z' + expiresAt: '2099-01-01T00:00:00Z', + heartbeatIntervalSeconds: 120 }, renewHeartbeat: { - expiresAt: '2026-01-01T00:00:00Z' + expiresAt: '2099-01-01T00:00:00Z', + heartbeatIntervalSeconds: 60 }, sendMemberHeartbeat: { - expiresAt: '2026-01-01T00:00:00Z' + expiresAt: '2099-01-01T00:00:00Z', + heartbeatIntervalSeconds: 120 } } }), diff --git a/test/unit/mesh_service_v2_global_vars.js b/test/unit/mesh_service_v2_global_vars.js index e2207e04589..c0dcd80c42a 100644 --- a/test/unit/mesh_service_v2_global_vars.js +++ b/test/unit/mesh_service_v2_global_vars.js @@ -104,7 +104,8 @@ test('MeshV2Service Global Variables', t => { id: 'group1', name: 'groupName', domain: 'domain1', - expiresAt: '2025-12-30T12:00:00Z' + expiresAt: '2099-01-01T00:00:00Z', + heartbeatIntervalSeconds: 60 } } }), From fc79a7074148e891de8096601a92e92cadffed44 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Sun, 4 Jan 2026 11:09:47 +0900 Subject: [PATCH 8/8] test: suppress excessive logs in Mesh V2 unit tests - Suppressed info/debug logs in Mesh V2 tests for better readability. - Kept info enabled in extension_mesh_v2_service.js for log verification. Co-Authored-By: Gemini --- test/unit/extension_mesh_v2.js | 5 +++++ test/unit/extension_mesh_v2_integration.js | 5 +++++ test/unit/extension_mesh_v2_issue66.js | 5 +++++ test/unit/extension_mesh_v2_service.js | 4 ++++ test/unit/mesh_service_v2.js | 5 +++++ test/unit/mesh_service_v2_global_vars.js | 5 +++++ test/unit/mesh_service_v2_integration.js | 5 +++++ test/unit/mesh_service_v2_order.js | 5 +++++ test/unit/mesh_service_v2_subscription.js | 5 +++++ test/unit/mesh_service_v2_timestamp.js | 5 +++++ 10 files changed, 49 insertions(+) diff --git a/test/unit/extension_mesh_v2.js b/test/unit/extension_mesh_v2.js index 757b58be043..fb8ca98a9b2 100644 --- a/test/unit/extension_mesh_v2.js +++ b/test/unit/extension_mesh_v2.js @@ -1,4 +1,9 @@ const test = require('tap').test; +const minilog = require('minilog'); +// Suppress debug and info logs during tests +minilog.suggest.deny('vm', 'debug'); +minilog.suggest.deny('vm', 'info'); + const URLSearchParams = require('url').URLSearchParams; const MeshV2Blocks = require('../../src/extensions/scratch3_mesh_v2/index.js'); const Variable = require('../../src/engine/variable'); diff --git a/test/unit/extension_mesh_v2_integration.js b/test/unit/extension_mesh_v2_integration.js index 58644d72c28..7cab50b7579 100644 --- a/test/unit/extension_mesh_v2_integration.js +++ b/test/unit/extension_mesh_v2_integration.js @@ -1,4 +1,9 @@ const test = require('tap').test; +const minilog = require('minilog'); +// Suppress debug and info logs during tests +minilog.suggest.deny('vm', 'debug'); +minilog.suggest.deny('vm', 'info'); + const URLSearchParams = require('url').URLSearchParams; const MeshBlocks = require('../../src/extensions/scratch3_mesh/index.js'); const MeshV2Blocks = require('../../src/extensions/scratch3_mesh_v2/index.js'); diff --git a/test/unit/extension_mesh_v2_issue66.js b/test/unit/extension_mesh_v2_issue66.js index 66c28ebce71..6b6d7a4a8be 100644 --- a/test/unit/extension_mesh_v2_issue66.js +++ b/test/unit/extension_mesh_v2_issue66.js @@ -1,4 +1,9 @@ const test = require('tap').test; +const minilog = require('minilog'); +// Suppress debug and info logs during tests +minilog.suggest.deny('vm', 'debug'); +minilog.suggest.deny('vm', 'info'); + const URLSearchParams = require('url').URLSearchParams; const MeshV2Blocks = require('../../src/extensions/scratch3_mesh_v2/index.js'); const Variable = require('../../src/engine/variable'); diff --git a/test/unit/extension_mesh_v2_service.js b/test/unit/extension_mesh_v2_service.js index 8347edfa0f0..364d704c177 100644 --- a/test/unit/extension_mesh_v2_service.js +++ b/test/unit/extension_mesh_v2_service.js @@ -1,5 +1,9 @@ /* eslint-disable require-atomic-updates */ const test = require('tap').test; +const minilog = require('minilog'); +// Suppress debug logs during tests +minilog.suggest.deny('vm', 'debug'); + const MeshV2Service = require('../../src/extensions/scratch3_mesh_v2/mesh-service'); const log = require('../../src/util/log'); diff --git a/test/unit/mesh_service_v2.js b/test/unit/mesh_service_v2.js index b7acf643ed4..66d29793841 100644 --- a/test/unit/mesh_service_v2.js +++ b/test/unit/mesh_service_v2.js @@ -1,4 +1,9 @@ const test = require('tap').test; +const minilog = require('minilog'); +// Suppress debug and info logs during tests +minilog.suggest.deny('vm', 'debug'); +minilog.suggest.deny('vm', 'info'); + const MeshV2Service = require('../../src/extensions/scratch3_mesh_v2/mesh-service'); const {FIRE_EVENTS} = require('../../src/extensions/scratch3_mesh_v2/gql-operations'); const BlockUtility = require('../../src/engine/block-utility'); diff --git a/test/unit/mesh_service_v2_global_vars.js b/test/unit/mesh_service_v2_global_vars.js index c0dcd80c42a..d1efe7ae606 100644 --- a/test/unit/mesh_service_v2_global_vars.js +++ b/test/unit/mesh_service_v2_global_vars.js @@ -1,4 +1,9 @@ const test = require('tap').test; +const minilog = require('minilog'); +// Suppress debug and info logs during tests +minilog.suggest.deny('vm', 'debug'); +minilog.suggest.deny('vm', 'info'); + const MeshV2Service = require('../../src/extensions/scratch3_mesh_v2/mesh-service'); const Variable = require('../../src/engine/variable'); diff --git a/test/unit/mesh_service_v2_integration.js b/test/unit/mesh_service_v2_integration.js index 05149cdda2e..889a5ff1676 100644 --- a/test/unit/mesh_service_v2_integration.js +++ b/test/unit/mesh_service_v2_integration.js @@ -1,4 +1,9 @@ const test = require('tap').test; +const minilog = require('minilog'); +// Suppress debug and info logs during tests +minilog.suggest.deny('vm', 'debug'); +minilog.suggest.deny('vm', 'info'); + const MeshV2Service = require('../../src/extensions/scratch3_mesh_v2/mesh-service'); const BlockUtility = require('../../src/engine/block-utility'); diff --git a/test/unit/mesh_service_v2_order.js b/test/unit/mesh_service_v2_order.js index de576565d9b..1bd5cda4054 100644 --- a/test/unit/mesh_service_v2_order.js +++ b/test/unit/mesh_service_v2_order.js @@ -1,4 +1,9 @@ const test = require('tap').test; +const minilog = require('minilog'); +// Suppress debug and info logs during tests +minilog.suggest.deny('vm', 'debug'); +minilog.suggest.deny('vm', 'info'); + const MeshV2Service = require('../../src/extensions/scratch3_mesh_v2/mesh-service'); const {REPORT_DATA, FIRE_EVENTS} = require('../../src/extensions/scratch3_mesh_v2/gql-operations'); diff --git a/test/unit/mesh_service_v2_subscription.js b/test/unit/mesh_service_v2_subscription.js index 62b6cae4593..7428db2ff75 100644 --- a/test/unit/mesh_service_v2_subscription.js +++ b/test/unit/mesh_service_v2_subscription.js @@ -1,4 +1,9 @@ const test = require('tap').test; +const minilog = require('minilog'); +// Suppress debug and info logs during tests +minilog.suggest.deny('vm', 'debug'); +minilog.suggest.deny('vm', 'info'); + const MeshV2Service = require('../../src/extensions/scratch3_mesh_v2/mesh-service'); const {ON_MESSAGE_IN_GROUP} = require('../../src/extensions/scratch3_mesh_v2/gql-operations'); diff --git a/test/unit/mesh_service_v2_timestamp.js b/test/unit/mesh_service_v2_timestamp.js index 3b0c75fe64c..4523b852b9d 100644 --- a/test/unit/mesh_service_v2_timestamp.js +++ b/test/unit/mesh_service_v2_timestamp.js @@ -1,4 +1,9 @@ const test = require('tap').test; +const minilog = require('minilog'); +// Suppress debug and info logs during tests +minilog.suggest.deny('vm', 'debug'); +minilog.suggest.deny('vm', 'info'); + const MeshV2Service = require('../../src/extensions/scratch3_mesh_v2/mesh-service'); const createMockBlocks = () => ({