How to Add Persistent Chat History
Load persisted thread messages, merge them safely with live deliveries, and refetch after reconnecting to fill gaps.
Load past messages for a conversation thread with fetchChatHistory. On WebSocket clients, the response arrives via onMessage — not as the return value of the method call.
When to Fetch
| Moment | Action |
|---|---|
| User opens a thread | Fetch history for the peer ID or IDs |
| App returns from the background | Refetch or merge with live messages since the last view |
| After reconnecting | Refetch to fill gaps from the offline period |
API Contract
| Field | Description |
|---|---|
| senderId | The requesting user; must match the connected userId |
| receiverIds | Array of peer user IDs defining the thread |
await notifier.fetchChatHistory({
senderId: "user-alice",
receiverIds: ["user-bob"],
});
// Response delivered to onMessageFor 1:1 chat, pass a single peer ID in receiverIds. The server returns messages exchanged between the participants.
Handling the Response
History payloads arrive in onMessage. Their shape is application-dependent but commonly includes a messages array:
onMessage: (msg) => {
const body = msg.payload.toJSON();
if (Array.isArray(body?.messages)) {
setThreadMessages(body.messages);
}
},Distinguish history responses from live messages by checking for the messages array or a metadata hint your app recognizes.
HTTP Transport Note
With transport: "http", fetchChatHistory may return the response directly from the async call instead of routing it through onMessage. For chat UIs, prefer ws for unified live and history handling.
JavaScript / TypeScript
Fetch and render chat history with fetchChatHistory.
Fetch on Thread Open
import WebSocket from "ws";
import { DNotifier } from "@dnotifier-realtime/dnotifier";
const MY_USER_ID = "user-alice";
const PEER_USER_ID = "user-bob";
const notifier = new DNotifier({
appId: process.env.DNOTIFIER_APP_ID,
secret: process.env.DNOTIFIER_SECRET,
transport: "ws",
userId: MY_USER_ID,
WebSocketImpl: WebSocket,
onConnected: async () => {
await notifier.fetchChatHistory({
senderId: MY_USER_ID,
receiverIds: [PEER_USER_ID],
});
},
onMessage: (msg) => {
const body = msg.payload.toJSON();
if (Array.isArray(body?.messages)) {
console.log(`Loaded ${body.messages.length} history messages`);
renderHistory(body.messages);
return;
}
if (body?.type === "text") {
appendLiveMessage(msg.metadata, body);
}
},
onDisconnected: () => {},
});
await notifier.connect();Merge History With Live Messages
const messageMap = new Map();
function upsertMessage(msg) {
const id = msg.id ?? `${msg.sender}_${msg.timestamp}`;
messageMap.set(id, msg);
renderSorted([...messageMap.values()]);
}
onMessage: (msg) => {
const body = msg.payload.toJSON();
if (Array.isArray(body?.messages)) {
body.messages.forEach(upsertMessage);
return;
}
if (body?.type === "text") {
upsertMessage({
id: msg.metadata.id,
sender: msg.metadata.sender,
text: body.text,
timestamp: msg.metadata.timestamp,
});
}
},Refetch After Reconnect
async function onReconnect() {
await notifier.connect();
await notifier.fetchChatHistory({
senderId: MY_USER_ID,
receiverIds: [PEER_USER_ID],
});
}Troubleshooting
| Issue | Check |
|---|---|
| No history in onMessage | Confirm saveHistory: true was used when messages were sent |
| Empty array | The thread may have no persisted messages yet |
| Not connected | Call after connect() succeeds |
| Duplicate messages | Deduplicate by metadata.id when merging history and live messages |
Dart / Flutter
Fetch and render chat history with fetchChatHistory.
Fetch on Thread Open
import 'package:dnotifier/dnotifier.dart';
const myUserId = 'user-alice';
const peerUserId = 'user-bob';
Future<void> main() async {
late final DNotifier notifier;
notifier = DNotifier(
appId: 'your-app-id',
secret: 'your-app-secret',
transport: 'ws',
userId: myUserId,
onConnected: () async {
await notifier.fetchChatHistory(
senderId: myUserId,
receiverIds: [peerUserId],
);
},
onMessage: (msg) {
final body = msg.payload.toJSON();
if (body is Map && body['messages'] is List) {
final list = body['messages'] as List;
print('Loaded ${list.length} history messages');
renderHistory(list);
return;
}
if (body is Map && body['type'] == 'text') {
appendLiveMessage(msg.metadata, body);
}
},
);
await notifier.connect();
}Merge History With Live Messages
final messageMap = <String, Map<String, dynamic>>{};
void upsertMessage(Map<String, dynamic> msg) {
final id =
msg['id']?.toString() ?? '${msg['sender']}_${msg['timestamp']}';
messageMap[id] = msg;
renderSorted(messageMap.values.toList());
}
onMessage: (msg) {
final body = msg.payload.toJSON();
if (body is Map && body['messages'] is List) {
for (final item in body['messages'] as List) {
if (item is Map) {
upsertMessage(Map<String, dynamic>.from(item));
}
}
return;
}
if (body is Map && body['type'] == 'text') {
upsertMessage({
'id': msg.metadata.id,
'sender': msg.metadata.sender,
'text': body['text'],
'timestamp': msg.metadata.timestamp,
});
}
},Refetch After Reconnect
Future<void> onReconnect(DNotifier notifier) async {
await notifier.connect();
await notifier.fetchChatHistory(
senderId: myUserId,
receiverIds: [peerUserId],
);
}Troubleshooting
| Issue | Check |
|---|---|
| No history in onMessage | Confirm saveHistory: true when messages were sent |
| Empty list | The thread may have no persisted messages yet |
| StateError: Not connected | Call after connect() succeeds |
| Duplicate messages | Deduplicate by metadata.id |
Python
Fetch and render chat history with fetch_chat_history.
Fetch on Thread Open
import asyncio
from dnotifier import DNotifier
MY_USER_ID = "user-alice"
PEER_USER_ID = "user-bob"
def on_message(msg):
body = msg.payload.to_json()
if isinstance(body, dict) and isinstance(body.get("messages"), list):
print(f"Loaded {len(body['messages'])} history messages")
render_history(body["messages"])
return
if isinstance(body, dict) and body.get("type") == "text":
append_live_message(msg.metadata, body)
async def main():
notifier = DNotifier(
app_id="your-app-id",
secret="your-app-secret",
transport="ws",
user_id=MY_USER_ID,
on_connected=lambda: asyncio.create_task(
notifier.fetch_chat_history(
sender_id=MY_USER_ID,
receiver_ids=[PEER_USER_ID],
)
),
on_message=on_message,
)
await notifier.connect()
await asyncio.sleep(3600)
asyncio.run(main())Merge History With Live Messages
message_map: dict[str, dict] = {}
def upsert_message(msg: dict):
msg_id = str(
msg.get("id") or f"{msg.get('sender')}_{msg.get('timestamp')}"
)
message_map[msg_id] = msg
render_sorted(list(message_map.values()))
def on_message(msg):
body = msg.payload.to_json()
if isinstance(body, dict) and isinstance(body.get("messages"), list):
for item in body["messages"]:
if isinstance(item, dict):
upsert_message(item)
return
if isinstance(body, dict) and body.get("type") == "text":
upsert_message({
"id": msg.metadata.id,
"sender": msg.metadata.sender,
"text": body.get("text"),
"timestamp": msg.metadata.timestamp,
})Refetch After Reconnect
async def on_reconnect(notifier: DNotifier):
await notifier.connect()
await notifier.fetch_chat_history(
sender_id=MY_USER_ID,
receiver_ids=[PEER_USER_ID],
)Troubleshooting
| Issue | Check |
|---|---|
| No history in on_message | Confirm save_history=True when messages were sent |
| Empty list | The thread may have no persisted messages yet |
| RuntimeError: Not connected | Call after connect() succeeds |
| Duplicate messages | Deduplicate by metadata.id |