Learn how to save application data across code changes and versions using Multisynq’s persistence mechanism
Multisynq automatically handles session state through snapshots, but what happens when you update your code? This tutorial covers explicit persistence - how to save and restore application data across different versions of your application.
Understanding how Multisynq identifies sessions is crucial for persistence:
🆔 sessionId
Code-dependent session identifier
Combination of appId, name, and code hash
Changes with any code modification
Used for snapshots and live sessions
Copy
// These create the same sessionId:Session.join({ appId: "myapp", name: "room1" });// With identical code// This creates a different sessionId:Session.join({ appId: "myapp", name: "room1" });// With modified code (even tiny changes)
🔄 persistentId
Code-independent persistent identifier
Combination of appId and name only
Survives code changes
Used for persistence lookup
Copy
// These share the same persistentId:Session.join({ appId: "myapp", name: "room1" });// Regardless of code changes// Different persistentId:Session.join({ appId: "myapp", name: "room2" });
class MyModel extends Multisynq.Model { init() { this.userMap = new Map(); this.userSet = new Set(); this.createdAt = new Date(); } save() { this.persistSession(() => ({ // Convert Map to Array users: Array.from(this.userMap.entries()), // Convert Set to Array tags: Array.from(this.userSet), // Convert Date to timestamp createdAt: this.createdAt.getTime(), // Don't include functions or DOM elements // functions: this.myFunction, // ❌ Won't work // element: this.domElement, // ❌ Won't work })); } restoreFromPersisted(data) { // Restore Map from Array this.userMap = new Map(data.users || []); // Restore Set from Array this.userSet = new Set(data.tags || []); // Restore Date from timestamp this.createdAt = new Date(data.createdAt || Date.now()); }}
Unlike automatic snapshots, you control when to save persistent data. Balance data safety with performance.
Save after significant data modifications:
Copy
class DocumentEditor extends Multisynq.Model { addDocument(doc) { this.documents.set(doc.id, doc); this.save(); // Major change - save immediately } deleteDocument(id) { this.documents.delete(id); this.save(); // Major change - save immediately } updateCursor(position) { this.cursors.set(this.viewId, position); // Minor change - don't save for every cursor move }}
Save after significant data modifications:
Copy
class DocumentEditor extends Multisynq.Model { addDocument(doc) { this.documents.set(doc.id, doc); this.save(); // Major change - save immediately } deleteDocument(id) { this.documents.delete(id); this.save(); // Major change - save immediately } updateCursor(position) { this.cursors.set(this.viewId, position); // Minor change - don't save for every cursor move }}
Save after bursts of activity:
Copy
class TextEditor extends Multisynq.Model { init() { this.content = ""; this.saveTimer = null; this.hasChanges = false; } updateText(newText) { this.content = newText; this.hasChanges = true; // Reset save timer if (this.saveTimer) { this.saveTimer.cancel(); } // Save 30 seconds after last change this.saveTimer = this.future(30000).saveIfChanged(); } saveIfChanged() { if (this.hasChanges) { this.save(); this.hasChanges = false; } this.saveTimer = null; } save() { this.persistSession(() => ({ content: this.content, lastSaved: Date.now() })); }}
Critical: Implement error handling to prevent data corruption during development.
Protect against corrupted persistent data:
Copy
class SafeModel extends Multisynq.Model { init(options, persisted) { if (persisted) { delete this.loadingPersistentDataErrored; this.loadingPersistentData = true; try { this.fromSavedData(persisted); console.log("Successfully loaded persistent data"); } catch (error) { console.error("Error loading persistent data:", error); this.loadingPersistentDataErrored = true; // Fallback to defaults this.initializeDefaults(); } finally { delete this.loadingPersistentData; } } else { this.initializeDefaults(); } } save() { // Don't save while loading (prevents corruption) if (this.loadingPersistentData) return; // Don't save if loading failed (prevents overwriting good data) if (this.loadingPersistentDataErrored) return; this.persistSession(() => this.toSaveData()); } fromSavedData(persisted) { // Validate data structure if (!persisted.version) { throw new Error("Missing version in persistent data"); } if (persisted.version > 2) { throw new Error(`Unsupported version: ${persisted.version}`); } // Restore data this.documents = new Map(persisted.documents || []); this.metadata = persisted.metadata || {}; } toSaveData() { return { version: 2, documents: Array.from(this.documents.entries()), metadata: this.metadata }; }}
Protect against corrupted persistent data:
Copy
class SafeModel extends Multisynq.Model { init(options, persisted) { if (persisted) { delete this.loadingPersistentDataErrored; this.loadingPersistentData = true; try { this.fromSavedData(persisted); console.log("Successfully loaded persistent data"); } catch (error) { console.error("Error loading persistent data:", error); this.loadingPersistentDataErrored = true; // Fallback to defaults this.initializeDefaults(); } finally { delete this.loadingPersistentData; } } else { this.initializeDefaults(); } } save() { // Don't save while loading (prevents corruption) if (this.loadingPersistentData) return; // Don't save if loading failed (prevents overwriting good data) if (this.loadingPersistentDataErrored) return; this.persistSession(() => this.toSaveData()); } fromSavedData(persisted) { // Validate data structure if (!persisted.version) { throw new Error("Missing version in persistent data"); } if (persisted.version > 2) { throw new Error(`Unsupported version: ${persisted.version}`); } // Restore data this.documents = new Map(persisted.documents || []); this.metadata = persisted.metadata || {}; } toSaveData() { return { version: 2, documents: Array.from(this.documents.entries()), metadata: this.metadata }; }}
Safe development and testing process:
Test saving logic:
Copy
// Set breakpoint in toSaveData()toSaveData() { const data = { version: 1, content: this.content }; console.log("Saving data:", data); // Check structure return data;}
Test loading logic:
Copy
// Modify code slightly to create new sessionId// Add console.log("Testing v1.1"); to trigger new sessionfromSavedData(persisted) { console.log("Loading data:", persisted); // Verify structure // Set breakpoint here to inspect data}
Use separate deployments:
Production version: Create persistent data
Development version: Test loading persistent data
Prevents corrupting production data during development
// In Session.join()Session.join({ appId: "myapp", name: "session1", model: MyModel, view: MyView, debug: "session" // Enables persistence logging});// Or via URL parameter// https://myapp.com/#session1&debug=session
Console output:
Copy
[Multisynq] Session loaded from persistent data (persistentId: myapp-session1)[Multisynq] Restoring 1.2KB of persistent data[Multisynq] Persistent data saved (2.3KB)
Enable detailed logging:
Copy
// In Session.join()Session.join({ appId: "myapp", name: "session1", model: MyModel, view: MyView, debug: "session" // Enables persistence logging});// Or via URL parameter// https://myapp.com/#session1&debug=session
Console output:
Copy
[Multisynq] Session loaded from persistent data (persistentId: myapp-session1)[Multisynq] Restoring 1.2KB of persistent data[Multisynq] Persistent data saved (2.3KB)
Persistence is essential for applications where user data has long-term value. Plan for it early, implement it safely, and test thoroughly to ensure your users never lose their important work.
Assistant
Responses are generated using AI and may contain mistakes.