Skip to main content
Models are the heart of Multisynq applications - they contain all shared state and business logic. To maintain perfect synchronization across all users, models must follow specific constraints and patterns. This guide covers everything you need to know to write robust, synchronized models.

Core Constraints

Critical: Models must follow these constraints to maintain synchronization. Violating these rules will break multi-user functionality.

๐Ÿ”’ Deterministic

Models must produce identical results
  • Same inputs โ†’ Same outputs
  • No randomness (use Multisynq.Random)
  • No external dependencies
  • No system calls
// โœ… Deterministic
this.position.x += this.velocity.x;

// โŒ Non-deterministic
this.position.x += Math.random();

๐Ÿ”„ Serializable

All model state must be saveable
  • No functions in state
  • No DOM references
  • No external objects
  • Pure data structures
// โœ… Serializable
this.gameState = "playing";
this.players = new Map();

// โŒ Not serializable
this.callback = () => {};
this.element = document.div;

Model Registration

Every model class must be registered when defined for proper serialization.
  • Basic Registration
  • Nested Models
Register simple model classes
class Player extends Multisynq.Model {
    init(options) {
        this.name = options.name || "Anonymous";
        this.position = { x: 0, y: 0 };
        this.health = 100;
    }
    
    move(dx, dy) {
        this.position.x += dx;
        this.position.y += dy;
    }
}

// REQUIRED: Register the class
Player.register("Player");
The string name must match the class name exactly.

Model Creation and Destruction

Never use new to create model instances. Always use create() and destroy().
  • โœ… Correct Creation
  • โŒ Incorrect Creation
Use create() for model instantiation
class GameManager extends Multisynq.Model {
    init() {
        this.entities = new Map();
        this.nextId = 1;
    }
    
    spawnPlayer(name) {
        // โœ… Correct: Use create()
        const player = Player.create({
            id: this.nextId++,
            name: name,
            position: this.getSpawnPoint()
        });
        
        this.entities.set(player.id, player);
        this.publish("game", "player-spawned", player.id);
        
        return player;
    }
    
    removePlayer(playerId) {
        const player = this.entities.get(playerId);
        if (player) {
            // โœ… Correct: Use destroy()
            player.destroy();
            this.entities.delete(playerId);
            this.publish("game", "player-removed", playerId);
        }
    }
    
    getSpawnPoint() {
        return {
            x: Math.floor(this.world.width / 2),
            y: Math.floor(this.world.height / 2)
        };
    }
}

class Player extends Multisynq.Model {
    init(options) {
        // This is called automatically by create()
        this.id = options.id;
        this.name = options.name;
        this.position = options.position;
        this.health = 100;
        
        console.log(`Player ${this.name} created at`, this.position);
    }
    
    destroy() {
        // Cleanup logic before destruction
        this.publish("player", "dying", this.id);
        
        // Call parent destroy
        super.destroy();
    }
}

GameManager.register("GameManager");
Player.register("Player");

Initialization with init()

Always use init() for initialization, never constructors. The init() method is called only for new instances, not when restoring from snapshots.
  • Proper Initialization
  • Common Mistakes
Structure your init() method correctly
class GameObject extends Multisynq.Model {
    init(options = {}) {
        // Set default values first
        this.position = { x: 0, y: 0 };
        this.velocity = { x: 0, y: 0 };
        this.health = 100;
        this.maxHealth = 100;
        this.alive = true;
        
        // Apply options
        if (options.position) {
            this.position = { ...options.position };
        }
        if (options.health !== undefined) {
            this.health = options.health;
            this.maxHealth = options.health;
        }
        
        // Setup behaviors
        this.setupPhysics();
        this.startAI();
        
        // Subscribe to events
        this.subscribe(this.id, "damage", this.takeDamage);
        this.subscribe(this.id, "heal", this.heal);
    }
    
    setupPhysics() {
        // Start physics update loop
        this.future(1000/60).updatePhysics();
    }
    
    startAI() {
        // Start AI decision loop
        this.future(500).makeDecision();
    }
    
    takeDamage(damage) {
        this.health = Math.max(0, this.health - damage);
        if (this.health <= 0 && this.alive) {
            this.die();
        }
    }
    
    heal(amount) {
        this.health = Math.min(this.maxHealth, this.health + amount);
    }
    
    die() {
        this.alive = false;
        this.publish("game", "entity-died", this.id);
        
        // Remove after death animation
        this.future(2000).destroy();
    }
    
    updatePhysics() {
        if (!this.alive) return;
        
        this.position.x += this.velocity.x / 60;
        this.position.y += this.velocity.y / 60;
        
        // Continue physics loop
        this.future(1000/60).updatePhysics();
    }
    
    makeDecision() {
        if (!this.alive) return;
        
        // AI decision logic here
        this.chooseAction();
        
        // Continue AI loop
        this.future(500).makeDecision();
    }
}

GameObject.register("GameObject");

Constants and Global Data

No global variables in models. Use Multisynq.Constants for shared constants.
  • โœ… Using Constants
  • โŒ Global Variables
Properly define and use constants
// Define constants before session starts
const Q = Multisynq.Constants;

Q.GAME = {
    WORLD_WIDTH: 800,
    WORLD_HEIGHT: 600,
    GRAVITY: 0.5,
    JUMP_FORCE: -12,
    PLAYER_SPEED: 200
};

Q.PHYSICS = {
    STEP_MS: 1000 / 60,  // 60fps physics
    MAX_VELOCITY: 500,
    FRICTION: 0.8
};

Q.GAMEPLAY = {
    PLAYER_HEALTH: 100,
    DAMAGE_COOLDOWN: 1000,
    RESPAWN_TIME: 3000
};

class Game extends Multisynq.Model {
    init() {
        this.world = {
            width: Q.GAME.WORLD_WIDTH,
            height: Q.GAME.WORLD_HEIGHT
        };
        
        this.physics = {
            gravity: Q.GAME.GRAVITY,
            friction: Q.PHYSICS.FRICTION
        };
        
        // Start physics loop
        this.future(Q.PHYSICS.STEP_MS).physicsStep();
    }
    
    physicsStep() {
        // Use constants in calculations
        for (const player of this.players.values()) {
            player.velocity.y += Q.GAME.GRAVITY;
            player.velocity.x *= Q.PHYSICS.FRICTION;
            
            // Clamp velocity
            const maxVel = Q.PHYSICS.MAX_VELOCITY;
            player.velocity.x = Math.max(-maxVel, Math.min(maxVel, player.velocity.x));
            player.velocity.y = Math.max(-maxVel, Math.min(maxVel, player.velocity.y));
        }
        
        this.future(Q.PHYSICS.STEP_MS).physicsStep();
    }
    
    createPlayer(name) {
        return Player.create({
            name: name,
            health: Q.GAMEPLAY.PLAYER_HEALTH,
            speed: Q.GAME.PLAYER_SPEED
        });
    }
}

Game.register("Game");
Constants are recursively frozen once the session starts, preventing accidental modification.

Synchronization Rules

Models must be isolated from external systems
class SynchronizedModel extends Multisynq.Model {
    init() {
        // โœ… Use simulation time
        this.startTime = this.now();
        
        // โœ… Use Multisynq random
        this.randomSeed = this.random(1000);
        
        // โœ… Use constants
        this.maxPlayers = Q.GAME.MAX_PLAYERS;
    }
    
    gameLoop() {
        // โœ… Deterministic calculations
        const elapsed = this.now() - this.startTime;
        this.updateGameState(elapsed);
        
        this.future(Q.PHYSICS.STEP_MS).gameLoop();
    }
}

class UnsynchronizedModel extends Multisynq.Model {
    init() {
        // โŒ Never use system time
        this.startTime = Date.now();
        
        // โŒ Never use Math.random
        this.randomValue = Math.random();
        
        // โŒ Never access browser APIs
        this.windowWidth = window.innerWidth;
        
        // โŒ Never make network requests
        this.fetchUserData();
    }
    
    async fetchUserData() {
        // โŒ No async/await in models
        const data = await fetch('/api/user');
        this.userData = data;
    }
}
Models must be synchronous and deterministic
class SyncModel extends Multisynq.Model {
    init() {
        this.data = new Map();
        this.processQueue = [];
        
        // โœ… Use scheduled processing instead of async
        this.processData();
    }
    
    addWork(work) {
        this.processQueue.push(work);
    }
    
    processData() {
        // โœ… Process work synchronously in chunks
        const startTime = this.now();
        while (this.processQueue.length > 0 && this.now() - startTime < 5) {
            const work = this.processQueue.shift();
            this.processWorkItem(work);
        }
        
        // Continue processing next frame
        this.future(16).processData();
    }
    
    processWorkItem(work) {
        // Synchronous processing only
        const result = this.calculateResult(work);
        this.data.set(work.id, result);
    }
}

class AsyncModel extends Multisynq.Model {
    async init() {
        // โŒ Never use async init
        this.data = await this.loadData();
    }
    
    async processRequest(request) {
        // โŒ Never use async methods
        const response = await fetch('/api/process', {
            method: 'POST',
            body: JSON.stringify(request)
        });
        return response.json();
    }
    
    handleTimer() {
        // โŒ Never use setTimeout/setInterval
        setTimeout(() => {
            this.doSomething();
        }, 1000);
    }
}
Donโ€™t create Model โ†’ View โ†’ Model event chains
class GoodModel extends Multisynq.Model {
    init() {
        this.gameState = "waiting";
        this.players = new Map();
        
        // โœ… Listen to view events only
        this.subscribe("input", "player-action", this.handlePlayerAction);
    }
    
    handlePlayerAction(data) {
        // โœ… Process input and update state
        const player = this.players.get(data.playerId);
        if (player) {
            player.processAction(data.action);
        }
        
        // โœ… Notify views of state change (local only)
        this.publish("game", "state-updated", {
            gameState: this.gameState,
            playerCount: this.players.size
        });
    }
    
    startGame() {
        this.gameState = "playing";
        
        // โœ… Notify views locally
        this.publish("ui", "game-started", {});
    }
}

class BadModel extends Multisynq.Model {
    init() {
        this.waitingForViewResponse = false;
        
        // โŒ Don't expect responses from views
        this.subscribe("view-response", "confirmation", this.handleViewResponse);
    }
    
    doSomething() {
        // โŒ Don't query views for information
        this.publish("view-query", "get-user-preference", {});
        this.waitingForViewResponse = true;
        
        // โŒ Don't wait for view responses
        this.future(100).checkViewResponse();
    }
    
    checkViewResponse() {
        if (this.waitingForViewResponse) {
            // โŒ This creates unreliable behavior
            this.future(100).checkViewResponse();
        }
    }
}

Advanced: Non-Model Objects

Sometimes you need utility classes that arenโ€™t models. Use the types() system to handle their serialization.
  • Basic Non-Model Class
  • Custom Serialization
Simple utility classes with default serialization
// Utility class that isn't a Model
class Vector2D {
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }
    
    add(other) {
        return new Vector2D(this.x + other.x, this.y + other.y);
    }
    
    multiply(scalar) {
        return new Vector2D(this.x * scalar, this.y * scalar);
    }
    
    magnitude() {
        return Math.sqrt(this.x * this.x + this.y * this.y);
    }
    
    normalize() {
        const mag = this.magnitude();
        return mag > 0 ? this.multiply(1 / mag) : new Vector2D(0, 0);
    }
}

class PhysicsModel extends Multisynq.Model {
    static types() {
        return {
            "Vector2D": Vector2D  // Use default serialization
        };
    }
    
    init() {
        this.position = new Vector2D(100, 100);
        this.velocity = new Vector2D(0, 0);
        this.acceleration = new Vector2D(0, 0.5); // gravity
    }
    
    updatePhysics() {
        // Use Vector2D methods
        this.velocity = this.velocity.add(this.acceleration);
        this.position = this.position.add(this.velocity);
        
        // Bounce off ground
        if (this.position.y > 500) {
            this.position.y = 500;
            this.velocity = new Vector2D(this.velocity.x, -this.velocity.y * 0.8);
        }
        
        this.future(1000/60).updatePhysics();
    }
}

PhysicsModel.register("PhysicsModel");

Best Practices Summary

๐Ÿ—๏ธ Structure

Organize your models properly
  • Register all model classes
  • Use create() and destroy()
  • Initialize in init() method
  • Keep models focused and single-purpose
class Player extends Multisynq.Model {
    init(options) {
        this.setupPlayer(options);
        this.startBehaviors();
    }
}
Player.register("Player");

โšก Performance

Optimize for synchronization
  • Use constants for shared values
  • Batch operations when possible
  • Avoid unnecessary calculations
  • Clean up unused objects
// โœ… Efficient batching
batchUpdate() {
    this.updateMultipleEntities();
    this.future(Q.PHYSICS.STEP_MS).batchUpdate();
}

๐Ÿ”’ Safety

Maintain synchronization
  • No external dependencies
  • No async operations
  • No global variables
  • Deterministic behavior only
// โœ… Safe and synchronized
this.value = this.calculateDeterministic();

๐Ÿงช Testing

Test thoroughly
  • Test with multiple users
  • Verify snapshot restoration
  • Check deterministic behavior
  • Test edge cases
// Test both fresh start and snapshot load
console.log("Model state:", this.getState());

Common Mistakes

Avoid these common model development errors:
// โŒ NEVER do these in models:
const now = Date.now();              // Use this.now()
const random = Math.random();        // Use this.random()
const element = document.getElementById('canvas'); // No DOM access
const data = localStorage.getItem('data');        // No storage access
const response = fetch('/api/data'); // No network calls
// โŒ Don't store functions in model state
class BadModel extends Multisynq.Model {
    init() {
        this.callback = () => console.log("Hi"); // Won't serialize
        this.handlers = new Map([
            ['click', this.handleClick]  // Won't work
        ]);
    }
}

// โœ… Use method names instead
class GoodModel extends Multisynq.Model {
    init() {
        this.eventHandlers = ['handleClick', 'handleMove'];
        this.subscribe("input", "click", this.handleClick);
    }
}
// โŒ No promises or async/await
class AsyncModel extends Multisynq.Model {
    async init() {
        this.data = await this.loadData(); // Breaks sync
    }
    
    handleClick() {
        setTimeout(() => {
            this.doSomething(); // Breaks sync
        }, 1000);
    }
}

// โœ… Use future() for timing
class SyncModel extends Multisynq.Model {
    init() {
        this.loadDataSync();
    }
    
    handleClick() {
        this.future(1000).doSomething(); // Synchronized
    }
}

Next Steps

Writing good Multisynq models is fundamental to building successful multiplayer applications. Follow these constraints carefully, and your models will synchronize perfectly across all users, providing a seamless collaborative experience.
โŒ˜I