This tutorial demonstrates how to keep track of different users within the same session through a simple chat application. The app maintains a list of currently connected users, assigns random nicknames to new users, and includes automatic cleanup features.
Try it out!
Click or scan the QR code above to launch a new CodePen instance. Typing a message in either window will post the text to the shared chat screen under a randomly assigned nickname. Other people reading this documentation can also join the conversation!
What You’ll Learn
User Lifecycle Events Use "view-join"
and "view-exit"
events to track connections
View-Specific Data Store user information using viewId
as unique identifier
Direct Model Reading Safely read from model without breaking synchronization
Timeout Management Schedule actions with future()
and model.now()
Architecture Overview
The application uses a single Model subclass called ChatModel
that handles four main responsibilities:
User Management : Maps active views to their nicknames
Message History : Maintains chat conversation history
Event Handling : Processes chat posts and reset commands
Cleanup : Automatically clears inactive chats
Chat Model Implementation
Initialization
class ChatModel extends Multisynq . Model {
init () {
this . views = new Map ();
this . participants = 0 ;
this . history = [];
this . inactivity_timeout_ms = 20 * 60 * 1000 ; // 20 minutes
this . lastPostTime = null ;
// System event subscriptions
this . subscribe ( this . sessionId , "view-join" , this . viewJoin );
this . subscribe ( this . sessionId , "view-exit" , this . viewExit );
// User input event subscriptions
this . subscribe ( "input" , "newPost" , this . newPost );
this . subscribe ( "input" , "reset" , this . resetHistory );
}
}
We use Map
instead of plain objects for key-value collections to ensure identical behavior across users. Maps maintain insertion order when serialized/deserialized.
User Lifecycle Management
Handling User Joins
viewJoin ( viewId ) {
const existing = this . views . get ( viewId );
if ( ! existing ) {
const nickname = this . randomName ();
this . views . set ( viewId , nickname );
}
this . participants ++ ;
this . publish ( "viewInfo" , "refresh" );
}
When a user joins:
Check if the viewId
already exists (for reconnections)
Generate a random nickname if it’s a new user
Increment the participant count
Notify views to refresh their user information
Handling User Exits
viewExit ( viewId ) {
this . participants -- ;
this . views . delete ( viewId );
this . publish ( "viewInfo" , "refresh" );
}
The viewId
remains the same if a user reconnects from the same device. However, each browser tab gets a different viewId
, even on the same device.
Message Management
Processing New Messages
newPost ( post ) {
const postingView = post . viewId ;
const nickname = this . views . get ( postingView );
const chatLine = `<b> ${ nickname } :</b> ${ this . escape ( post . text ) } ` ;
this . addToHistory ({ viewId: postingView , html: chatLine });
this . lastPostTime = this . now ();
this . future ( this . inactivity_timeout_ms ). resetIfInactive ();
}
addToHistory ( item ) {
this . history . push ( item );
if ( this . history . length > 100 ) this . history . shift ();
this . publish ( "history" , "refresh" );
}
The message processing flow:
Extract the sender’s viewId
from the event data
Look up the user’s nickname
Build HTML chat line with nickname and escaped message
Add to history with size limit (100 messages)
Schedule inactivity timeout check
Inactivity Management
resetIfInactive () {
if ( this . lastPostTime !== this . now () - this . inactivity_timeout_ms ) return ;
this . resetHistory ( "due to inactivity" );
}
This method verifies that exactly inactivity_timeout_ms
milliseconds have passed since the last post. If another post arrived during the timeout period, lastPostTime
will be different, and the reset is skipped.
The inactivity check uses simulation time (this.now()
) rather than wall-clock time. This ensures consistent behavior across all users regardless of their local time settings.
Chat Reset Functionality
resetHistory ( reason ) {
this . history = [{ html: `<i>chat reset ${ reason } </i>` }];
this . lastPostTime = null ;
this . publish ( "history" , "refresh" );
}
Chat can be reset in three scenarios:
Inactivity timeout : No posts for 20 minutes
User command : Someone types /reset
New user alone : Solo user with no previous messages
Deterministic Random Names
randomName () {
const names = [ "Acorn" , "Banana" , "Cherry" , /* ... */ , "Zucchini" ];
return names [ Math . floor ( Math . random () * names . length )];
}
Even though separate instances run locally for each user, they all pick the same “random” name because Math.random()
calls from within the model are deterministic and synchronized.
Chat View Implementation
Constructor Setup
class ChatView extends Multisynq . View {
constructor ( model ) {
super ( model );
this . model = model ;
// Set up UI event handlers
sendButton . onclick = () => this . send ();
// Subscribe to model updates
this . subscribe ( "history" , "refresh" , this . refreshHistory );
this . subscribe ( "viewInfo" , "refresh" , this . refreshViewInfo );
// Initialize display
this . refreshHistory ();
this . refreshViewInfo ();
// Reset chat if alone with no contributions
if ( model . participants === 1 &&
! model . history . find ( item => item . viewId === this . viewId )) {
this . publish ( "input" , "reset" , "for new participants" );
}
}
}
Key Constructor Principles
Keep a reference to the model for direct reading access
Initialize from Model State
Set up the view completely from current model state
Rely on subscriptions for ongoing synchronization
Views can read from models but must never write directly
Message Sending
send () {
const text = textIn . value ;
textIn . value = "" ;
if ( text === "/reset" ) {
this . publish ( "input" , "reset" , "at user request" );
} else {
this . publish ( "input" , "newPost" , { viewId: this . viewId , text });
}
}
The send method handles both regular messages and the special /reset
command. Note that this.viewId
is automatically available in all View classes.
Display Updates
refreshViewInfo () {
nickname . innerHTML = "<b>Nickname:</b> " + this . model . views . get ( this . viewId );
viewCount . innerHTML = "<b>Total Views:</b> " + this . model . participants ;
}
Chat History Display
refreshHistory () {
textOut . innerHTML = "<b>Welcome to Multisynq Chat!</b><br><br>" +
this . model . history . map ( item => item . html ). join ( "<br>" );
textOut . scrollTop = Math . max ( 10000 , textOut . scrollHeight );
}
Both methods directly read from the model to update the display. The history display automatically scrolls to show the latest messages.
Model Safety with modelOnly()
To prevent accidental model modification, you can use explicit getter/setter methods:
class MyModel extends Multisynq . Model {
init () {
this . data = null ;
}
getData () {
return this . data ;
}
setData ( newData ) {
this . modelOnly (); // Throws error if called from view
this . data = newData ;
}
}
Model.modelOnly()
throws an error if called outside normal model execution. Use this in setter methods to prevent accidental view access.
Key Architecture Patterns
Event Scoping Use different scopes ("input"
, "history"
, "viewInfo"
) to organize events
ViewId as Key Use viewId
as unique identifier for user-specific data
Future Scheduling Schedule cleanup operations with future()
method
Safe Model Access Read from model freely, but write only through events
System Events Reference
Sent when a user joins the session. Contains the user’s viewId
. Sent to all users including the one who joined.
Sent when a user leaves the session. Contains the user’s viewId
. Not sent to the user who left.
Both events use this.sessionId
as the scope, which is the default for system-generated events.
Best Practices Demonstrated
Deterministic Behavior Use model’s random() for consistent results across users
Resource Management Limit history size to prevent memory growth
Graceful Cleanup Auto-reset inactive chats for fresh user experience
Reconnection Handling Preserve user nicknames across reconnections
Next Steps
This tutorial demonstrates essential patterns for user management, message handling, and proper Model-View interaction in Multisynq applications. These concepts form the foundation for building more complex collaborative applications.
Responses are generated using AI and may contain mistakes.