Skip to main content
A complete Multisynq application consists of two main components: a Model that contains all shared state and logic, and a View that handles user interface and input. This guide shows you how to structure and launch your applications.

Application Structure

Every Multisynq app requires exactly two classes: one Model and one View.
  • Basic Structure
  • Complete Game Example
Essential application template
// 1. Define your Model class
class MyModel extends Multisynq.Model {
    init() {
        // Initialize shared state and logic
        this.gameState = "waiting";
        this.players = new Map();
        this.startTime = this.now();
        
        // Subscribe to user input events
        this.subscribe("input", "player-action", this.handlePlayerAction);
        
        // Start main game loop
        this.future(1000/60).gameLoop();
    }
    
    handlePlayerAction(data) {
        // Process user input and update state
        console.log("Player action:", data);
    }
    
    gameLoop() {
        // Main simulation logic
        if (this.gameState === "playing") {
            this.updateGame();
        }
        
        // Continue the loop
        this.future(1000/60).gameLoop();
    }
    
    updateGame() {
        // Game simulation logic here
    }
}

// 2. REQUIRED: Register your model
MyModel.register("MyModel");

// 3. Define your View class
class MyView extends Multisynq.View {
    init() {
        // Get reference to the model
        this.model = this.wellKnownModel("MyModel");
        
        // Set up user interface
        this.setupUI();
        this.setupInputHandlers();
        this.startRenderLoop();
        
        // Subscribe to model events
        this.subscribe("game", "state-changed", this.updateDisplay);
    }
    
    setupUI() {
        // Create DOM elements
        this.canvas = document.getElementById('gameCanvas');
        this.ctx = this.canvas.getContext('2d');
        
        // Style the interface
        document.body.style.margin = '0';
        document.body.style.backgroundColor = '#222';
    }
    
    setupInputHandlers() {
        // Handle user input
        this.canvas.addEventListener('click', (event) => {
            const rect = this.canvas.getBoundingClientRect();
            const x = event.clientX - rect.left;
            const y = event.clientY - rect.top;
            
            // Send input to model
            this.publish("input", "player-action", { x, y, type: "click" });
        });
        
        document.addEventListener('keydown', (event) => {
            this.publish("input", "player-action", { 
                key: event.key, 
                type: "keydown" 
            });
        });
    }
    
    startRenderLoop() {
        this.render();
    }
    
    render() {
        // Clear canvas
        this.ctx.fillStyle = '#222';
        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
        
        // Draw current state
        this.drawGame();
        
        // Continue render loop
        requestAnimationFrame(() => this.render());
    }
    
    drawGame() {
        // Read model state and draw UI
        this.ctx.fillStyle = 'white';
        this.ctx.font = '24px Arial';
        this.ctx.fillText(
            `Game State: ${this.model.gameState}`, 
            20, 40
        );
        this.ctx.fillText(
            `Players: ${this.model.players.size}`, 
            20, 70
        );
    }
    
    updateDisplay() {
        // React to model changes (optional)
        console.log("Model state updated");
    }
}

// 4. Register your view
MyView.register("MyView");
Critical: Every Model subclass must call .register("ClassName") with the exact class name.

Launching a Session

You need an API key from multisynq.io/coder to launch sessions.
  • Basic Session Launch
  • Production Session Setup
Standard session setup
// Configure your session
const apiKey = "your_api_key_here";         // Get from multisynq.io/coder
const appId = "com.example.myapp";          // Unique app identifier
const name = Multisynq.App.autoSession();   // Auto-generate session name
const password = Multisynq.App.autoPassword(); // Auto-generate password

// Launch the session
Multisynq.Session.join({ 
    apiKey, 
    appId, 
    name, 
    password, 
    model: MyModel, 
    view: MyView 
});
The autoSession() and autoPassword() helpers parse URL parameters and create random values if needed.

Session Lifecycle

Understanding the session lifecycle helps you build robust applications that handle joining, leaving, and persistence correctly.
  • Session Startup Process
  • Handling User Joins/Leaves
What happens when you join a session
// When Session.join() is called, this sequence occurs:

// 1. Connect to synchronizer
console.log("Connecting to Multisynq synchronizer...");

// 2. Check for existing session state
// If session exists: Load from snapshot
// If new session: Initialize fresh

// 3. Instantiate Model
console.log("Creating model instance...");
const model = MyModel.create();

// 4. Initialize or restore model state
if (isNewSession) {
    // First user - run init()
    model.init();
    console.log("Model initialized with fresh state");
} else {
    // Joining existing session - load snapshot
    // model.init() is NOT called
    console.log("Model restored from snapshot");
}

// 5. Instantiate View
console.log("Creating view instance...");
const view = new MyView();
view.setModel(model);

// 6. Start main execution loop
console.log("Starting main loop...");
startMainLoop(model, view);
Critical: model.init() only runs for the first user in a session. Subsequent users get the model from a snapshot.

Main Loop Execution

Multisynq automatically manages the main loop, processing model events first, then view events, then rendering.
Default behavior (recommended)
// Standard session with automatic main loop
const session = await Multisynq.Session.join({
    apiKey: "your_api_key",
    appId: "com.example.app",
    name: "my-session",
    password: "my-password",
    model: MyModel,
    view: MyView
    // step: "auto" is the default
});

// The automatic main loop does this every frame:
// 1. Process all pending model events
// 2. Process all pending view events  
// 3. Call view.render() if it exists
// 4. Repeat at ~60fps

class MyView extends Multisynq.View {
    init() {
        this.model = this.wellKnownModel("MyModel");
        this.setupCanvas();
        
        // Optional: Define render method for automatic calling
    }
    
    render() {
        // This gets called automatically every frame
        this.clearCanvas();
        this.drawGame();
        this.drawUI();
    }
    
    clearCanvas() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }
    
    drawGame() {
        // Read model state and draw
        for (const entity of this.model.entities) {
            this.drawEntity(entity);
        }
    }
    
    drawUI() {
        // Draw user interface elements
        this.drawScore();
        this.drawHealth();
        this.drawMinimap();
    }
}
Custom control for advanced applications
// Manual stepping for custom main loops
const session = await Multisynq.Session.join({
    apiKey: "your_api_key",
    appId: "com.example.app", 
    name: "my-session",
    password: "my-password",
    model: MyModel,
    view: MyView,
    step: "manual"  // Disable automatic stepping
});

// Create your own main loop
window.requestAnimationFrame(mainLoop);

function mainLoop(timestamp) {
    if (session.view) {
        // Custom pre-processing
        handleCustomInput();
        updateAnimations(timestamp);
        
        // Process Multisynq events and models
        session.step(timestamp);
        
        // Custom post-processing  
        renderEffects();
        updateAudio();
        handleNetworking();
    }
    
    // Continue the loop
    window.requestAnimationFrame(mainLoop);
}

function handleCustomInput() {
    // Custom input processing before model updates
    if (inputBuffer.length > 0) {
        const inputs = inputBuffer.splice(0, inputBuffer.length);
        
        // Batch process inputs
        session.view.publish("input", "batch-actions", inputs);
    }
}

function updateAnimations(timestamp) {
    // Update view-only animations
    tweenManager.update(timestamp);
    particleSystem.update(timestamp);
}

function renderEffects() {
    // Custom rendering after model processing
    effectsRenderer.render();
    uiAnimator.render();
}

function updateAudio() {
    // Audio processing
    audioManager.update(session.model.gameState);
    soundEffects.process();
}

function handleNetworking() {
    // Custom networking (be careful not to break sync!)
    if (shouldSendAnalytics()) {
        sendAnalyticsData(session.model.getStats());
    }
}

// Advanced: Custom frame rate control
let lastUpdateTime = 0;
const targetFPS = 30; // Custom frame rate
const frameTime = 1000 / targetFPS;

function customFrameRateLoop(timestamp) {
    if (timestamp - lastUpdateTime >= frameTime) {
        // Run update logic
        session.step(timestamp);
        
        lastUpdateTime = timestamp;
    }
    
    requestAnimationFrame(customFrameRateLoop);
}
Specialized main loops for different game types
// Real-time action game
class ActionGameLoop {
    constructor(session) {
        this.session = session;
        this.fixedTimeStep = 1000 / 60; // 60fps simulation
        this.accumulator = 0;
        this.lastTime = 0;
    }
    
    start() {
        requestAnimationFrame((time) => this.loop(time));
    }
    
    loop(currentTime) {
        const deltaTime = currentTime - this.lastTime;
        this.lastTime = currentTime;
        
        this.accumulator += deltaTime;
        
        // Fixed timestep updates for consistent physics
        while (this.accumulator >= this.fixedTimeStep) {
            this.session.step(currentTime);
            this.accumulator -= this.fixedTimeStep;
        }
        
        // Interpolated rendering for smooth visuals
        const alpha = this.accumulator / this.fixedTimeStep;
        this.interpolateRender(alpha);
        
        requestAnimationFrame((time) => this.loop(time));
    }
    
    interpolateRender(alpha) {
        // Smooth rendering between fixed updates
        this.session.view.renderInterpolated(alpha);
    }
}

// Turn-based strategy game
class TurnBasedLoop {
    constructor(session) {
        this.session = session;
        this.isWaitingForInput = false;
        this.turnTimeLimit = 30000; // 30 seconds per turn
    }
    
    start() {
        this.processLoop();
    }
    
    async processLoop() {
        while (this.session.isActive) {
            if (this.session.model.gameState === "waiting-for-turn") {
                await this.handleTurnInput();
            } else {
                // Process any pending events
                this.session.step(performance.now());
            }
            
            // Update display
            this.session.view.render();
            
            // Small delay to prevent busy waiting
            await this.sleep(16); // ~60fps display updates
        }
    }
    
    async handleTurnInput() {
        this.isWaitingForInput = true;
        
        // Wait for player input or timeout
        const inputPromise = this.waitForPlayerInput();
        const timeoutPromise = this.sleep(this.turnTimeLimit);
        
        await Promise.race([inputPromise, timeoutPromise]);
        
        this.isWaitingForInput = false;
    }
    
    waitForPlayerInput() {
        return new Promise((resolve) => {
            const handler = (event) => {
                if (event.type === "turn-submitted") {
                    this.session.view.unsubscribe("input", "turn-submitted", handler);
                    resolve();
                }
            };
            
            this.session.view.subscribe("input", "turn-submitted", handler);
        });
    }
    
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

// Usage
const session = await Multisynq.Session.join({...options, step: "manual"});

// Choose appropriate loop for your game type
if (gameType === "action") {
    new ActionGameLoop(session).start();
} else if (gameType === "turn-based") {
    new TurnBasedLoop(session).start();
}

Best Practices

🏗️ Application Structure

Organize your code effectively
// ✅ Good structure
class GameModel extends Multisynq.Model {
    init() {
        this.initializeGameState();
        this.setupEventHandlers();
        this.startGameSystems();
    }
    
    initializeGameState() {
        // Clear separation of concerns
    }
}

// ✅ Modular views
class GameView extends Multisynq.View {
    init() {
        this.ui = new UIManager(this);
        this.renderer = new GameRenderer(this);
        this.input = new InputHandler(this);
    }
}

🔄 Session Management

Handle session lifecycle properly
// ✅ Robust session handling
try {
    const session = await Multisynq.Session.join(options);
    
    // Add error handling
    session.on('error', handleSessionError);
    session.on('disconnect', handleDisconnect);
    
    return session;
} catch (error) {
    console.error("Session failed:", error);
    showErrorDialog(error);
}

🎯 Performance

Optimize for smooth gameplay
// ✅ Efficient rendering
render() {
    if (this.needsRedraw) {
        this.drawGame();
        this.needsRedraw = false;
    }
}

// ✅ Batch operations
handleMultipleInputs(inputs) {
    // Process inputs in batches
    this.publish("input", "batch", inputs);
}

🛡️ Error Handling

Build resilient applications
// ✅ Graceful error handling
class RobustModel extends Multisynq.Model {
    init() {
        try {
            this.setupGame();
        } catch (error) {
            this.handleInitError(error);
        }
    }
    
    handleInitError(error) {
        console.error("Init failed:", error);
        this.gameState = "error";
        this.publish("error", "init-failed", error);
    }
}

Common Patterns

class StateManagedGame extends Multisynq.Model {
    init() {
        this.gameState = "menu";
        this.stateData = {};
        
        this.subscribe("game", "change-state", this.changeState);
    }
    
    changeState(newState, data = {}) {
        const oldState = this.gameState;
        
        // Exit current state
        this.exitState(oldState);
        
        // Enter new state
        this.gameState = newState;
        this.stateData = data;
        this.enterState(newState);
        
        this.publish("game", "state-changed", {
            from: oldState,
            to: newState,
            data: data
        });
    }
    
    enterState(state) {
        switch (state) {
            case "menu":
                this.setupMenu();
                break;
            case "playing":
                this.startGameplay();
                break;
            case "paused":
                this.pauseGameplay();
                break;
            case "game-over":
                this.endGameplay();
                break;
        }
    }
    
    exitState(state) {
        // Cleanup state-specific resources
        switch (state) {
            case "playing":
                this.pauseAllSounds();
                break;
        }
    }
}
class ConfigurableApp {
    static async launch(userConfig = {}) {
        // Merge user config with defaults
        const config = {
            apiKey: "",
            appId: "com.example.default",
            sessionName: null,
            password: null,
            maxPlayers: 8,
            gameMode: "casual",
            ...userConfig
        };
        
        // Validate required config
        this.validateConfig(config);
        
        // Auto-generate missing values
        if (!config.sessionName) {
            config.sessionName = await Multisynq.App.autoSession();
        }
        if (!config.password) {
            config.password = await Multisynq.App.autoPassword();
        }
        
        // Create model and view with config
        const ModelClass = this.getModelClass(config.gameMode);
        const ViewClass = this.getViewClass(config.gameMode);
        
        return Multisynq.Session.join({
            apiKey: config.apiKey,
            appId: config.appId,
            name: config.sessionName,
            password: config.password,
            model: ModelClass,
            view: ViewClass,
            options: {
                maxPlayers: config.maxPlayers
            }
        });
    }
    
    static validateConfig(config) {
        if (!config.apiKey) {
            throw new Error("API key is required");
        }
        if (!config.appId) {
            throw new Error("App ID is required");
        }
    }
    
    static getModelClass(gameMode) {
        const models = {
            casual: CasualGameModel,
            competitive: CompetitiveGameModel,
            sandbox: SandboxGameModel
        };
        return models[gameMode] || CasualGameModel;
    }
    
    static getViewClass(gameMode) {
        const views = {
            casual: CasualGameView,
            competitive: CompetitiveGameView,
            sandbox: SandboxGameView
        };
        return views[gameMode] || CasualGameView;
    }
}

// Usage
ConfigurableApp.launch({
    apiKey: "your_key",
    appId: "com.yourcompany.yourgame",
    gameMode: "competitive",
    maxPlayers: 16
});

Next Steps

Building a complete Multisynq application requires understanding both the technical structure (Model + View + Session) and the patterns for managing user interaction, state changes, and session lifecycle. Start with simple applications and gradually add complexity as you master these fundamentals.
I