SDK ReferenceCore
Message History API
Fetch and paginate through historical messages
Message History API
The Message History API allows you to fetch past messages from any topic with cursor-based pagination. Perfect for chat applications, message recovery, and catch-up scenarios.
Overview
Messages are stored for 3 days and can be retrieved using:
- Cursor-based pagination - Efficient pagination without offset counting
- Bidirectional navigation - Forward (oldest→newest) or backward (newest→oldest)
- ULID sequences - Lexicographically sortable timestamps
Basic Usage
Fetch Latest Messages
const history = await client.getHistory("chat:lobby");
console.log(`Fetched ${history.items.length} messages`);
console.log(`Next cursor: ${history.nextCursor}`);
history.items.forEach(msg => {
console.log(`[${msg.seq}] ${msg.senderId}: ${msg.payload}`);
});
Pagination
// First page
const page1 = await client.getHistory("chat:lobby", {
limit: 50,
direction: "backward"
});
// Second page using cursor
if (page1.nextCursor) {
const page2 = await client.getHistory("chat:lobby", {
cursor: page1.nextCursor,
limit: 50,
direction: "backward"
});
}
API Reference
getHistory(topic, options?)
Fetch historical messages for a topic.
Parameters:
topic: string
- Topic to fetch history foroptions?: HistoryOptions
cursor?: string
- ULID cursor for pagination (omit for first page)limit?: number
- Messages per page (1-1000, default: 50)direction?: "forward" | "backward"
- Sort order (default: "backward")
Returns: Promise<HistoryResponse>
interface HistoryResponse {
items: MessageBody[]; // Array of messages
nextCursor: string | null; // Cursor for next page (null = no more)
}
Direction Modes
Backward (Newest → Oldest)
Default mode. Returns newest messages first.
const history = await client.getHistory("chat:lobby", {
direction: "backward",
limit: 50
});
// Most recent message
const latest = history.items[0];
console.log("Latest:", latest.payload);
Use cases:
- Loading recent chat history
- "Load more" button in chat UI
- Displaying latest activity
Forward (Oldest → Newest)
Returns oldest messages first.
const history = await client.getHistory("chat:lobby", {
direction: "forward",
limit: 50
});
// Oldest message
const oldest = history.items[0];
console.log("Oldest:", oldest.payload);
Use cases:
- Catch-up after reconnection
- Chronological message replay
- Data export/archival
Pagination Patterns
1. Load More Button
let cursor: string | null = null;
async function loadMore() {
const history = await client.getHistory("chat:lobby", {
cursor: cursor || undefined,
limit: 20,
direction: "backward"
});
// Append to UI
history.items.forEach(msg => appendMessage(msg));
// Update cursor for next load
cursor = history.nextCursor;
// Hide button if no more messages
if (!cursor) {
hideLoadMoreButton();
}
}
2. Infinite Scroll
let cursor: string | null = null;
let loading = false;
window.addEventListener("scroll", async () => {
if (loading || !cursor) return;
const scrolledToBottom =
window.scrollY + window.innerHeight >= document.body.scrollHeight - 100;
if (scrolledToBottom) {
loading = true;
const history = await client.getHistory("chat:lobby", {
cursor,
limit: 20
});
history.items.forEach(msg => appendMessage(msg));
cursor = history.nextCursor;
loading = false;
}
});
3. Full History Export
async function exportAllMessages(topic: string) {
const allMessages: MessageBody[] = [];
let cursor: string | null = null;
do {
const history = await client.getHistory(topic, {
cursor: cursor || undefined,
limit: 1000, // Max per request
direction: "forward"
});
allMessages.push(...history.items);
cursor = history.nextCursor;
console.log(`Exported ${allMessages.length} messages...`);
} while (cursor);
return allMessages;
}
// Usage
const messages = await exportAllMessages("chat:lobby");
console.log(`Total: ${messages.length} messages`);
Iterator Helper
Use createHistoryIterator()
for cleaner pagination code:
Basic Iterator
const getNext = client.createHistoryIterator("chat:lobby", {
limit: 50,
direction: "backward"
});
// Fetch pages on demand
const page1 = await getNext(); // { items: [...], hasMore: true }
const page2 = await getNext(); // { items: [...], hasMore: false }
const done = await getNext(); // null (exhausted)
Load More with Iterator
const getNext = client.createHistoryIterator("chat:lobby", { limit: 20 });
async function loadMore() {
const batch = await getNext();
if (!batch) {
console.log("No more messages");
return;
}
batch.items.forEach(msg => appendMessage(msg));
if (batch.hasMore) {
showLoadMoreButton();
} else {
hideLoadMoreButton();
}
}
Stream All Messages
async function streamHistory(topic: string, onBatch: (msgs: MessageBody[]) => void) {
const getNext = client.createHistoryIterator(topic, {
limit: 100,
direction: "forward"
});
let batch;
while ((batch = await getNext())) {
onBatch(batch.items);
// Optional: delay between batches
await new Promise(r => setTimeout(r, 100));
}
}
// Usage
await streamHistory("chat:lobby", (messages) => {
console.log(`Processing ${messages.length} messages`);
messages.forEach(processMessage);
});
Advanced Patterns
Catch-Up After Disconnect
// Save last seen sequence before disconnect
let lastSeenSeq: string | null = localStorage.getItem("lastSeq");
async function catchUp(topic: string) {
if (!lastSeenSeq) {
console.log("No catch-up needed");
return;
}
const missed = await client.getHistory(topic, {
cursor: lastSeenSeq,
direction: "forward",
limit: 1000
});
console.log(`Caught up on ${missed.items.length} missed messages`);
// Process missed messages
missed.items.forEach(msg => processMessage(msg, { isCatchUp: true }));
// Update last seen
if (missed.items.length > 0) {
lastSeenSeq = missed.items[missed.items.length - 1].seq;
localStorage.setItem("lastSeq", lastSeenSeq);
}
}
// On reconnect
client.on("reconnect", async () => {
await catchUp("chat:lobby");
await client.subscribe("chat:lobby", (msg) => {
processMessage(msg);
lastSeenSeq = msg.seq;
localStorage.setItem("lastSeq", msg.seq);
});
});
Search in History
async function searchMessages(topic: string, query: string): Promise<MessageBody[]> {
const results: MessageBody[] = [];
const getNext = client.createHistoryIterator(topic, { limit: 100 });
let batch;
while ((batch = await getNext())) {
const matches = batch.items.filter(msg =>
msg.payload.toLowerCase().includes(query.toLowerCase())
);
results.push(...matches);
// Stop if we found enough
if (results.length >= 50) {
break;
}
}
return results;
}
// Usage
const results = await searchMessages("chat:lobby", "hello");
console.log(`Found ${results.length} messages containing "hello"`);
Load Context Around Message
async function loadMessageContext(topic: string, messageSeq: string) {
// Get messages before
const before = await client.getHistory(topic, {
cursor: messageSeq,
limit: 10,
direction: "backward"
});
// Get messages after
const after = await client.getHistory(topic, {
cursor: messageSeq,
limit: 10,
direction: "forward"
});
return {
before: before.items,
after: after.items
};
}
// Usage - jump to specific message with context
const context = await loadMessageContext("chat:lobby", "01JBCD1234567890ABCDEFGHIJ");
displayMessages([...context.before, ...context.after]);
Time-Based Queries (Using ULID)
import { ulid } from "ulid";
// Get messages from specific time
function getMessagesFromTime(topic: string, timestamp: Date) {
// Generate ULID from timestamp
const cursor = ulid(timestamp.getTime());
return client.getHistory(topic, {
cursor,
direction: "forward",
limit: 100
});
}
// Get last 24 hours of messages
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
const recent = await getMessagesFromTime("chat:lobby", yesterday);
console.log(`Messages in last 24h: ${recent.items.length}`);
Performance Tips
1. Adjust Page Size
- Smaller pages (20-50): Better for incremental loading, lower latency
- Larger pages (100-1000): Better for bulk operations, fewer requests
// Mobile: smaller pages for faster initial load
const mobile = await client.getHistory(topic, { limit: 20 });
// Desktop: larger pages for efficiency
const desktop = await client.getHistory(topic, { limit: 100 });
// Export: maximum pages
const export = await client.getHistory(topic, { limit: 1000 });
2. Cache Results
const historyCache = new Map<string, HistoryResponse>();
async function getCachedHistory(topic: string, cursor?: string) {
const cacheKey = `${topic}:${cursor || "initial"}`;
if (historyCache.has(cacheKey)) {
return historyCache.get(cacheKey)!;
}
const history = await client.getHistory(topic, { cursor });
historyCache.set(cacheKey, history);
return history;
}
3. Parallel Fetching
// Fetch multiple topics in parallel
const [chat1, chat2, chat3] = await Promise.all([
client.getHistory("chat:lobby", { limit: 20 }),
client.getHistory("chat:support", { limit: 20 }),
client.getHistory("chat:general", { limit: 20 })
]);
Error Handling
try {
const history = await client.getHistory("chat:lobby");
} catch (error) {
if (error.message.includes("HTTP base URL required")) {
console.error("History API not configured");
} else if (error.message.includes("Channel must be set")) {
console.error("Call joinChannel() first");
} else if (error.message.includes("History API failed")) {
console.error("Server error:", error);
}
}
Typed Schema Facade History
When using the Schema Facade, history methods provide automatic type validation:
Typed getHistory
import { ErebusPubSubSchemas } from "@erebus-sh/sdk/client";
import { z } from "zod";
const schemas = {
chat: z.object({
text: z.string(),
username: z.string(),
timestamp: z.number(),
}),
};
const typed = new ErebusPubSubSchemas(baseClient, schemas);
// Fetch typed history
const history = await typed.getHistory("chat", "lobby", {
limit: 50,
direction: "backward"
});
// TypeScript knows the payload type!
history.items.forEach(msg => {
console.log(`${msg.payload.username}: ${msg.payload.text}`); // ✅ Fully typed
});
Typed History Iterator
const getNext = typed.createHistoryIterator("chat", "lobby", {
limit: 20,
direction: "backward"
});
const batch = await getNext();
if (batch) {
batch.items.forEach(msg => {
// Payload is validated and typed
console.log(`[${msg.payload.username}] ${msg.payload.text}`);
});
console.log(`Has more: ${batch.hasMore}`);
}
Schema Validation Benefits
- Runtime validation: Invalid messages are rejected automatically
- Type safety: TypeScript ensures correct payload access
- Consistent API: Same pagination patterns as core client
// Validation happens automatically
try {
const history = await typed.getHistory("chat", "room-1");
// All items guaranteed to have valid chat payloads
} catch (error) {
console.error("History fetch or validation failed:", error);
}
Limitations
- Retention: Messages are stored for 3 days
- Max per topic: Up to 100 messages buffered per topic
- Rate limits: Standard API rate limits apply
- No search: Use client-side search or export to database
TypeScript Types
interface HistoryOptions {
cursor?: string;
limit?: number;
direction?: "forward" | "backward";
}
interface HistoryResponse {
items: MessageBody[];
nextCursor: string | null;
}
interface MessageBody {
id: string;
topic: string;
senderId: string;
seq: string;
sentAt: Date;
payload: string;
clientMsgId?: string;
clientPublishTs?: number;
}
Next Steps
- Learn about Typed Schema Facade
- See Complete API Reference
- Check out Example Apps