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
๐ซ 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 ;
}
}
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 ();
}
}
}
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.