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:

  1. User Management: Maps active views to their nicknames
  2. Message History: Maintains chat conversation history
  3. Event Handling: Processes chat posts and reset commands
  4. 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:

  1. Check if the viewId already exists (for reconnections)
  2. Generate a random nickname if it’s a new user
  3. Increment the participant count
  4. 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:

  1. Extract the sender’s viewId from the event data
  2. Look up the user’s nickname
  3. Build HTML chat line with nickname and escaped message
  4. Add to history with size limit (100 messages)
  5. 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:

  1. Inactivity timeout: No posts for 20 minutes
  2. User command: Someone types /reset
  3. 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

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

User Information Display

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

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.