Get started with PAN (Page Area Network) in 5 minutes.
PAN is a DOM-native message bus for building loosely-coupled web applications. Components communicate via topics instead of direct imports, making your code more modular and testable.
Key Features:
<!DOCTYPE html>
<html>
<head>
<title>My PAN App</title>
</head>
<body>
<!-- 1. Add the bus element -->
<pan-bus></pan-bus>
<!-- 2. Include autoloader -->
<script type="module" src="./pan/core/pan-autoload.mjs"></script>
<!-- 3. Your app code -->
<script type="module">
import { PanClient } from './pan/core/pan-client.mjs';
const client = new PanClient();
await client.ready();
// Start using PAN!
client.publish({
topic: 'app.started',
data: { timestamp: Date.now() }
});
</script>
</body>
</html>
<pan-bus></pan-bus>
<script type="module" src="https://unpkg.com/@larc/pan-bus@1.0.0"></script>
<script type="module">
import { PanClient } from 'https://unpkg.com/@larc/pan-client@1.0.0';
// ...
</script>
Let’s build a simple counter that shows PAN’s core concepts.
<!DOCTYPE html>
<html>
<head>
<title>Counter App</title>
</head>
<body>
<pan-bus></pan-bus>
<div id="app">
<h1>Counter: <span id="count">0</span></h1>
<button id="increment">+1</button>
<button id="decrement">-1</button>
<button id="reset">Reset</button>
</div>
<script type="module" src="./pan/core/pan-autoload.mjs"></script>
<script type="module" src="./app.js"></script>
</body>
</html>
import { PanClient } from './pan/core/pan-client.mjs';
// Create client and wait for bus
const client = new PanClient();
await client.ready();
// Current count
let count = 0;
// Publish count state
function publishCount() {
client.publish({
topic: 'counter.state',
data: { count },
retain: true // Late subscribers get current value
});
}
// Button handlers
document.getElementById('increment').addEventListener('click', () => {
count++;
publishCount();
});
document.getElementById('decrement').addEventListener('click', () => {
count--;
publishCount();
});
document.getElementById('reset').addEventListener('click', () => {
count = 0;
publishCount();
});
// Subscribe to count changes
client.subscribe('counter.state', (msg) => {
document.getElementById('count').textContent = msg.data.count;
}, { retained: true }); // Get current count immediately
// Publish initial state
publishCount();
Open index.html in a browser. Click buttons to see the counter update!
What’s happening:
retain: true means new subscribers get the current countclient.publish({
topic: 'users.updated',
data: { id: 123, name: 'Alice' }
});
client.subscribe('users.updated', (msg) => {
console.log('User updated:', msg.data);
});
// Match all user-related topics
client.subscribe('users.*', (msg) => {
console.log('User event:', msg.topic);
});
// Make a request
const response = await client.request('users.get', { id: 123 });
console.log('User:', response.data);
// Respond to requests
client.subscribe('users.get', (msg) => {
if (!msg.replyTo) return;
const user = getUserFromDatabase(msg.data.id);
client.publish({
topic: msg.replyTo,
data: { ok: true, user },
correlationId: msg.correlationId
});
});
// Publish retained state
client.publish({
topic: 'app.theme',
data: { mode: 'dark' },
retain: true
});
// New subscribers get current value
client.subscribe('app.theme', (msg) => {
applyTheme(msg.data.mode);
}, { retained: true });
// Publish state
client.publish({
topic: 'users.list.state',
data: { users: [user1, user2, user3] },
retain: true
});
// Subscribe to state
client.subscribe('users.list.state', (msg) => {
renderUserList(msg.data.users);
}, { retained: true });
// Create
const response = await client.request('users.item.save', {
item: { name: 'Alice', email: 'alice@example.com' }
});
// Read
const user = await client.request('users.item.get', { id: 123 });
// Update
await client.request('users.item.save', {
item: { id: 123, name: 'Alice Updated' }
});
// Delete
await client.request('users.item.delete', { id: 123 });
// Publish event
client.publish({
topic: 'users.item.created',
data: { item: newUser }
});
// Multiple subscribers
client.subscribe('users.item.created', (msg) => {
console.log('User created:', msg.data.item);
});
client.subscribe('users.item.created', (msg) => {
sendWelcomeEmail(msg.data.item);
});
// Publish command
client.publish({
topic: 'nav.goto',
data: { route: '/users/123' }
});
// Handle command
client.subscribe('nav.goto', (msg) => {
router.navigateTo(msg.data.route);
});
Here’s how to use PAN with Web Components:
class UserCard extends HTMLElement {
connectedCallback() {
this.client = new PanClient(this);
// Subscribe to user data
const userId = this.getAttribute('user-id');
this.unsub = this.client.subscribe(`users.item.state.${userId}`, (msg) => {
this.render(msg.data);
}, { retained: true });
// Request initial data
this.loadUser(userId);
}
disconnectedCallback() {
// Clean up subscription
if (this.unsub) this.unsub();
}
async loadUser(userId) {
const response = await this.client.request('users.item.get', { id: userId });
if (response.data.ok) {
this.render(response.data.item);
}
}
render(user) {
this.innerHTML = `
<div class="user-card">
<h3>${user.name}</h3>
<p>${user.email}</p>
</div>
`;
}
}
customElements.define('user-card', UserCard);
Usage:
<user-card user-id="123"></user-card>
Wait for ready:
const client = new PanClient();
await client.ready();
// Now safe to publish
Use retained for state:
client.publish({
topic: 'app.state',
data: { ... },
retain: true
});
Clean up subscriptions:
const unsub = client.subscribe('topic', handler);
// Later:
unsub();
Use specific topics:
client.subscribe('users.updated', handler); // Good
Don’t publish before ready:
const client = new PanClient();
client.publish({ ... }); // May be lost!
Don’t subscribe to everything:
client.subscribe('*', handler); // Too broad!
Don’t forget to unsubscribe:
// Memory leak!
client.subscribe('topic', handler);
// Component removed but subscription remains
Try building these to practice:
examples/ directorytests/ directory for usage patternsProblem: Published messages aren’t being received
Solutions:
// 1. Wait for bus to be ready
await client.ready();
// 2. Check topic names match exactly
client.publish({ topic: 'users.updated', data: {} });
client.subscribe('users.updated', handler); // Must match
// 3. Use wildcard for debugging
client.subscribe('*', (msg) => {
console.log('All messages:', msg.topic);
});
Problem: Memory leaks from subscriptions
Solution:
// Store unsubscribe function
this.unsubs = [];
// Add subscriptions
this.unsubs.push(client.subscribe('topic1', handler1));
this.unsubs.push(client.subscribe('topic2', handler2));
// Clean up all
disconnectedCallback() {
this.unsubs.forEach(unsub => unsub());
}
// OR use AbortSignal
const controller = new AbortController();
client.subscribe('topic', handler, { signal: controller.signal });
controller.abort(); // Unsubscribe all
Problem: Requests timing out
Solutions:
// 1. Increase timeout
const response = await client.request('topic', data, {
timeoutMs: 10000 // 10 seconds
});
// 2. Check responder is subscribed
client.subscribe('topic', (msg) => {
if (!msg.replyTo) return; // Must check this!
// ... send reply
});
// 3. Handle timeout gracefully
try {
const response = await client.request('topic', data);
} catch (err) {
console.error('Request failed:', err);
// Fallback behavior
}
Ready to build? Start with the API Reference for complete details!
Last Updated: November 2024