"Design a chat application like WhatsApp. Users can message each other one-on-one or in groups, see delivery and read receipts, and reconnect after being offline without losing or duplicating messages."
B. Difficulty
Advanced. This problem is the natural capstone of the Observer arc this document has built since Ride-Sharing's first small hook (Problem 5): a chat conversation is, structurally, a Pub-Sub topic with its participants as subscribers. The genuine new difficulty is per-conversation message ordering, exactly-once delivery across reconnects, and correctly separating the Mediator concern (decoupling participants from each other) from the Observer concern (notifying connected clients).
C. What This Problem Tests
Recognizing a conversation as a Pub-Sub topic — the same subject/observer relationship this document has used as a small hook in Ride-Sharing, Notification System, Library, Hotel, Movie Ticket Booking, and Food Delivery, and generalized fully in Pub-Sub (Problem 10) — now applied at the scale it was always heading toward
Mediator for group chat: participants hold a reference to the Conversation, never to each other directly, avoiding an O(n²) mesh of peer references as group size grows
State machine for per-message delivery status (sent → delivered → read)
Per-conversation message ordering assigned centrally, not by client clocks
Exactly-once delivery on reconnect, and a presence-status race across a user's multiple devices
D. Requirements
Functional
Send a 1:1 or group message; all participants receive it in a consistent order
Track delivery and read status per message (and, for groups, per recipient)
Show a user's online/offline presence, correctly across multiple simultaneously-connected devices
On reconnect after being offline, deliver missed messages exactly once, in order
Non-Functional
Two participants must never see the same conversation's messages in a different relative order
A reconnecting client must not receive a duplicate of a message it already has, nor miss one
Group size growth must not require every participant's client to hold a reference to every other participant
E. Clarifying Questions
#
Question
Why It Matters
Design Decision Affected
1
Is read/delivery status tracked per-recipient in a group, or just an aggregate for the whole conversation?
Per-recipient tracking means Message needs a receipt map, not a single status field
Shape of the delivery-status model
2
Can one user be connected from multiple devices simultaneously?
Introduces a presence race (is the user "online" if any device is connected, or the most-recently-active one?) and multi-device message fan-out
Design of the presence-tracking mechanism
3
How is message order determined when two participants send at nearly the same instant?
Client timestamps are subject to clock skew; determines whether ordering needs a server-assigned sequence
Design of per-conversation sequencing
4
What happens to messages sent while a recipient is offline?
Determines whether a per-user/per-conversation message queue with reconnect-replay is needed
Design of the offline-delivery queue
F. Assumptions
Confirmed: 1:1 and group messaging, delivery/read receipts, presence, exactly-once reconnect delivery.
Assumption: a user can be connected from multiple devices; presence is "online" if at least one device is connected.
Assumption: message ordering within a conversation is assigned by the server at receipt time, not derived from client-reported timestamps.
G. Actors
Actor
Responsibility
User
Sends/receives messages from one or more devices
Conversation (Direct or Group)
Owns the participant list, message order, and delivery fan-out
ChatService (facade)
Top-level API coordinating send, receipt tracking, and reconnect replay
H. Core Domain Entities
Class
Responsibility
Key Attributes
Key Methods
Does NOT Handle
User
One account, possibly connected from multiple devices
id, connectedDevices
—
Conversation membership logic directly
Conversation (abstract)
Mediator between its participants, and the Observer subject they're all subscribed to
participants, messages, nextSequence
send(message), addParticipant()
Transport-level delivery to a specific device
DirectConversation
Exactly-two-participant conversation
—
—
—
GroupConversation
N-participant conversation; the Mediator role is load-bearing here
—
removeParticipant()
—
Message
One message and its per-recipient receipts
sender, content, sequenceNo, receipts
—
—
MessageStatus (interface)
One delivery-status state's legal transitions, per recipient
—
onDelivered(), onRead()
—
PresenceTracker
A user's aggregate online/offline status across devices
Conversation is simultaneously a Mediator and an Observer subject — two complementary lenses, not competing patterns: as a Mediator, Conversation decouples its participants from each other — a User never holds a direct reference to any other participant, only to the conversations they're part of, which avoids an O(n²) mesh of peer references that would otherwise form as a group grows. As an Observer subject, Conversation is exactly the same subject/observer relationship this document has used repeatedly as a small hook — Ride-Sharing's driver-matched notification (Problem 5), Notification System's event triggers (Problem 7), Library's hold-queue availability (Problem 15), Hotel's booking confirmation (Problem 16), Movie Ticket's waitlist (Problem 17), Food Delivery's status push (Problem 18) — and generalized fully as the entire point of Pub-Sub (Problem 10). A chat conversation is arguably the most natural full-scale realization of that generalization: every participant is a subscriber, every sent message is a publish, and the whole document's Observer arc converges here.
DirectConversation needs no real mediation, but shares the base class anyway: with exactly two participants, there's no reference-explosion problem to solve, so the Mediator justification specifically doesn't kick in — but modeling it as the same Conversation abstraction (rather than a special-cased 1:1 path) keeps ChatService's API uniform regardless of conversation size, and costs nothing since a 2-subscriber topic is a trivial case of the general one.
MessageStatus as a per-recipient state machine:SENT → DELIVERED → READ, tracked independently per recipient in a group conversation (so "read by 3 of 5" is representable), rather than one status field on the message that can only mean one thing for every recipient at once.
J. Class Diagram
View Mermaid source
classDiagram
class User {
id
}
class Conversation {
participants
nextSequence
+send(message)
+addParticipant()
}
class DirectConversation
class GroupConversation {
+removeParticipant()
}
class Message {
sender
content
sequenceNo
receipts
}
class MessageStatus {
<<interface>>
+onDelivered()
+onRead()
}
class PresenceTracker {
+deviceConnected()
+deviceDisconnected()
+isOnline()
}
class ChatService {
+sendMessage()
+reconnect()
}
DirectConversation --|> Conversation
GroupConversation --|> Conversation
Conversation --> "*" User
Conversation --> "*" Message
Message --> MessageStatus
ChatService --> Conversation
ChatService --> PresenceTracker
K. Responsibility Mapping
Class
Responsibility
Does NOT Handle
Conversation
Participant decoupling (Mediator) and message fan-out (Observer subject), sequencing
Transport/socket delivery to a specific device
MessageStatus implementations
One recipient's delivery-status transitions
Fan-out itself
PresenceTracker
Aggregating multi-device connection state into one online/offline fact
One User class directly tracking every other user it's ever messaged, plus delivery status, plus presence
OCP
A new conversation type (e.g. a broadcast channel) is a new Conversation subtype; a new delivery-status nuance is a new MessageStatus
A ChatService.send() method with an if isGroup branch scattered through every method that touches messaging
LSP
DirectConversation and GroupConversation both honor the same send()/addParticipant() contract regardless of participant count
DirectConversation.addParticipant() silently doing nothing or throwing in a way callers don't expect from the shared interface
ISP
MessageStatus stays a small, focused interface
A combined "MessagePolicy" interface mixing delivery status with retention/encryption rules
DIP
ChatService depends on the Conversation abstraction, not on DirectConversation/GroupConversation concretely
ChatService.send() branching on conversation type internally instead of polymorphic dispatch
M. Design Patterns
Pattern
Problem It Solves
Where It Appears
When NOT to Use
Mediator
Participants shouldn't need direct references to every other participant as group size grows
GroupConversation
Weak justification for strictly 1:1 chat — see I
Observer
A sent message needs to reach every connected subscriber without the send path knowing who's listening
Conversation as Pub-Sub topic, reusing Problem 10's foundation
Never skip — this is the structural core of the whole problem
State
A message's delivery status has genuinely different legal next-transitions per recipient
MessageStatus
Skip if the round only needs a single boolean "delivered"
Explicitly avoided: giving every User direct references to every other participant they've ever chatted with — see I for why this is the specific mesh-growth problem Mediator exists to prevent.
PresenceTracker internals, same public isOnline()-style contract
O. Design Evolution
V1: one flat message list per pair of users, no groups, no receipts, no reconnect handling.
V2: introduce Conversation as the shared abstraction (Mediator + Observer subject) the moment groups are requested — this is exactly when direct user-to-user references would otherwise start multiplying.
V3: add per-recipient MessageStatus once delivery/read receipts are requested.
V4: add server-assigned sequencing once ordering-under-concurrent-sends becomes a real concern, not a demo simplification.
V5: add the reconnect-replay queue and PresenceTracker once offline delivery and multi-device presence are requested.
What stays stable:ChatService's public send/reconnect API. New conversation and content types are additive.
Ownership/validation notes:sendMessage() assigns the message's sequenceNo from the target Conversation's own monotonic counter — never from the client's clock — and reconnect() returns only messages strictly after lastSeenSequence, making replay naturally idempotent: calling it twice with the same cursor returns the same (or an empty) result, never a duplicate delivery.
Q. Pseudocode — Critical Logic
Conversation.send(sender, content):
lock(this.sequenceCounter)
try:
seq = ++this.nextSequence // server-assigned, monotonic per conversation
message = new Message(sender, content, seq)
finally:
unlock(this.sequenceCounter)
this.messages.append(message)
for participant in this.participants: // Observer fan-out
if presenceTracker.isOnline(participant):
deliver(participant, message)
else:
offlineQueueFor(participant).enqueue(message)
return message
ChatService.reconnect(user, lastSeenSequence):
missed = offlineQueueFor(user).allAfter(lastSeenSequence) // idempotent: same cursor -> same result
offlineQueueFor(user).clearUpTo(lastSeenSequence + missed.size)
return missed
Message send(User sender, String content) {
Message message;
lock(this.sequenceCounter);
try {
long seq = ++this.nextSequence; // server-assigned, monotonic per conversation
message = new Message(sender, content, seq);
} finally {
unlock(this.sequenceCounter);
}
this.messages.add(message);
for (User participant : this.participants) { // Observer fan-out
if (presenceTracker.isOnline(participant)) {
deliver(participant, message);
} else {
offlineQueueFor(participant).enqueue(message);
}
}
return message;
}
List<Message> reconnect(User user, long lastSeenSequence) {
List<Message> missed = offlineQueueFor(user).allAfter(lastSeenSequence); // idempotent: same cursor -> same result
offlineQueueFor(user).clearUpTo(lastSeenSequence + missed.size());
return missed;
}
std::shared_ptr<Message> Conversation::send(std::shared_ptr<User> sender, const std::string& content) {
std::shared_ptr<Message> message;
lock(sequenceCounter_);
try {
long seq = ++nextSequence_; // server-assigned, monotonic per conversation
message = std::make_shared<Message>(sender, content, seq);
} catch (...) {
unlock(sequenceCounter_);
throw;
}
unlock(sequenceCounter_);
messages_.push_back(message);
for (auto& participant : participants_) { // Observer fan-out
if (presenceTracker->isOnline(participant)) {
deliver(participant, message);
} else {
offlineQueueFor(participant)->enqueue(message);
}
}
return message;
}
std::vector<std::shared_ptr<Message>> ChatService::reconnect(std::shared_ptr<User> user, long lastSeenSequence) {
auto missed = offlineQueueFor(user)->allAfter(lastSeenSequence); // idempotent: same cursor -> same result
offlineQueueFor(user)->clearUpTo(lastSeenSequence + missed.size());
return missed;
}
send(sender, content) {
let message;
lock(this.sequenceCounter);
try {
const seq = ++this.nextSequence; // server-assigned, monotonic per conversation
message = new Message(sender, content, seq);
} finally {
unlock(this.sequenceCounter);
}
this.messages.push(message);
for (const participant of this.participants) { // Observer fan-out
if (presenceTracker.isOnline(participant)) {
deliver(participant, message);
} else {
offlineQueueFor(participant).enqueue(message);
}
}
return message;
}
reconnect(user, lastSeenSequence) {
const missed = offlineQueueFor(user).allAfter(lastSeenSequence); // idempotent: same cursor -> same result
offlineQueueFor(user).clearUpTo(lastSeenSequence + missed.length);
return missed;
}
send(sender: User, content: string): Message {
let message: Message;
lock(this.sequenceCounter);
try {
const seq = ++this.nextSequence; // server-assigned, monotonic per conversation
message = new Message(sender, content, seq);
} finally {
unlock(this.sequenceCounter);
}
this.messages.push(message);
for (const participant of this.participants) { // Observer fan-out
if (presenceTracker.isOnline(participant)) {
deliver(participant, message);
} else {
offlineQueueFor(participant).enqueue(message);
}
}
return message;
}
reconnect(user: User, lastSeenSequence: number): Message[] {
const missed = offlineQueueFor(user).allAfter(lastSeenSequence); // idempotent: same cursor -> same result
offlineQueueFor(user).clearUpTo(lastSeenSequence + missed.length);
return missed;
}
R. End-to-End Use Case
groupChat.send(userA, "Hi all") → sequenceNo=42 assigned under the conversation's counter
│ fan-out: userB online → delivered immediately; userC offline → enqueued
▼
userC reconnects with lastSeenSequence=41
│ reconnect() returns exactly [message #42], marked delivered
│ a duplicate reconnect() call with the same cursor returns the same result, not a re-delivery
▼
userB opens the chat → MessageStatus for userB's receipt transitions DELIVERED → READ
S. Data Structures
Structure
Used For
Why
Alternative & Trade-off
Monotonic per-conversation counter
Server-assigned message ordering
Immune to client clock skew, unlike sorting by client-reported timestamp
A distributed sequence (e.g. per-partition offset) is the real-world analogue at scale — see Y
Per-user offline message queue, keyed by conversation and sequence number
Exactly-once reconnect replay
A cursor (last-seen sequence) makes replay idempotent by construction — replaying from the same cursor always yields the same result
A simple unread-count flag is cheaper but can't reconstruct exactly which messages were missed
Receipt map (recipientId → MessageStatus) per message
Per-recipient delivery/read tracking in groups
A single status field can't represent "read by 3 of 5 participants"
—
T. Complexity Analysis
Operation
Time
Space
send() to a conversation with k participants
O(k) fan-out
O(1) amortized per participant's queue
reconnect()
O(missed messages)
O(missed messages) until delivered
Receipt status update
O(1) per recipient
O(participants) per message
U. Testing Strategy
Happy path: a message sent to a group is received by every online participant in the same relative order.
Edge cases: a participant offline at send time receives the message on reconnect, correctly marked as delivered only once they actually connect, not at send time.
Idempotency tests: calling reconnect() twice with the same lastSeenSequence cursor returns the same result and does not re-fire delivery receipts.
Concurrency tests: two participants sending to the same conversation at nearly the same instant receive server-assigned sequence numbers that are strictly ordered and consistent across every participant's view.
V. Interviewer Follow-Ups
Follow-Up
Testing
Design Change
Classes Changed
Pattern/Trade-off
"Add typing indicators and last-seen timestamps."
Whether presence was already isolated behind one abstraction
Extend PresenceTracker's internal state; public contract unchanged
PresenceTracker only
Validates the presence abstraction from N
"What if the same user is logged in on phone and laptop, and closes the laptop?"
Whether presence assumed a single device per user
PresenceTracker already models a device set — removing one device only flips to offline if it was the last one
None if designed correctly; PresenceTracker if not
Tests the multi-device race directly — see W
"Scale group conversations to thousands of participants (e.g. a broadcast channel)."
Whether fan-out was assumed to be synchronous, in-process delivery
Fan-out moves to an async message queue per participant, same conceptual Observer relationship, different transport
Conversation's delivery mechanism, not its public API
LLD→HLD boundary — see Y
W. Concurrency
Shared mutable state: a conversation's sequence counter, and a user's set of connected devices.
Per-conversation ordering must be centrally assigned: if two participants' clients each stamped their own message with a local timestamp, clock skew between devices could reorder messages inconsistently across different participants' views. Assigning sequenceNo from the conversation's own lock-guarded counter at receipt time guarantees a single, consistent order every participant agrees on.
Exactly-once delivery on reconnect: replaying missed messages by a cursor (last-seen sequence) rather than a "have I sent this before" flag per message makes the operation naturally idempotent — the same lesson already established for payment-webhook retries in Movie Ticket Booking (Problem 17) and E-commerce Checkout (Problem 19), now applied to message redelivery instead of payment confirmation.
The presence race across multiple devices: device A disconnecting while device B is simultaneously connecting for the same user is a check-then-act race on the user's aggregate online status; modeling PresenceTracker as a set of connected device ids (rather than a single boolean flipped by whichever event arrives last) avoids the race entirely — "online" is simply "is this set non-empty," and add/remove operations on a set are commutative regardless of arrival order.
X. Persistence
Messages and their receipts are durable by nature — a chat history that disappears on restart is a trust-destroying failure, matching Movie Ticket Booking's and E-commerce's treatment of confirmed transactional records. Presence state is the opposite: inherently transient, tied to an active connection, and reasonably rebuilt from scratch (as "offline") on any restart rather than persisted.
Y. Scaling Boundary
Layer
What Belongs Here
LLD
Conversation as Mediator+Observer, per-conversation sequencing, MessageStatus, PresenceTracker, reconnect-cursor replay
HLD
Distributed pub-sub transport for fan-out at scale (this document's Pub-Sub, Problem 10, generalized to production infrastructure), multi-region message replication, end-to-end encryption key exchange
Explicitly out of scope
Distributed-systems transport infrastructure and encryption protocol detail unless the round pivots to HLD
Z. Failure Scenarios
A participant's client crashes mid-delivery, after receiving a message but before acknowledging it: on reconnect, the cursor-based replay in reconnect() naturally re-delivers anything not yet acknowledged — the idempotent-by-construction design from W means this "failure" requires no special-case handling.
The conversation's sequence counter's lock is held by a slow send while a burst of new sends arrives: the lock window is intentionally tiny (just the increment, not the full fan-out), keeping contention bounded even under a message burst.
AA. Common Bad Designs
Giving every User direct references to every other participant they've chatted with instead of routing through Conversation as a Mediator — the defining mistake for group chat; see I.
Ordering messages by client-reported timestamp instead of a server-assigned sequence — breaks the instant two devices have any clock skew.
Tracking "delivered" as a single flag per message instead of a per-recipient receipt map — can't represent partial delivery/read state in a group.
Modeling presence as one boolean flipped by whichever connect/disconnect event arrives last instead of a device set — reintroduces exactly the race a set-based model avoids for free.
AB. Design Trade-Offs
Decision
Option A
Option B
Recommendation
Message ordering authority
Server-assigned sequence per conversation
Client-reported timestamp
Server-assigned — the only option that guarantees a single consistent order across every participant regardless of clock skew
Offline delivery fan-out
Per-participant queue, replayed on reconnect
Push-only, missed messages simply lost
Per-participant queue — a chat app that silently drops messages sent while a user is offline fails the core promise of the product
AC. Interviewer Evaluation
Strong signal: naming Conversation as both a Mediator and an Observer subject and explaining why those are complementary, not competing, framings; assigning message order from a server-side counter unprompted; designing reconnect as cursor-based (hence idempotent) rather than flag-based; modeling presence as a device set rather than a single racy boolean.
Red flag: direct peer references between users in a group; client-timestamp-based ordering; a single delivered/not-delivered boolean per message; a presence boolean that races across devices.
AD. Interview Simulation
Interviewer: Design a chat application supporting both 1:1 and group messaging.
Candidate: I'd model a Conversation as the shared abstraction for both — every participant holds a reference to the conversation, never to each other directly. That's a Mediator role: it prevents an O(n²) mesh of direct references as a group grows. It's simultaneously an Observer subject — every participant is a subscriber, and sending a message is a publish. That's the same subject/observer shape this design has used as a small hook in several other systems, generalized fully here.
Interviewer: How do you keep message order consistent for everyone in a group?
Candidate: I wouldn't trust client timestamps — clock skew between devices could reorder messages differently for different viewers. I'd assign a sequence number from the conversation's own server-side counter at the moment the message is received, so there's one authoritative order everyone agrees on.
Interviewer: A user reconnects after being offline for an hour. How do you avoid duplicating or losing messages?
Candidate: Reconnect takes the client's last-seen sequence number as a cursor and replays everything strictly after it. That's naturally idempotent — replaying from the same cursor twice returns the same result, so a retried reconnect can't double-deliver.
Interviewer: What about a user logged in on two devices, and one disconnects?
Candidate: I'd model presence as a set of connected device ids per user rather than one boolean — "online" just means the set is non-empty. Add and remove on a set commute regardless of arrival order, which sidesteps the race a single last-writer-wins boolean would have.
Requirements-and-entity walkthrough covering 1:1/group messaging, delivery status, and presence
Clear functional decomposition of the messaging surface
Group chat is generally not explicitly framed through Mediator, and message ordering is often left as an implementation detail rather than an explicit server-assigned-sequence decision
Good functional baseline; this document's explicit Mediator+Observer framing and sequencing/idempotency depth are the additions worth layering on top
Direct peer references instead of Mediator; client-timestamp ordering; single delivered flag instead of per-recipient receipts; racy presence boolean
What NOT to over-engineer
Don't design distributed transport infrastructure or encryption protocols unless asked
AG. Full Implementation
A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.
// Problem 20: Chat Application (WhatsApp-style)
// Patterns: Mediator + Observer (Conversation), State (MessageStatus per recipient), Facade (ChatService)
import java.util.*;
public class p20 {
// ---------- User ----------
static class User {
final String id;
final Set<String> connectedDevices = new HashSet<>();
User(String id) { this.id = id; }
}
// ---------- MessageStatus (per-recipient state machine) ----------
interface MessageStatus {
MessageStatus onDelivered();
MessageStatus onRead();
String name();
}
static class SentStatus implements MessageStatus {
@Override public MessageStatus onDelivered() { return new DeliveredStatus(); }
@Override public MessageStatus onRead() { throw new IllegalStateException("Must be delivered before read"); }
@Override public String name() { return "SENT"; }
}
static class DeliveredStatus implements MessageStatus {
@Override public MessageStatus onDelivered() { return this; }
@Override public MessageStatus onRead() { return new ReadStatus(); }
@Override public String name() { return "DELIVERED"; }
}
static class ReadStatus implements MessageStatus {
@Override public MessageStatus onDelivered() { return this; }
@Override public MessageStatus onRead() { return this; }
@Override public String name() { return "READ"; }
}
// ---------- Message ----------
static class Message {
final User sender;
final String content;
final long sequenceNo;
final Map<String, MessageStatus> receipts = new HashMap<>(); // recipientId -> status
Message(User sender, String content, long sequenceNo, List<User> recipients) {
this.sender = sender; this.content = content; this.sequenceNo = sequenceNo;
for (User r : recipients) receipts.put(r.id, new SentStatus());
}
void markDelivered(String userId) { receipts.computeIfPresent(userId, (k, s) -> s.onDelivered()); }
void markRead(String userId) { receipts.computeIfPresent(userId, (k, s) -> s.onRead()); }
}
// ---------- PresenceTracker ----------
static class PresenceTracker {
private final Map<String, Set<String>> connectedDeviceIds = new HashMap<>();
void deviceConnected(String userId, String deviceId) {
connectedDeviceIds.computeIfAbsent(userId, k -> new HashSet<>()).add(deviceId);
}
void deviceDisconnected(String userId, String deviceId) {
Set<String> devices = connectedDeviceIds.get(userId);
if (devices != null) devices.remove(deviceId);
}
boolean isOnline(String userId) {
Set<String> devices = connectedDeviceIds.get(userId);
return devices != null && !devices.isEmpty();
}
}
// Simple per-user append-only log of messages missed while offline.
// NOTE (interview-scale simplification): the cursor passed to reconnect() is treated as an
// index into this per-user queue (not the message's own per-conversation sequenceNo), since
// a user's offline queue spans multiple conversations whose sequence numbers aren't
// comparable to each other. Same cursor -> same slice, so replay is idempotent.
static class OfflineQueue {
private final List<Message> messages = new ArrayList<>();
void enqueue(Message m) { messages.add(m); }
List<Message> allAfter(long cursor) {
int from = Math.min((int) cursor, messages.size());
return new ArrayList<>(messages.subList(from, messages.size()));
}
void clearUpTo(long cursor) {
int upTo = Math.min((int) cursor, messages.size());
messages.subList(0, upTo).clear();
}
}
// ---------- Conversation (Mediator + Observer subject) ----------
static abstract class Conversation {
final String id;
final List<User> participants = new ArrayList<>();
final List<Message> messages = new ArrayList<>();
long nextSequence = 0;
private final PresenceTracker presenceTracker;
private final Map<String, OfflineQueue> offlineQueues; // shared reference, owned by ChatService
Conversation(String id, PresenceTracker presenceTracker, Map<String, OfflineQueue> offlineQueues) {
this.id = id; this.presenceTracker = presenceTracker; this.offlineQueues = offlineQueues;
}
void addParticipant(User user) {
if (!participants.contains(user)) participants.add(user);
}
Message send(User sender, String content) {
long seq = ++nextSequence; // server-assigned, monotonic per conversation
List<User> recipients = new ArrayList<>();
for (User p : participants) if (!p.id.equals(sender.id)) recipients.add(p);
Message message = new Message(sender, content, seq, recipients);
messages.add(message);
for (User participant : recipients) { // Observer fan-out
if (presenceTracker.isOnline(participant.id)) {
message.markDelivered(participant.id);
} else {
offlineQueues.computeIfAbsent(participant.id, k -> new OfflineQueue()).enqueue(message);
}
}
return message;
}
}
static class DirectConversation extends Conversation {
DirectConversation(String id, User a, User b, PresenceTracker tracker, Map<String, OfflineQueue> offlineQueues) {
super(id, tracker, offlineQueues);
participants.add(a); participants.add(b);
}
@Override void addParticipant(User user) {
throw new UnsupportedOperationException("DirectConversation is fixed at 2 participants");
}
}
static class GroupConversation extends Conversation {
GroupConversation(String id, List<User> initialParticipants, PresenceTracker tracker, Map<String, OfflineQueue> offlineQueues) {
super(id, tracker, offlineQueues);
participants.addAll(initialParticipants);
}
void removeParticipant(User user) { participants.remove(user); }
}
// ---------- ChatService (Facade) ----------
static class ChatService {
private final PresenceTracker presenceTracker = new PresenceTracker();
private final Map<String, OfflineQueue> offlineQueues = new HashMap<>();
private int nextConversationId = 1;
PresenceTracker getPresenceTracker() { return presenceTracker; }
DirectConversation createDirectConversation(User a, User b) {
return new DirectConversation("C" + (nextConversationId++), a, b, presenceTracker, offlineQueues);
}
GroupConversation createGroupConversation(List<User> participants) {
return new GroupConversation("C" + (nextConversationId++), participants, presenceTracker, offlineQueues);
}
Message sendMessage(User sender, Conversation conversation, String content) {
return conversation.send(sender, content);
}
// Exactly-once replay: client passes back the cursor from its last successful reconnect.
List<Message> reconnect(User user, long lastSeenSequence) {
OfflineQueue queue = offlineQueues.computeIfAbsent(user.id, k -> new OfflineQueue());
List<Message> missed = queue.allAfter(lastSeenSequence);
queue.clearUpTo(lastSeenSequence + missed.size());
for (Message m : missed) m.markDelivered(user.id);
return missed;
}
}
// ---------- Demo ----------
public static void main(String[] args) {
ChatService chatService = new ChatService();
User alice = new User("alice");
User bob = new User("bob");
User carol = new User("carol");
chatService.getPresenceTracker().deviceConnected("alice", "alice-phone");
chatService.getPresenceTracker().deviceConnected("bob", "bob-laptop");
// carol is offline (no connected device)
GroupConversation group = chatService.createGroupConversation(Arrays.asList(alice, bob, carol));
Message m1 = chatService.sendMessage(alice, group, "Hello everyone!");
System.out.println("Sent seq=" + m1.sequenceNo + " bob receipt=" + m1.receipts.get("bob").name()
+ " carol receipt=" + m1.receipts.get("carol").name());
Message m2 = chatService.sendMessage(bob, group, "Hi Alice!");
System.out.println("Sent seq=" + m2.sequenceNo);
// Carol reconnects and should receive both missed messages, exactly once, in order.
List<Message> missed = chatService.reconnect(carol, 0);
System.out.println("Carol missed " + missed.size() + " messages, first content='" + missed.get(0).content + "'");
// Idempotent retry with an already-advanced cursor should now return nothing new.
List<Message> replay = chatService.reconnect(carol, missed.size());
System.out.println("Carol replay after ack: " + replay.size() + " messages (expect 0)");
m1.markRead("bob");
System.out.println("Bob's receipt for m1 after reading: " + m1.receipts.get("bob").name());
}
}
// Problem 20: Chat Application (WhatsApp-style)
// Patterns: Mediator + Observer (Conversation), State (MessageStatus per recipient), Facade (ChatService)
#include <iostream>
#include <vector>
#include <map>
#include <set>
#include <memory>
#include <string>
#include <algorithm>
#include <stdexcept>
using namespace std;
// ---------- User ----------
struct User {
string id;
set<string> connectedDevices;
User(string id_) : id(move(id_)) {}
};
// ---------- MessageStatus (per-recipient state machine) ----------
class MessageStatus {
public:
virtual ~MessageStatus() = default;
virtual shared_ptr<MessageStatus> onDelivered() = 0;
virtual shared_ptr<MessageStatus> onRead() = 0;
virtual string name() const = 0;
};
class SentStatus : public MessageStatus {
public:
shared_ptr<MessageStatus> onDelivered() override;
shared_ptr<MessageStatus> onRead() override { throw runtime_error("Must be delivered before read"); }
string name() const override { return "SENT"; }
};
class DeliveredStatus : public MessageStatus {
public:
shared_ptr<MessageStatus> onDelivered() override { return make_shared<DeliveredStatus>(); }
shared_ptr<MessageStatus> onRead() override;
string name() const override { return "DELIVERED"; }
};
class ReadStatus : public MessageStatus {
public:
shared_ptr<MessageStatus> onDelivered() override { return make_shared<ReadStatus>(); }
shared_ptr<MessageStatus> onRead() override { return make_shared<ReadStatus>(); }
string name() const override { return "READ"; }
};
shared_ptr<MessageStatus> SentStatus::onDelivered() { return make_shared<DeliveredStatus>(); }
shared_ptr<MessageStatus> DeliveredStatus::onRead() { return make_shared<ReadStatus>(); }
// ---------- Message ----------
class Message {
public:
shared_ptr<User> sender;
string content;
long long sequenceNo;
map<string, shared_ptr<MessageStatus>> receipts; // recipientId -> status
Message(shared_ptr<User> sender_, string content_, long long seq, const vector<shared_ptr<User>>& recipients)
: sender(sender_), content(move(content_)), sequenceNo(seq) {
for (auto& r : recipients) receipts[r->id] = make_shared<SentStatus>();
}
void markDelivered(const string& userId) {
auto it = receipts.find(userId);
if (it != receipts.end()) it->second = it->second->onDelivered();
}
void markRead(const string& userId) {
auto it = receipts.find(userId);
if (it != receipts.end()) it->second = it->second->onRead();
}
};
// ---------- PresenceTracker ----------
class PresenceTracker {
map<string, set<string>> connectedDeviceIds;
public:
void deviceConnected(const string& userId, const string& deviceId) { connectedDeviceIds[userId].insert(deviceId); }
void deviceDisconnected(const string& userId, const string& deviceId) {
auto it = connectedDeviceIds.find(userId);
if (it != connectedDeviceIds.end()) it->second.erase(deviceId);
}
bool isOnline(const string& userId) {
auto it = connectedDeviceIds.find(userId);
return it != connectedDeviceIds.end() && !it->second.empty();
}
};
// Simple per-user append-only log of messages missed while offline.
// NOTE (interview-scale simplification): the cursor passed to reconnect() is treated as an
// index into this per-user queue (not the message's own per-conversation sequenceNo), since
// a user's offline queue spans multiple conversations whose sequence numbers aren't
// comparable to each other. Same cursor -> same slice, so replay is idempotent.
class OfflineQueue {
vector<shared_ptr<Message>> messages;
public:
void enqueue(shared_ptr<Message> m) { messages.push_back(m); }
vector<shared_ptr<Message>> allAfter(long long cursor) {
size_t from = min((size_t) max<long long>(cursor, 0), messages.size());
return vector<shared_ptr<Message>>(messages.begin() + from, messages.end());
}
void clearUpTo(long long cursor) {
size_t upTo = min((size_t) max<long long>(cursor, 0), messages.size());
messages.erase(messages.begin(), messages.begin() + upTo);
}
};
// ---------- Conversation (Mediator + Observer subject) ----------
class Conversation {
protected:
PresenceTracker* presenceTracker;
map<string, shared_ptr<OfflineQueue>>* offlineQueues; // shared reference, owned by ChatService
public:
string id;
vector<shared_ptr<User>> participants;
vector<shared_ptr<Message>> messages;
long long nextSequence = 0;
Conversation(string id_, PresenceTracker* tracker, map<string, shared_ptr<OfflineQueue>>* queues)
: presenceTracker(tracker), offlineQueues(queues), id(move(id_)) {}
virtual ~Conversation() = default;
virtual void addParticipant(shared_ptr<User> user) {
if (find(participants.begin(), participants.end(), user) == participants.end()) participants.push_back(user);
}
shared_ptr<Message> send(shared_ptr<User> sender, const string& content) {
long long seq = ++nextSequence; // server-assigned, monotonic per conversation
vector<shared_ptr<User>> recipients;
for (auto& p : participants) if (p->id != sender->id) recipients.push_back(p);
auto message = make_shared<Message>(sender, content, seq, recipients);
messages.push_back(message);
for (auto& participant : recipients) { // Observer fan-out
if (presenceTracker->isOnline(participant->id)) {
message->markDelivered(participant->id);
} else {
auto& q = (*offlineQueues)[participant->id];
if (!q) q = make_shared<OfflineQueue>();
q->enqueue(message);
}
}
return message;
}
};
class DirectConversation : public Conversation {
public:
DirectConversation(string id_, shared_ptr<User> a, shared_ptr<User> b, PresenceTracker* tracker,
map<string, shared_ptr<OfflineQueue>>* queues)
: Conversation(move(id_), tracker, queues) {
participants.push_back(a); participants.push_back(b);
}
void addParticipant(shared_ptr<User> user) override {
throw runtime_error("DirectConversation is fixed at 2 participants");
}
};
class GroupConversation : public Conversation {
public:
GroupConversation(string id_, vector<shared_ptr<User>> initial, PresenceTracker* tracker,
map<string, shared_ptr<OfflineQueue>>* queues)
: Conversation(move(id_), tracker, queues) {
participants = move(initial);
}
void removeParticipant(shared_ptr<User> user) {
participants.erase(remove(participants.begin(), participants.end(), user), participants.end());
}
};
// ---------- ChatService (Facade) ----------
class ChatService {
PresenceTracker presenceTracker;
map<string, shared_ptr<OfflineQueue>> offlineQueues;
int nextConversationId = 1;
public:
PresenceTracker& getPresenceTracker() { return presenceTracker; }
shared_ptr<DirectConversation> createDirectConversation(shared_ptr<User> a, shared_ptr<User> b) {
return make_shared<DirectConversation>("C" + to_string(nextConversationId++), a, b, &presenceTracker, &offlineQueues);
}
shared_ptr<GroupConversation> createGroupConversation(vector<shared_ptr<User>> participants) {
return make_shared<GroupConversation>("C" + to_string(nextConversationId++), move(participants), &presenceTracker, &offlineQueues);
}
shared_ptr<Message> sendMessage(shared_ptr<User> sender, shared_ptr<Conversation> conversation, const string& content) {
return conversation->send(sender, content);
}
// Exactly-once replay: client passes back the cursor from its last successful reconnect.
vector<shared_ptr<Message>> reconnect(shared_ptr<User> user, long long lastSeenSequence) {
auto& q = offlineQueues[user->id];
if (!q) q = make_shared<OfflineQueue>();
auto missed = q->allAfter(lastSeenSequence);
q->clearUpTo(lastSeenSequence + (long long) missed.size());
for (auto& m : missed) m->markDelivered(user->id);
return missed;
}
};
// ---------- Demo ----------
int main() {
ChatService chatService;
auto alice = make_shared<User>("alice");
auto bob = make_shared<User>("bob");
auto carol = make_shared<User>("carol");
chatService.getPresenceTracker().deviceConnected("alice", "alice-phone");
chatService.getPresenceTracker().deviceConnected("bob", "bob-laptop");
// carol is offline (no connected device)
auto group = chatService.createGroupConversation({alice, bob, carol});
auto m1 = chatService.sendMessage(alice, group, "Hello everyone!");
cout << "Sent seq=" << m1->sequenceNo << " bob receipt=" << m1->receipts["bob"]->name()
<< " carol receipt=" << m1->receipts["carol"]->name() << endl;
auto m2 = chatService.sendMessage(bob, group, "Hi Alice!");
cout << "Sent seq=" << m2->sequenceNo << endl;
auto missed = chatService.reconnect(carol, 0);
cout << "Carol missed " << missed.size() << " messages, first content='" << missed[0]->content << "'" << endl;
auto replay = chatService.reconnect(carol, (long long) missed.size());
cout << "Carol replay after ack: " << replay.size() << " messages (expect 0)" << endl;
m1->markRead("bob");
cout << "Bob's receipt for m1 after reading: " << m1->receipts["bob"]->name() << endl;
return 0;
}
// Problem 20: Chat Application (WhatsApp-style)
// Patterns: Mediator + Observer (Conversation), State (MessageStatus per recipient), Facade (ChatService)
// ---------- User ----------
class User {
constructor(id) { this.id = id; this.connectedDevices = new Set(); }
}
// ---------- MessageStatus (per-recipient state machine) ----------
class SentStatus {
onDelivered() { return new DeliveredStatus(); }
onRead() { throw new Error('Must be delivered before read'); }
name() { return 'SENT'; }
}
class DeliveredStatus {
onDelivered() { return this; }
onRead() { return new ReadStatus(); }
name() { return 'DELIVERED'; }
}
class ReadStatus {
onDelivered() { return this; }
onRead() { return this; }
name() { return 'READ'; }
}
// ---------- Message ----------
class Message {
constructor(sender, content, sequenceNo, recipients) {
this.sender = sender; this.content = content; this.sequenceNo = sequenceNo;
this.receipts = new Map(); // recipientId -> status
for (const r of recipients) this.receipts.set(r.id, new SentStatus());
}
markDelivered(userId) {
if (this.receipts.has(userId)) this.receipts.set(userId, this.receipts.get(userId).onDelivered());
}
markRead(userId) {
if (this.receipts.has(userId)) this.receipts.set(userId, this.receipts.get(userId).onRead());
}
}
// ---------- PresenceTracker ----------
class PresenceTracker {
constructor() { this.connectedDeviceIds = new Map(); }
deviceConnected(userId, deviceId) {
if (!this.connectedDeviceIds.has(userId)) this.connectedDeviceIds.set(userId, new Set());
this.connectedDeviceIds.get(userId).add(deviceId);
}
deviceDisconnected(userId, deviceId) {
const devices = this.connectedDeviceIds.get(userId);
if (devices) devices.delete(deviceId);
}
isOnline(userId) {
const devices = this.connectedDeviceIds.get(userId);
return !!devices && devices.size > 0;
}
}
// Simple per-user append-only log of messages missed while offline.
// NOTE (interview-scale simplification): the cursor passed to reconnect() is treated as an
// index into this per-user queue (not the message's own per-conversation sequenceNo), since
// a user's offline queue spans multiple conversations whose sequence numbers aren't
// comparable to each other. Same cursor -> same slice, so replay is idempotent.
class OfflineQueue {
constructor() { this.messages = []; }
enqueue(m) { this.messages.push(m); }
allAfter(cursor) {
const from = Math.min(cursor, this.messages.length);
return this.messages.slice(from);
}
clearUpTo(cursor) {
const upTo = Math.min(cursor, this.messages.length);
this.messages.splice(0, upTo);
}
}
// ---------- Conversation (Mediator + Observer subject) ----------
class Conversation {
constructor(id, presenceTracker, offlineQueues) {
this.id = id;
this.participants = [];
this.messages = [];
this.nextSequence = 0;
this.presenceTracker = presenceTracker;
this.offlineQueues = offlineQueues; // shared Map reference, owned by ChatService
}
addParticipant(user) {
if (!this.participants.includes(user)) this.participants.push(user);
}
send(sender, content) {
const seq = ++this.nextSequence; // server-assigned, monotonic per conversation
const recipients = this.participants.filter(p => p.id !== sender.id);
const message = new Message(sender, content, seq, recipients);
this.messages.push(message);
for (const participant of recipients) { // Observer fan-out
if (this.presenceTracker.isOnline(participant.id)) {
message.markDelivered(participant.id);
} else {
if (!this.offlineQueues.has(participant.id)) this.offlineQueues.set(participant.id, new OfflineQueue());
this.offlineQueues.get(participant.id).enqueue(message);
}
}
return message;
}
}
class DirectConversation extends Conversation {
constructor(id, a, b, presenceTracker, offlineQueues) {
super(id, presenceTracker, offlineQueues);
this.participants.push(a, b);
}
addParticipant() { throw new Error('DirectConversation is fixed at 2 participants'); }
}
class GroupConversation extends Conversation {
constructor(id, initialParticipants, presenceTracker, offlineQueues) {
super(id, presenceTracker, offlineQueues);
this.participants.push(...initialParticipants);
}
removeParticipant(user) { this.participants = this.participants.filter(p => p !== user); }
}
// ---------- ChatService (Facade) ----------
class ChatService {
constructor() {
this.presenceTracker = new PresenceTracker();
this.offlineQueues = new Map();
this.nextConversationId = 1;
}
createDirectConversation(a, b) {
return new DirectConversation(`C${this.nextConversationId++}`, a, b, this.presenceTracker, this.offlineQueues);
}
createGroupConversation(participants) {
return new GroupConversation(`C${this.nextConversationId++}`, participants, this.presenceTracker, this.offlineQueues);
}
sendMessage(sender, conversation, content) {
return conversation.send(sender, content);
}
// Exactly-once replay: client passes back the cursor from its last successful reconnect.
reconnect(user, lastSeenSequence) {
if (!this.offlineQueues.has(user.id)) this.offlineQueues.set(user.id, new OfflineQueue());
const queue = this.offlineQueues.get(user.id);
const missed = queue.allAfter(lastSeenSequence);
queue.clearUpTo(lastSeenSequence + missed.length);
for (const m of missed) m.markDelivered(user.id);
return missed;
}
}
// ---------- Demo ----------
function main() {
const chatService = new ChatService();
const alice = new User('alice');
const bob = new User('bob');
const carol = new User('carol');
chatService.presenceTracker.deviceConnected('alice', 'alice-phone');
chatService.presenceTracker.deviceConnected('bob', 'bob-laptop');
// carol is offline (no connected device)
const group = chatService.createGroupConversation([alice, bob, carol]);
const m1 = chatService.sendMessage(alice, group, 'Hello everyone!');
console.log(`Sent seq=${m1.sequenceNo} bob receipt=${m1.receipts.get('bob').name()} carol receipt=${m1.receipts.get('carol').name()}`);
const m2 = chatService.sendMessage(bob, group, 'Hi Alice!');
console.log(`Sent seq=${m2.sequenceNo}`);
const missed = chatService.reconnect(carol, 0);
console.log(`Carol missed ${missed.length} messages, first content='${missed[0].content}'`);
const replay = chatService.reconnect(carol, missed.length);
console.log(`Carol replay after ack: ${replay.length} messages (expect 0)`);
m1.markRead('bob');
console.log(`Bob's receipt for m1 after reading: ${m1.receipts.get('bob').name()}`);
}
if (require.main === module) main();
module.exports = {
User, SentStatus, DeliveredStatus, ReadStatus, Message, PresenceTracker,
OfflineQueue, Conversation, DirectConversation, GroupConversation, ChatService,
};
// Problem 20: Chat Application (WhatsApp-style)
// Patterns: Mediator + Observer (Conversation), State (MessageStatus per recipient), Facade (ChatService)
// ---------- User ----------
class User {
connectedDevices: Set<string> = new Set();
constructor(public id: string) {}
}
// ---------- MessageStatus (per-recipient state machine) ----------
interface MessageStatus {
onDelivered(): MessageStatus;
onRead(): MessageStatus;
name(): string;
}
class SentStatus implements MessageStatus {
onDelivered(): MessageStatus { return new DeliveredStatus(); }
onRead(): MessageStatus { throw new Error('Must be delivered before read'); }
name(): string { return 'SENT'; }
}
class DeliveredStatus implements MessageStatus {
onDelivered(): MessageStatus { return this; }
onRead(): MessageStatus { return new ReadStatus(); }
name(): string { return 'DELIVERED'; }
}
class ReadStatus implements MessageStatus {
onDelivered(): MessageStatus { return this; }
onRead(): MessageStatus { return this; }
name(): string { return 'READ'; }
}
// ---------- Message ----------
class Message {
readonly receipts = new Map<string, MessageStatus>(); // recipientId -> status
constructor(public sender: User, public content: string, public sequenceNo: number, recipients: User[]) {
for (const r of recipients) this.receipts.set(r.id, new SentStatus());
}
markDelivered(userId: string): void {
const status = this.receipts.get(userId);
if (status) this.receipts.set(userId, status.onDelivered());
}
markRead(userId: string): void {
const status = this.receipts.get(userId);
if (status) this.receipts.set(userId, status.onRead());
}
}
// ---------- PresenceTracker ----------
class PresenceTracker {
private connectedDeviceIds = new Map<string, Set<string>>();
deviceConnected(userId: string, deviceId: string): void {
if (!this.connectedDeviceIds.has(userId)) this.connectedDeviceIds.set(userId, new Set());
this.connectedDeviceIds.get(userId)!.add(deviceId);
}
deviceDisconnected(userId: string, deviceId: string): void {
this.connectedDeviceIds.get(userId)?.delete(deviceId);
}
isOnline(userId: string): boolean {
const devices = this.connectedDeviceIds.get(userId);
return !!devices && devices.size > 0;
}
}
// Simple per-user append-only log of messages missed while offline.
// NOTE (interview-scale simplification): the cursor passed to reconnect() is treated as an
// index into this per-user queue (not the message's own per-conversation sequenceNo), since
// a user's offline queue spans multiple conversations whose sequence numbers aren't
// comparable to each other. Same cursor -> same slice, so replay is idempotent.
class OfflineQueue {
private messages: Message[] = [];
enqueue(m: Message): void { this.messages.push(m); }
allAfter(cursor: number): Message[] {
const from = Math.min(cursor, this.messages.length);
return this.messages.slice(from);
}
clearUpTo(cursor: number): void {
const upTo = Math.min(cursor, this.messages.length);
this.messages.splice(0, upTo);
}
}
// ---------- Conversation (Mediator + Observer subject) ----------
abstract class Conversation {
participants: User[] = [];
messages: Message[] = [];
nextSequence = 0;
constructor(
public readonly id: string,
protected presenceTracker: PresenceTracker,
protected offlineQueues: Map<string, OfflineQueue>, // shared reference, owned by ChatService
) {}
addParticipant(user: User): void {
if (!this.participants.includes(user)) this.participants.push(user);
}
send(sender: User, content: string): Message {
const seq = ++this.nextSequence; // server-assigned, monotonic per conversation
const recipients = this.participants.filter(p => p.id !== sender.id);
const message = new Message(sender, content, seq, recipients);
this.messages.push(message);
for (const participant of recipients) { // Observer fan-out
if (this.presenceTracker.isOnline(participant.id)) {
message.markDelivered(participant.id);
} else {
if (!this.offlineQueues.has(participant.id)) this.offlineQueues.set(participant.id, new OfflineQueue());
this.offlineQueues.get(participant.id)!.enqueue(message);
}
}
return message;
}
}
class DirectConversation extends Conversation {
constructor(id: string, a: User, b: User, presenceTracker: PresenceTracker, offlineQueues: Map<string, OfflineQueue>) {
super(id, presenceTracker, offlineQueues);
this.participants.push(a, b);
}
override addParticipant(_user: User): void { throw new Error('DirectConversation is fixed at 2 participants'); }
}
class GroupConversation extends Conversation {
constructor(id: string, initialParticipants: User[], presenceTracker: PresenceTracker, offlineQueues: Map<string, OfflineQueue>) {
super(id, presenceTracker, offlineQueues);
this.participants.push(...initialParticipants);
}
removeParticipant(user: User): void { this.participants = this.participants.filter(p => p !== user); }
}
// ---------- ChatService (Facade) ----------
class ChatService {
readonly presenceTracker = new PresenceTracker();
private offlineQueues = new Map<string, OfflineQueue>();
private nextConversationId = 1;
createDirectConversation(a: User, b: User): DirectConversation {
return new DirectConversation(`C${this.nextConversationId++}`, a, b, this.presenceTracker, this.offlineQueues);
}
createGroupConversation(participants: User[]): GroupConversation {
return new GroupConversation(`C${this.nextConversationId++}`, participants, this.presenceTracker, this.offlineQueues);
}
sendMessage(sender: User, conversation: Conversation, content: string): Message {
return conversation.send(sender, content);
}
// Exactly-once replay: client passes back the cursor from its last successful reconnect.
reconnect(user: User, lastSeenSequence: number): Message[] {
if (!this.offlineQueues.has(user.id)) this.offlineQueues.set(user.id, new OfflineQueue());
const queue = this.offlineQueues.get(user.id)!;
const missed = queue.allAfter(lastSeenSequence);
queue.clearUpTo(lastSeenSequence + missed.length);
for (const m of missed) m.markDelivered(user.id);
return missed;
}
}
// ---------- Demo ----------
function main(): void {
const chatService = new ChatService();
const alice = new User('alice');
const bob = new User('bob');
const carol = new User('carol');
chatService.presenceTracker.deviceConnected('alice', 'alice-phone');
chatService.presenceTracker.deviceConnected('bob', 'bob-laptop');
// carol is offline (no connected device)
const group = chatService.createGroupConversation([alice, bob, carol]);
const m1 = chatService.sendMessage(alice, group, 'Hello everyone!');
console.log(`Sent seq=${m1.sequenceNo} bob receipt=${m1.receipts.get('bob')!.name()} carol receipt=${m1.receipts.get('carol')!.name()}`);
const m2 = chatService.sendMessage(bob, group, 'Hi Alice!');
console.log(`Sent seq=${m2.sequenceNo}`);
const missed = chatService.reconnect(carol, 0);
console.log(`Carol missed ${missed.length} messages, first content='${missed[0].content}'`);
const replay = chatService.reconnect(carol, missed.length);
console.log(`Carol replay after ack: ${replay.length} messages (expect 0)`);
m1.markRead('bob');
console.log(`Bob's receipt for m1 after reading: ${m1.receipts.get('bob')!.name()}`);
}
main();
export {
User, MessageStatus, SentStatus, DeliveredStatus, ReadStatus, Message, PresenceTracker,
OfflineQueue, Conversation, DirectConversation, GroupConversation, ChatService,
};