Skip to main content
Events are the primary communication mechanism in Multisynq applications. The publish-subscribe (pub-sub) pattern enables clean, decoupled communication between Models and Views while maintaining perfect synchronization.

Core Event API

These functions are only available to classes that inherit from Multisynq.Model or Multisynq.View.
  • publish()
  • subscribe()
  • unsubscribe()
  • unsubscribeAll()
Sends an event to all subscribers
publish(scope, event, data)
  • scope: Namespace for the event (String)
  • event: Name of the event (String)
  • data: Optional data payload (any serializable type)
// Example: Publishing player input
this.publish("player-123", "move", { direction: "left", speed: 5 });

Handler Requirements

Critical difference: Model and View handlers have different requirements!
  • Model Handlers
  • View Handlers
Must use method names, not function expressions
// ✅ Correct - use method name
class GameModel extends Multisynq.Model {
    init() {
        this.subscribe("input", "move", this.handleMove);
    }
    
    handleMove(data) {
        // Handle the move
    }
}
// ❌ Incorrect - function expressions don't work
class GameModel extends Multisynq.Model {
    init() {
        this.subscribe("input", "move", (data) => {
            // This won't work in models!
        });
    }
}
This is because functions cannot be serialized, so only the method name is stored.

Event Routing Patterns

Understanding event routing is crucial for proper Multisynq development:
  • View → Model
  • Model → View
  • Model → Model
  • View → View
Events are transmitted to the synchronizer and mirrored to all users
// View publishes
this.publish("game", "player-action", { type: "jump" });

// Model receives on ALL devices during next simulation
this.subscribe("game", "player-action", this.handlePlayerAction);
Use case: User input, game actions, any synchronized state changes

Scopes and Namespacing

Scopes provide namespacing to avoid event name conflicts and enable targeted communication:
  • Global Scopes
  • Model ID Scopes
  • Custom Scopes
Built-in scopes available everywhere
// Session-wide events
this.subscribe(this.sessionId, "game-start", this.handleGameStart);

// User-specific events
this.subscribe(this.viewId, "user-input", this.handleUserInput);

// Public events
this.subscribe("public", "announcement", this.handleAnnouncement);

Practical Examples

Complete input handling pattern
class GameView extends Multisynq.View {
    init() {
        document.addEventListener('keydown', (e) => {
            // Send input to model
            this.publish(this.viewId, "input", {
                key: e.key,
                action: "down"
            });
        });
        
        document.addEventListener('keyup', (e) => {
            this.publish(this.viewId, "input", {
                key: e.key,
                action: "up"
            });
        });
    }
}

class GameModel extends Multisynq.Model {
    init() {
        this.players = new Map();
        // Listen for all player inputs
        this.subscribe("*", "input", this.handleInput);
    }
    
    handleInput(data) {
        const playerId = this.getEventSender();
        const player = this.players.get(playerId);
        if (player) {
            player.processInput(data);
        }
    }
}
Model-triggered visual effects
class GameModel extends Multisynq.Model {
    explodeAsteroid(asteroid) {
        // Destroy the asteroid
        asteroid.destroy();
        
        // Trigger visual effect locally
        this.publish("effects", "explosion", {
            x: asteroid.x,
            y: asteroid.y,
            size: asteroid.size
        });
    }
}

class GameView extends Multisynq.View {
    init() {
        this.particles = [];
        this.subscribe("effects", "explosion", this.createExplosion);
    }
    
    createExplosion(data) {
        // Create particle effect
        for (let i = 0; i < 10; i++) {
            this.particles.push(new Particle(data.x, data.y));
        }
    }
}
Agent-specific communication channels
class AIAgent extends Multisynq.Model {
    init() {
        // Each agent has its own communication channel
        this.subscribe(this.id, "command", this.handleCommand);
        this.subscribe(this.id, "sensor-data", this.processSensorData);
    }
    
    handleCommand(command) {
        switch(command.type) {
            case "move":
                this.moveTo(command.target);
                break;
            case "attack":
                this.attackTarget(command.target);
                break;
        }
    }
    
    reportStatus() {
        // Report to central command
        this.publish("command", "agent-status", {
            id: this.id,
            status: this.status,
            position: { x: this.x, y: this.y }
        });
    }
}

Best Practices

🎯 Targeted Communication

Use specific scopes for efficient communication
// ✅ Good - specific scope
this.publish(playerId, "player-event", data);

// ❌ Avoid - broad scope when specific is better
this.publish("global", "player-event", data);

📦 Small Payloads

Keep event data small and simple
// ✅ Good - minimal data
this.publish("input", "move", { direction: "left" });

// ❌ Avoid - large data structures
this.publish("input", "move", { 
    player: entirePlayerObject,
    gameState: entireGameState
});

🔄 Avoid Chains

Don’t create Model → View → Model chains
// ❌ Avoid this pattern
class GameModel extends Multisynq.Model {
    someMethod() {
        // Model triggers view event
        this.publish("ui", "update", data);
    }
}

class GameView extends Multisynq.View {
    init() {
        this.subscribe("ui", "update", (data) => {
            // View triggers model event - BAD!
            this.publish("model", "response", processedData);
        });
    }
}

🧹 Clean Up

Unsubscribe when no longer needed
class TemporaryComponent extends Multisynq.View {
    init() {
        this.subscribe("temp", "event", this.handler);
    }
    
    destroy() {
        this.unsubscribe("temp", "event");
        // Or use unsubscribeAll() for all subscriptions
        super.destroy();
    }
}

Common Patterns

  • State Updates
  • Lifecycle Events
  • Timer Events
Centralized state management
class GameState extends Multisynq.Model {
    init() {
        this.score = 0;
        this.level = 1;
        
        this.subscribe("game", "score", this.updateScore);
        this.subscribe("game", "level", this.updateLevel);
    }
    
    updateScore(points) {
        this.score += points;
        this.publish("ui", "score-changed", this.score);
    }
    
    updateLevel(newLevel) {
        this.level = newLevel;
        this.publish("ui", "level-changed", this.level);
    }
}

Event Debugging

  • Event Logging
  • Event Validation
Track event flow in development
class DebugModel extends Multisynq.Model {
    publish(scope, event, data) {
        console.log(`[MODEL] Publishing: ${scope}:${event}`, data);
        super.publish(scope, event, data);
    }
    
    subscribe(scope, event, handler) {
        console.log(`[MODEL] Subscribing to: ${scope}:${event}`);
        super.subscribe(scope, event, handler);
    }
}

Performance Considerations

Avoid these performance pitfalls:
  1. High-frequency events: Don’t publish events every frame
  2. Large payloads: Keep event data minimal
  3. Circular subscriptions: Avoid event loops
  4. Unused subscriptions: Always clean up when done
  • Throttling
  • Batching
Limit event frequency
class ThrottledInput extends Multisynq.View {
    init() {
        this.lastMove = 0;
        document.addEventListener('mousemove', this.handleMouseMove);
    }
    
    handleMouseMove(e) {
        const now = Date.now();
        if (now - this.lastMove < 16) return; // ~60fps max
        
        this.lastMove = now;
        this.publish("input", "mouse-move", {
            x: e.clientX,
            y: e.clientY
        });
    }
}

Next Steps

Events are the nervous system of Multisynq applications. Understanding when and how to use them effectively is crucial for building responsive, maintainable multiplayer experiences.
I