| title | Snapshots |
|---|---|
| description | Understand how Multisynq automatically saves and restores session state for persistence and new user synchronization |
Snapshots are automatic copies of the model state that Multisynq saves to the cloud. This system provides seamless persistence and enables new users to join existing sessions efficiently without replaying the entire event history.
**Snapshots are serialized copies of your complete model state**, saved periodically to the cloud. They capture: - All model properties and data - Current object relationships - Session state at a specific moment - Everything needed to restore the exact state Snapshots are taken automatically by the Multisynq system - you don't need to manually create them. The Multisynq reflector periodically requests one of the session participants to create a snapshot of the current model state.// This happens automatically - no code needed
// The system serializes your entire model state// Snapshots are stored with metadata:
// - Session ID
// - Timestamp
// - Model state hash
// - Event sequence number// Your model's init() method won't be called
// The state is restored directly from the snapshotWhen users quit or reload:
- Session state is preserved in the latest snapshot
- When they return, the exact state is restored
- No progress is lost
// Example: Game state preservation
class GameModel extends Multisynq.Model {
init() {
this.level = 1;
this.score = 0;
this.playerPositions = new Map();
}
// All of this state is automatically saved in snapshots
// and restored when the session resumes
}When a new user joins an active session:
- Load snapshot: Get the most recent state
- Replay events: Apply events since the snapshot
- Sync complete: Now in sync with other users
graph TD
A[New User Joins] --> B[Load Latest Snapshot]
B --> C[Replay Events Since Snapshot]
C --> D[Initialize View]
D --> E[User Synchronized]
// Instead of calling init(), your model is restored from snapshot
// This bypasses normal initialization
class GameModel extends Multisynq.Model {
init() {
// This WON'T be called for users joining from snapshot
console.log("Fresh session start");
}
}// All events since the snapshot are replayed
// This brings the snapshot up to the current state
// Events are processed in the exact same order// Your model processes all missed events
// This happens automatically and synchronously
// The model becomes current with other usersclass GameView extends Multisynq.View {
init() {
// This IS called for snapshot joins
// Your view should be prepared to handle existing state
this.canvas = document.getElementById('canvas');
// The model may already have data from the snapshot
this.displayExistingPlayers();
}
displayExistingPlayers() {
// Handle the case where players already exist
for (const player of this.model.players.values()) {
this.createPlayerVisual(player);
}
}
} // This assumes the model starts empty
// But it might already have players from a snapshot!
this.subscribe("player", "joined", this.addPlayer);
}
addPlayer(player) {
this.players.set(player.id, new PlayerVisual(player));
}
// Missing: handling existing players from snapshot
}
</Tab>
<Tab title="✅ Correct Approach">
```js
class GameView extends Multisynq.View {
init() {
this.players = new Map();
// Handle existing players from snapshot
this.initializeExistingPlayers();
// Subscribe to future player events
this.subscribe("player", "joined", this.addPlayer);
this.subscribe("player", "left", this.removePlayer);
}
initializeExistingPlayers() {
// Check if model already has players (from snapshot)
for (const player of this.model.players.values()) {
this.createPlayerVisual(player);
}
}
createPlayerVisual(player) {
const visual = new PlayerVisual(player);
this.players.set(player.id, visual);
}
addPlayer(player) {
this.createPlayerVisual(player);
}
removePlayer(playerId) {
const visual = this.players.get(playerId);
if (visual) {
visual.destroy();
this.players.delete(playerId);
}
}
}
// During snapshot creation:
// 1. Model state is serialized
// 2. Data is compressed
// 3. Snapshot is transmitted
// 4. Normal execution resumes- Keep models lean: Avoid storing unnecessary data
- Use efficient data structures: Prefer simple objects over complex ones
- Clean up regularly: Remove unused objects
- Monitor model size: Be aware of your state footprint
class OptimizedModel extends Multisynq.Model {
init() {
// Use efficient data structures
this.players = new Map(); // Better than array for lookups
this.cleanup();
}
cleanup() {
// Regularly clean up unused data
this.future(10000).cleanup();
// Remove expired objects
for (const [id, player] of this.players) {
if (player.isExpired()) {
this.players.delete(id);
}
}
}
}- Use simple, serializable data types
- Avoid circular references
- Keep the state tree flat when possible
- Clean up unused objects regularly
// Good for snapshots
this.score = 100;
this.players = new Map();
this.gameState = "active";
// Avoid complex nested structures
this.deepNestedData = {...};- Check for existing model data in
init() - Create visuals for pre-existing objects
- Subscribe to future events
- Handle both fresh starts and snapshot loads
init() {
// Handle snapshot case
this.initializeExistingState();
// Handle future events
this.subscribeToEvents();
}- Test fresh session starts
- Test joining existing sessions
- Verify state restoration
- Check view initialization
// Test both scenarios:
// 1. Fresh session (init() called)
// 2. Snapshot join (init() not called)- Monitor your model size
- Use efficient data structures
- Clean up regularly
- Avoid storing view-specific data in models
// Good: Store only essential state
this.gameLogic = essentialData;
// Bad: Store view-specific data
this.uiElements = domElements;class GameModel extends Multisynq.Model {
init() {
// This only runs for fresh sessions
this.fromSnapshot = false;
console.log("Fresh session start");
}
// Called after snapshot restore OR fresh init
start() {
if (!this.fromSnapshot) {
this.fromSnapshot = true;
console.log("Loaded from snapshot");
}
}
}class GameModel extends Multisynq.Model {
validateState() {
// Check state integrity after snapshot load
console.log("Players:", this.players.size);
console.log("Game state:", this.gameState);
console.log("Score:", this.score);
// Verify relationships
for (const player of this.players.values()) {
if (!player.isValid()) {
console.error("Invalid player state:", player);
}
}
}
}class GameModel extends Multisynq.Model {
init() {
// Fresh game start
this.level = 1;
this.score = 0;
this.players = new Map();
this.gameState = "waiting";
this.startTime = Date.now();
}
// All this state is automatically preserved
// When users rejoin, they continue exactly where they left off
}
class GameView extends Multisynq.View {
init() {
// Handle both fresh starts and snapshot resumes
this.initializeUI();
this.displayCurrentState();
}
displayCurrentState() {
// Show current level, score, etc.
this.updateScore(this.model.score);
this.updateLevel(this.model.level);
// Display existing players
for (const player of this.model.players.values()) {
this.createPlayerDisplay(player);
}
}
}class ChatModel extends Multisynq.Model {
init() {
this.messages = [];
this.users = new Map();
}
addMessage(message) {
this.messages.push(message);
// Keep only last 100 messages for snapshot efficiency
if (this.messages.length > 100) {
this.messages.shift();
}
}
}
class ChatView extends Multisynq.View {
init() {
this.chatContainer = document.getElementById('chat');
// Display existing messages from snapshot
this.displayMessageHistory();
// Subscribe to new messages
this.subscribe("chat", "message", this.displayMessage);
}
displayMessageHistory() {
for (const message of this.model.messages) {
this.displayMessage(message);
}
}
}- Invisible performance: No more hitches during snapshot creation
- Optimized storage: Smaller snapshots, faster loading
- Better compression: Reduced network overhead
- Incremental snapshots: Only save changes since last snapshot