Real-Time Streaming
Concepts
Periscope provides real-time context streaming over WebSocket connections. Rather than polling the REST API for changes, agents and applications can subscribe to channels that deliver events as they happen -- typically within milliseconds of the browser activity that triggered them. This page covers the channel system, authentication handshake, subscription lifecycle, heartbeats, multi-device propagation, and reconnection strategy.
Channels
Periscope organizes real-time events into four channels. Each channel delivers a different slice of context data:
user-contexts
Delivers all context changes for the authenticated user. Every time a BrowserContext is created, updated, or deleted, a corresponding event appears on this channel. This is the most comprehensive channel -- if you only subscribe to one channel, this is the one.
Use this when your agent needs a complete picture of the user's browsing activity across all tabs and windows.
current-context
Delivers updates only for the user's currently active context. When the user switches tabs, this channel begins delivering events from the new active tab and stops delivering events from the previous one. This is a filtered view of user-contexts that always reflects "what is the user looking at right now?"
Use this when your agent only cares about the user's immediate focus. This significantly reduces message volume compared to user-contexts because background tab activity is excluded.
tab-events
Delivers tab lifecycle events: tab activated, tab navigated, tab closed. Does not deliver context content -- only structural changes to the tab state. This is useful for agents that need to track which tabs are open and which is active, without receiving full context data.
global-events
Delivers system-wide events: maintenance announcements, configuration changes, service status updates. These events are not user-specific and are broadcast to all connected clients regardless of authentication. This channel is low-volume and primarily used for operational coordination.
Connection Lifecycle
A WebSocket connection goes through a specific sequence of messages to become fully operational:
1. Connect
The client opens a WebSocket connection to the Periscope service. The server responds with a welcome message containing a unique connectionId. No authentication is required at this point.
2. Authenticate
The client sends an auth message with a bearer token and user ID. The server validates the token and associates the connection with the user. Until authentication succeeds, the only messages the server will accept are auth and heartbeat. Subscription and broadcast requests are rejected with an unauthorized error.
3. Subscribe
After authentication, the client sends a subscribe message specifying which channels to join. The server confirms with a subscription-confirmed message listing the channels that were successfully subscribed.
// Client sends:
{
type: "subscribe",
channels: ["user-contexts", "tab-events"],
requestId: "req_001"
}
// Server responds:
{
type: "subscription-confirmed",
channels: ["user-contexts", "tab-events"],
requestId: "req_001",
subscribedAt: "2024-01-15T14:32:18.000Z"
}
4. Receive Events
Once subscribed, events flow automatically. Each event arrives as a state-update message:
{
type: "state-update",
channel: "user-contexts",
event: {
id: "evt_abc123",
timestamp: "2024-01-15T14:32:19.000Z",
userId: "user_456",
channelId: "user:456",
source: { /* ... */ },
payload: {
kind: "text.selection",
data: { text: "selected text", range: { start: 0, end: 13 } }
}
},
publishedAt: "2024-01-15T14:32:19.050Z",
sequenceNumber: 42
}
The sequenceNumber is per-channel and monotonically increasing. If a client detects a gap in sequence numbers, it knows it missed events (likely during a reconnection) and can fetch the missing events via the REST API.
The difference between event.timestamp (when the event occurred in the browser) and publishedAt (when the server published it to the channel) represents the end-to-end latency.
5. Unsubscribe
The client can unsubscribe from specific channels at any time without closing the connection:
{
type: "unsubscribe",
channels: ["tab-events"],
requestId: "req_002"
}
Heartbeats
Both the client and server participate in heartbeat exchange to detect stale connections:
Client heartbeat interval: 25-30 seconds. The client sends a heartbeat message at this interval to indicate it is still alive.
Server heartbeat check: Every 30 seconds. The server checks when it last received a message (any message, not just heartbeats) from each connection.
Server timeout: 60 seconds. If the server has not received any message from a connection in 60 seconds, it considers the connection dead and cleans it up -- removing it from all channels and freeing resources.
The asymmetry (client sends every 25-30 seconds, server times out at 60 seconds) provides tolerance for network jitter and temporary delays without false positives.
Multi-Device Event Propagation
A single user might have Periscope running on multiple devices -- Chrome on a laptop, Firefox on a desktop, a mobile browser on a phone. Each device maintains its own WebSocket connection to the service.
When the user performs an action on one device, the resulting event is delivered to all of that user's connected WebSocket sessions via broadcastToUser. This means an agent running on the laptop can see events from the desktop browser in real time.
Channel broadcasting (broadcastToChannel) works differently -- it delivers to all subscribers of a channel regardless of user. This is used for global-events and for scenarios where multiple users share a channel (team contexts, collaborative sessions).
Connection Cleanup
When a WebSocket connection closes -- whether the client disconnected gracefully, the network dropped, or the server detected a timeout -- the server automatically:
- Removes the connection from all subscribed channels
- Updates channel subscriber counts
- Frees connection resources (authentication state, subscription tracking)
- Logs the disconnection with connection metadata
This cleanup is idempotent. If a connection is already removed (for example, the cleanup fires on both a close event and a timeout), the second cleanup is a no-op.
REST vs. WebSocket Decision Guide
Not everything needs to be real-time. Here is when to use each:
| Use Case | Recommended | Why |
|---|---|---|
| Agent needs current context | WebSocket (current-context) | Lowest latency, no polling overhead |
| Agent reacts to user actions | WebSocket (user-contexts) | Events arrive as they happen |
| Query historical contexts | REST API | Time-range queries, pagination, search |
| One-time context fetch | REST API | Single request, no connection overhead |
| Background analytics | REST API | Batch processing, no real-time need |
| Dashboard showing live activity | WebSocket (tab-events) | Immediate tab state updates |
| System health monitoring | WebSocket (global-events) | Maintenance and status updates |
| Building a context search UI | REST API | Complex filtering, full-text search |
As a general rule: if you need to know about changes as they happen, use WebSocket. If you need to query data that already exists, use REST.
Reconnection Strategy
Network connections fail. Periscope clients should implement a reconnection strategy with exponential backoff:
Attempt 1: wait 1 second
Attempt 2: wait 2 seconds
Attempt 3: wait 4 seconds
Attempt 4: wait 8 seconds
Attempt 5: wait 16 seconds
Attempt 6: wait 30 seconds (cap)
Attempt 7+: wait 30 seconds (cap)
Add random jitter (0-500ms) to each delay to prevent thundering herd when many clients reconnect simultaneously after a service restart.
On reconnection:
- Open a new WebSocket connection
- Re-authenticate with the same token
- Re-subscribe to all previously subscribed channels (track these client-side)
- Check the last received
sequenceNumberper channel - Fetch any missed events via REST API using the sequence gap
The critical detail is step 3: the client must track its subscriptions locally. The server does not remember subscriptions across connections. When you reconnect, you get a fresh connection with no subscriptions. Your client code must maintain a set of desired subscriptions and re-establish them after each reconnection.
// Pseudocode for reconnection
class PeriscopeClient {
private desiredChannels: Set<string> = new Set();
private lastSequence: Map<string, number> = new Map();
subscribe(channel: string) {
this.desiredChannels.add(channel);
this.sendSubscribe([channel]);
}
private async onReconnect() {
await this.authenticate(this.token);
// Re-subscribe to all channels
if (this.desiredChannels.size > 0) {
this.sendSubscribe([...this.desiredChannels]);
}
// Fetch missed events
for (const [channel, seq] of this.lastSequence) {
const missed = await this.fetchMissedEvents(channel, seq);
this.processMissedEvents(missed);
}
}
}
Message Types Reference
Client-to-Server Messages
| Type | Purpose | Requires Auth? |
|---|---|---|
subscribe | Join channel(s) | Yes |
unsubscribe | Leave channel(s) | Yes |
broadcast | Send event to channel subscribers | Yes |
heartbeat | Liveness signal | No |
Server-to-Client Messages
| Type | Purpose |
|---|---|
state-update | Delivers an event from a subscribed channel |
subscription-confirmed | Confirms channel subscription |
error | Reports an error (invalid message, unauthorized, rate limited) |
Error Codes
| Code | Meaning | Recovery |
|---|---|---|
invalid_message | Message failed validation | Fix message format |
unauthorized | Not authenticated or token expired | Re-authenticate |
channel_not_found | Requested channel does not exist | Check channel name |
rate_limited | Too many messages | Back off and retry |
Next Steps
- Event System -- The event types that flow through WebSocket channels
- Sync Protocol -- How the extension sends contexts that become channel events
- Browser Context Model -- The context data structure delivered through channels