Last Updated: 4/7/2026
Worker Command System
Agricultural Microworlds executes student-written Blockly programs in Web Workers to keep the simulation running smoothly. This architecture prevents long-running or infinite loops in student code from freezing the browser UI. Understanding this system is essential for adding new blocks that interact with the simulation.
Architecture Overview
The worker command system uses a request/response pattern:
- Blockly generates JavaScript code that calls
simulationMethodsfunctions - Worker sends command messages to the main thread with command name and arguments
- simulationEngine processes commands and updates simulation state
- Engine sends response back to worker when async operations complete
- Worker resumes execution after receiving the response
This allows student code to await simulation actions (like movement or waiting) while the simulation loop continues running on the main thread.
Worker Creation and Execution
When the user clicks “Run”, SimulationControlsContainer creates workers for each vehicle’s code:
const spawnWorker = (userCode, vType) => {
if (!userCode.trim()) return;
this.expectedWorkers++;
const blob = this.createWorkerBlob(userCode);
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = (e) => {
if (e.data.type === "COMMAND") {
this.simulationEngine.handleWorkerMessage(
{ ...e.data, vehicleType: vType },
worker,
);
} else if (e.data.type === "DONE") {
this.completedWorkers++;
if (this.completedWorkers === this.expectedWorkers) {
this.stopButtonOnClick(); // Auto-stop when both finish
}
}
};
this.workers.push(worker);
};
// 0 = HARVESTER, 1 = SEEDER
spawnWorker(harvesterCode, 0);
spawnWorker(seederCode, 1);Key points:
- Each vehicle (harvester and seeder) gets its own worker
- Workers run independently and can execute commands simultaneously
- The simulation auto-stops when all workers complete (
type: "DONE") vehicleTypeis attached to each command to route it to the correct vehicle
Worker Blob Construction
The createWorkerBlob() method wraps user code with the simulationMethods API:
createWorkerBlob(userCode) {
const workerScript = `
const simulationMethods = {
_send: function(command, args) {
return new Promise((resolve) => {
const reqId = Math.random().toString(36).substring(7);
self.postMessage({
type: 'COMMAND',
command: command,
args: args,
requestId: reqId
});
const listener = (e) => {
if (e.data.type === 'RESPONSE' && e.data.requestId === reqId) {
self.removeEventListener('message', listener);
resolve(e.data.result);
}
};
self.addEventListener('message', listener);
});
},
moveForward: function(d) { return this._send('moveForward', [d]); },
turnXDegrees: function(d) { return this._send('turnXDegrees', [d]); },
waitXWeeks: function(d) { return this._send('waitXWeeks', [d]); },
toggleHarvesting: function(b) { return this._send('toggleHarvesting', [b]); },
toggleSeeding: function(b) { return this._send('toggleSeeding', [b]); },
switchCropBeingPlanted: function(c) { return this._send('switchCropBeingPlanted', [c]); }
};
async function runUserCode() {
${userCode}
}
runUserCode().then(() => {
self.postMessage({ type: 'DONE' });
}).catch(err => {
console.error("Worker Error:", err);
});
`;
return new Blob([workerScript], { type: "application/javascript" });
}The _send Helper
The _send method implements the request/response pattern:
- Generate unique requestId: Random string to match responses to requests
- Post command message: Sends
{ type: 'COMMAND', command, args, requestId } - Set up listener: Waits for
{ type: 'RESPONSE', requestId }message - Return Promise: Resolves when matching response arrives
This allows worker code to await commands:
await simulationMethods.moveForward(5); // Waits for movement to complete
await simulationMethods.turnXDegrees(90); // Then waits for turn to completeAvailable Commands
The simulationMethods object exposes these commands:
| Command | Parameters | Async? | Purpose |
|---|---|---|---|
moveForward(duration) | duration (seconds) | Yes | Move vehicle forward for specified time |
turnXDegrees(angle) | angle (degrees, ± for direction) | Yes | Rotate vehicle by angle |
waitXWeeks(weeks) | weeks (simulation weeks) | Yes | Pause vehicle, advance simulation time |
toggleHarvesting(isOn) | isOn (boolean) | No | Enable/disable harvesting mode |
toggleSeeding(isOn) | isOn (boolean) | No | Enable/disable seeding mode |
switchCropBeingPlanted(cropType) | cropType (CROP_TYPES enum) | No | Change which crop the seeder plants |
CheckIfPlantInFront(stage) | stage (CROP_STAGES enum) | No | Check if tile ahead matches stage |
Async commands return Promises that resolve when the action completes. Sync commands return immediately after updating state.
Engine Command Handler
The simulationEngine.handleWorkerMessage() method routes commands to simulation methods:
async handleWorkerMessage(data, worker) {
if (!this.isRunning) return;
const { command, args, vehicleType, requestId } = data;
// Route the string command to the actual simulation API
switch (command) {
case "moveForward":
await this.moveForward(args[0], vehicleType);
break;
case "turnXDegrees":
await this.turnXDegrees(args[0], vehicleType);
break;
case "waitXWeeks":
await this.waitXWeeks(args[0], vehicleType);
break;
case "toggleHarvesting":
this.toggleHarvesting(args[0], vehicleType);
break;
case "toggleSeeding":
this.toggleSeeding(args[0], vehicleType);
break;
case "switchCropBeingPlanted":
this.switchCropBeingPlanted(args[0], vehicleType);
break;
default:
console.warn("Unknown worker command:", command);
break;
}
// Once the await finishes, send response back to worker
if (worker) {
worker.postMessage({
type: "RESPONSE",
requestId: requestId,
result: true,
});
}
}Key behavior:
- Commands are executed with
await, so the handler waits for async operations - The
vehicleTypeparameter routes commands to the correct vehicle (harvester or seeder) - After completion, a
RESPONSEmessage is sent back with the matchingrequestId - Unknown commands log a warning but don’t crash the simulation
Active Task System
Async commands (movement, turning, waiting) use the activeTasks map to track in-progress operations:
async moveForward(durationInSeconds, targetVehicleType) {
const mySessionId = this.simulationSessionId;
const vehicle = this.getTargetVehicle(targetVehicleType);
if (vehicle) vehicle.isMoving = true;
return new Promise((resolve) => {
if (vehicle) {
this.activeTasks.set(vehicle.type, {
type: "TIMER",
timeLeft: Number(durationInSeconds),
resolve: resolve,
sessionId: mySessionId,
});
} else {
resolve();
}
});
}The simulation loop decrements timeLeft each frame:
this.activeTasks.forEach((task, vehicleType) => {
if (task.sessionId !== this.simulationSessionId) {
this.activeTasks.clear();
} else {
if (task.type === "TIMER") {
task.timeLeft -= simDeltaTime;
if (task.timeLeft <= 0) {
if (nextStates.vehicles) {
const vehicle = nextStates.vehicles.find(
(v) => v.type == vehicleType,
);
if (vehicle) vehicle.isMoving = false;
}
this.resolveActiveTask(vehicleType);
}
} else if (task.type === "TURN") {
const v = nextStates.vehicles?.find((v) => v.type == vehicleType);
if (v) {
const diff = Math.abs(v.goalAngle - v.angle);
if (diff < 0.5) {
v.angle = v.goalAngle;
this.resolveActiveTask(vehicleType);
}
}
}
}
});When the task completes, resolveActiveTask() calls the stored resolve() function, which unblocks the worker’s await.
Task Types
TIMER tasks (moveForward, waitXWeeks):
- Decrement
timeLeftbysimDeltaTimeeach frame - Resolve when
timeLeft <= 0 - Used for time-based actions
TURN tasks (turnXDegrees):
- Check angle difference each frame
- Resolve when
|goalAngle - angle| < 0.5 - Used for rotation actions
Complete Flow Example
Here’s the full flow for a move_forward block:
-
Blockly generates code:
await simulationMethods.moveForward(5); -
Worker executes code, calls
_send('moveForward', [5]) -
Worker posts message:
{ type: 'COMMAND', command: 'moveForward', args: [5], requestId: 'abc123' } -
Main thread receives message, calls
handleWorkerMessage() -
Engine calls
moveForward(5, vehicleType):- Sets
vehicle.isMoving = true - Creates TIMER task with
timeLeft: 5 - Returns Promise
- Sets
-
Simulation loop runs:
- Each frame, decrements
task.timeLeftbydeltaTime - After ~5 seconds of simulation time,
timeLeft <= 0
- Each frame, decrements
-
Engine resolves task:
- Calls
task.resolve() - Sets
vehicle.isMoving = false
- Calls
-
Engine posts response:
{ type: 'RESPONSE', requestId: 'abc123', result: true } -
Worker receives response:
- Matches
requestIdto pending request - Resolves the Promise from step 2
- Matches
-
Worker continues execution to the next line of code
Adding a New Command
To add a new worker command:
1. Add to simulationMethods
In createWorkerBlob(), add the method:
const simulationMethods = {
// ... existing methods
myNewCommand: function(arg1, arg2) {
return this._send('myNewCommand', [arg1, arg2]);
}
};2. Add Engine Handler
In handleWorkerMessage(), add a case:
switch (command) {
// ... existing cases
case "myNewCommand":
await this.myNewCommand(args[0], args[1], vehicleType);
break;
}3. Implement Engine Method
In simulationEngine.js, add the method:
async myNewCommand(arg1, arg2, targetVehicleType) {
const vehicle = this.getTargetVehicle(targetVehicleType);
// Implement your logic here
// For async commands, return a Promise with an active task
return new Promise((resolve) => {
this.activeTasks.set(vehicle.type, {
type: "CUSTOM_TYPE",
// ... task data
resolve: resolve,
sessionId: this.simulationSessionId,
});
});
// For sync commands, just update state and return
}4. Update Block Generator
In blocklyJSGenerator.js, generate the call:
javascriptGenerator.forBlock["my_new_block"] = function (block, generator) {
const arg1 = generator.valueToCode(block, "ARG1", generator.ORDER_ATOMIC) || "0";
const arg2 = block.getFieldValue("ARG2");
return `await simulationMethods.myNewCommand(${arg1}, ${arg2});\n`;
};Session Management
The simulationSessionId prevents stale tasks from previous runs:
stopMovement() {
this.isRunning = false;
this.simulationSessionId++; // Increment to invalidate old tasks
this.activeTasks.clear();
cancelAnimationFrame(this.animationId);
}When the user clicks “Stop” or “Run” again:
simulationSessionIdincrements- All active tasks check
task.sessionId !== this.simulationSessionId - Mismatched tasks are ignored and cleared
This prevents a worker from a previous run from affecting the new simulation state.
Multi-Vehicle Coordination
Each vehicle has independent code and an independent worker:
// Harvester worker executes:
await simulationMethods.moveForward(10);
await simulationMethods.toggleHarvesting(true);
// Seeder worker executes (simultaneously):
await simulationMethods.moveForward(5);
await simulationMethods.toggleSeeding(true);Both workers send commands with their vehicleType (0 for harvester, 1 for seeder). The engine routes commands to the correct vehicle using getTargetVehicle():
getTargetVehicle(targetType) {
const vehicleType = targetType !== undefined
? targetType
: this.stateManager.getState("activeVehicleType");
const vehicles = this.stateManager.getState("vehicles");
if (!vehicles) return null;
return vehicles.find((v) => v.type == vehicleType);
}This allows students to program both vehicles independently, creating complex multi-vehicle behaviors.
Error Handling
Worker errors are caught and logged:
runUserCode().then(() => {
self.postMessage({ type: 'DONE' });
}).catch(err => {
console.error("Worker Error:", err);
});Unknown commands log a warning:
default:
console.warn("Unknown worker command:", command);
break;Missing vehicles are handled gracefully:
const vehicle = this.getTargetVehicle(targetVehicleType);
if (!vehicle) {
resolve(); // Resolve immediately if vehicle doesn't exist
return;
}Performance Considerations
Workers run in parallel: Multiple vehicles can execute commands simultaneously without blocking each other.
Main thread stays responsive: Long-running student code (infinite loops, large iterations) runs in the worker without freezing the UI.
Simulation loop continues: Even when workers are waiting for async commands, the simulation loop keeps running at 60 FPS.
Memory cleanup: Workers are terminated on stop:
stopButtonOnClick() {
this.simulationEngine.stopMovement();
this.workers.forEach((w) => w.terminate());
this.workers = [];
}Debugging Worker Code
To debug worker execution:
- Check console logs: Worker errors appear in the browser console
- Add console.log in worker blob: Modify
createWorkerBlob()to add logging - Inspect command messages: Log
datainhandleWorkerMessage() - Check active tasks: Log
this.activeTasksin the simulation loop - Verify requestId matching: Ensure responses match requests
Example debug logging:
async handleWorkerMessage(data, worker) {
console.log("Received command:", data.command, "with args:", data.args);
// ... rest of handler
}What’s Next
- Creating Custom Blocks: Learn how to create blocks that use these worker commands
- State Management System: Understand the simulation state updates triggered by commands
- Simulation Engine Architecture: Explore the simulation loop that processes active tasks