Learn the essential constraints and best practices for building synchronized models that work perfectly across all users
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.
Critical: Models must follow these constraints to maintain synchronization. Violating these rules will break multi-user functionality.
Models must produce identical results
// ✅ Deterministic
this.position.x += this.velocity.x;
// ❌ Non-deterministic
this.position.x += Math.random();
All model state must be saveable
// ✅ Serializable
this.gameState = "playing";
this.players = new Map();
// ❌ Not serializable
this.callback = () => {};
this.element = document.div;
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.
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.
Register all model subclasses
class Game extends Multisynq.Model {
init() {
this.players = new Map();
this.world = World.create();
}
addPlayer(name) {
const player = Player.create({ name });
this.players.set(player.id, player);
return player;
}
}
class World extends Multisynq.Model {
init() {
this.width = 800;
this.height = 600;
this.obstacles = [];
}
}
class Player extends Multisynq.Model {
init(options) {
this.name = options.name;
this.position = { x: 0, y: 0 };
}
}
// Register ALL model classes
Game.register("Game");
World.register("World");
Player.register("Player");
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");
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");
Don’t use new
or constructors
class BadModel extends Multisynq.Model {
constructor(options) {
// ❌ NEVER implement constructor
super();
this.data = options.data;
}
createChild() {
// ❌ NEVER use new
const child = new ChildModel({ parent: this });
return child;
}
}
// This will break synchronization!
Using new
or implementing constructors will break snapshot restoration and synchronization.
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");
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");
Avoid these initialization errors
class BadModel extends Multisynq.Model {
constructor() {
// ❌ Never implement constructor
super();
this.setupData();
}
init(options) {
// ❌ Don't call setup methods that might fail
this.connectToExternalAPI(); // Could fail
// ❌ Don't use system time
this.createdAt = Date.now();
// ❌ Don't store function references
this.callback = options.onComplete;
// ❌ Don't access global variables
this.config = window.gameConfig;
// ❌ Don't use async operations
this.loadDataAsync();
}
async loadDataAsync() {
// ❌ Async not allowed in models
const data = await fetch('/api/data');
this.data = 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.
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.
Don’t use global variables
// ❌ Don't do this - not synchronized
const WORLD_WIDTH = 800;
let gameState = "playing";
var playerCount = 0;
class BadModel extends Multisynq.Model {
init() {
// ❌ These may not be synchronized
this.width = WORLD_WIDTH;
this.state = gameState;
this.playerCount = playerCount;
// ❌ Modifying globals breaks sync
playerCount++;
gameState = "active";
}
}
This breaks synchronization because global variables aren’t saved in snapshots and may have different values on different devices.
🚫 No External Dependencies
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;
}
}
🔄 No Async Operations
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);
}
}
📡 No View Communication
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();
}
}
}
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");
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");
Complex classes with custom write()
and read()
methods
// Complex utility class needing custom serialization
class GameBoard {
constructor(width, height) {
this.width = width;
this.height = height;
this.cells = new Array(width * height).fill(0);
this.specialData = new Map(); // Maps aren't JSON serializable
}
getCellIndex(x, y) {
return y * this.width + x;
}
setCell(x, y, value) {
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
this.cells[this.getCellIndex(x, y)] = value;
}
}
getCell(x, y) {
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
return this.cells[this.getCellIndex(x, y)];
}
return 0;
}
setSpecialData(key, value) {
this.specialData.set(key, value);
}
}
class BoardGameModel extends Multisynq.Model {
static types() {
return {
"GameBoard": {
cls: GameBoard,
write: (board) => ({
width: board.width,
height: board.height,
cells: board.cells,
specialData: Array.from(board.specialData.entries())
}),
read: (data) => {
const board = new GameBoard(data.width, data.height);
board.cells = data.cells;
board.specialData = new Map(data.specialData);
return board;
}
}
};
}
init() {
this.board = new GameBoard(10, 10);
this.currentPlayer = 1;
// Initialize board
this.setupInitialBoard();
}
setupInitialBoard() {
for (let x = 0; x < this.board.width; x++) {
for (let y = 0; y < this.board.height; y++) {
// Set up initial game state
if (y === 0) {
this.board.setCell(x, y, 1); // Player 1 pieces
} else if (y === this.board.height - 1) {
this.board.setCell(x, y, 2); // Player 2 pieces
}
}
}
// Set special data
this.board.setSpecialData("lastMove", null);
this.board.setSpecialData("turnCount", 0);
}
makeMove(fromX, fromY, toX, toY) {
const piece = this.board.getCell(fromX, fromY);
if (piece === this.currentPlayer) {
this.board.setCell(fromX, fromY, 0);
this.board.setCell(toX, toY, piece);
this.board.setSpecialData("lastMove", { fromX, fromY, toX, toY });
this.board.setSpecialData("turnCount",
this.board.specialData.get("turnCount") + 1);
this.currentPlayer = this.currentPlayer === 1 ? 2 : 1;
this.publish("game", "move-made", {
from: { x: fromX, y: fromY },
to: { x: toX, y: toY },
player: piece
});
}
}
}
BoardGameModel.register("BoardGameModel");
Organize your models properly
create()
and destroy()
init()
methodclass Player extends Multisynq.Model {
init(options) {
this.setupPlayer(options);
this.startBehaviors();
}
}
Player.register("Player");
Optimize for synchronization
// ✅ Efficient batching
batchUpdate() {
this.updateMultipleEntities();
this.future(Q.PHYSICS.STEP_MS).batchUpdate();
}
Maintain synchronization
// ✅ Safe and synchronized
this.value = this.calculateDeterministic();
Test thoroughly
// Test both fresh start and snapshot load
console.log("Model state:", this.getState());
Avoid these common model development errors:
❌ Using System APIs
// ❌ 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
❌ Storing Functions
// ❌ 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);
}
}
❌ Async Operations
// ❌ 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
}
}
Learn to build views that work with your models
Master communication between models and views
Understand timing and scheduling in models
Learn synchronized random number generation
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.