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.

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().

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.

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.

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

Advanced: Non-Model Objects

Sometimes you need utility classes that aren’t models. Use the types() system to handle their 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:

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.