183 lines
5.0 KiB
Markdown
183 lines
5.0 KiB
Markdown
---
|
|
title: Implement Realtime Subscriptions Correctly
|
|
impact: MEDIUM
|
|
impactDescription: Live updates without polling, reduced server load
|
|
tags: realtime, subscriptions, sse, websocket
|
|
---
|
|
|
|
## Implement Realtime Subscriptions Correctly
|
|
|
|
PocketBase uses Server-Sent Events (SSE) for realtime updates. Proper subscription management prevents memory leaks and ensures reliable event delivery.
|
|
|
|
**Incorrect (memory leaks and poor management):**
|
|
|
|
```javascript
|
|
// Missing unsubscribe - memory leak!
|
|
function PostList() {
|
|
useEffect(() => {
|
|
pb.collection('posts').subscribe('*', (e) => {
|
|
updatePosts(e);
|
|
});
|
|
// No cleanup - subscription persists forever!
|
|
}, []);
|
|
}
|
|
|
|
// Subscribing multiple times
|
|
function loadPosts() {
|
|
// Called on every render - creates duplicate subscriptions!
|
|
pb.collection('posts').subscribe('*', handleChange);
|
|
}
|
|
|
|
// Not handling reconnection
|
|
pb.collection('posts').subscribe('*', (e) => {
|
|
// Assumes connection is always stable
|
|
updateUI(e);
|
|
});
|
|
```
|
|
|
|
**Correct (proper subscription management):**
|
|
|
|
```javascript
|
|
// React example with cleanup
|
|
function PostList() {
|
|
const [posts, setPosts] = useState([]);
|
|
|
|
useEffect(() => {
|
|
// Initial load
|
|
loadPosts();
|
|
|
|
// Subscribe to changes
|
|
const unsubscribe = pb.collection('posts').subscribe('*', (e) => {
|
|
if (e.action === 'create') {
|
|
setPosts(prev => [e.record, ...prev]);
|
|
} else if (e.action === 'update') {
|
|
setPosts(prev => prev.map(p =>
|
|
p.id === e.record.id ? e.record : p
|
|
));
|
|
} else if (e.action === 'delete') {
|
|
setPosts(prev => prev.filter(p => p.id !== e.record.id));
|
|
}
|
|
});
|
|
|
|
// Cleanup on unmount
|
|
return () => {
|
|
unsubscribe();
|
|
};
|
|
}, []);
|
|
|
|
async function loadPosts() {
|
|
const result = await pb.collection('posts').getList(1, 50);
|
|
setPosts(result.items);
|
|
}
|
|
|
|
return <PostListUI posts={posts} />;
|
|
}
|
|
|
|
// Subscribe to specific record
|
|
async function watchPost(postId) {
|
|
return pb.collection('posts').subscribe(postId, (e) => {
|
|
console.log('Post changed:', e.action, e.record);
|
|
});
|
|
}
|
|
|
|
// Subscribe to collection changes
|
|
async function watchAllPosts() {
|
|
return pb.collection('posts').subscribe('*', (e) => {
|
|
console.log(`Post ${e.action}:`, e.record.title);
|
|
});
|
|
}
|
|
|
|
// Handle connection events
|
|
pb.realtime.subscribe('PB_CONNECT', (e) => {
|
|
console.log('Realtime connected, client ID:', e.clientId);
|
|
// Re-sync data after reconnection
|
|
refreshData();
|
|
});
|
|
|
|
// Vanilla JS with proper cleanup
|
|
class PostManager {
|
|
unsubscribes = [];
|
|
|
|
async init() {
|
|
this.unsubscribes.push(
|
|
await pb.collection('posts').subscribe('*', this.handlePostChange)
|
|
);
|
|
this.unsubscribes.push(
|
|
await pb.collection('comments').subscribe('*', this.handleCommentChange)
|
|
);
|
|
}
|
|
|
|
destroy() {
|
|
this.unsubscribes.forEach(unsub => unsub());
|
|
this.unsubscribes = [];
|
|
}
|
|
|
|
handlePostChange = (e) => { /* ... */ };
|
|
handleCommentChange = (e) => { /* ... */ };
|
|
}
|
|
```
|
|
|
|
**Subscription event structure:**
|
|
|
|
```javascript
|
|
pb.collection('posts').subscribe('*', (event) => {
|
|
event.action; // 'create' | 'update' | 'delete'
|
|
event.record; // The affected record
|
|
});
|
|
|
|
// Full event type
|
|
interface RealtimeEvent {
|
|
action: 'create' | 'update' | 'delete';
|
|
record: RecordModel;
|
|
}
|
|
```
|
|
|
|
**Unsubscribe patterns:**
|
|
|
|
```javascript
|
|
// Unsubscribe from specific callback
|
|
const unsub = await pb.collection('posts').subscribe('*', callback);
|
|
unsub(); // Remove this specific subscription
|
|
|
|
// Unsubscribe from all subscriptions on a topic
|
|
pb.collection('posts').unsubscribe('*'); // All collection subs
|
|
pb.collection('posts').unsubscribe('RECORD_ID'); // Specific record
|
|
|
|
// Unsubscribe from all collection subscriptions
|
|
pb.collection('posts').unsubscribe();
|
|
|
|
// Unsubscribe from everything
|
|
pb.realtime.unsubscribe();
|
|
```
|
|
|
|
**Performance considerations:**
|
|
|
|
```javascript
|
|
// Prefer specific record subscriptions over collection-wide when possible.
|
|
// subscribe('*') checks ListRule for every connected client on each change.
|
|
// subscribe(recordId) checks ViewRule -- fewer records to evaluate.
|
|
|
|
// For high-traffic collections, subscribe to specific records:
|
|
await pb.collection('orders').subscribe(orderId, handleOrderUpdate);
|
|
// Instead of: pb.collection('orders').subscribe('*', handleAllOrders);
|
|
|
|
// Use subscription options to reduce payload size (SDK v0.21+):
|
|
await pb.collection('posts').subscribe('*', handleChange, {
|
|
fields: 'id,title,updated', // Only receive specific fields
|
|
expand: 'author', // Include expanded relations
|
|
filter: 'status = "published"' // Only receive matching records
|
|
});
|
|
```
|
|
|
|
**Subscription scope guidelines:**
|
|
|
|
| Scenario | Recommended Scope |
|
|
|----------|-------------------|
|
|
| Watching a specific document | `subscribe(recordId)` |
|
|
| Chat room messages | `subscribe('*')` with filter for room |
|
|
| User notifications | `subscribe('*')` with filter for user |
|
|
| Admin dashboard | `subscribe('*')` (need to see all) |
|
|
| High-frequency data (IoT) | `subscribe(recordId)` per device |
|
|
|
|
Reference: [PocketBase Realtime](https://pocketbase.io/docs/api-realtime/)
|