Lightweight Asynchronous Relay Core β The PAN (Page Area Network) messaging bus implementation
LARC Core provides the foundational messaging infrastructure for building loosely-coupled, event-driven web applications. It implements the PAN (Page Area Network) protocol, enabling seamless communication between components, iframes, workers, and tabs.
- π Zero build required β Drop-in
<pan-bus>element, communicate via CustomEvents - π Loose coupling β Components depend on topic contracts, not imports
- π Framework friendly β Works with React, Vue, Angular - not as a replacement
- π¬ Rich messaging β Pub/sub, request/reply, retained messages, cross-tab mirroring
- π― Lightweight β ~5KB minified, no dependencies (vs 400-750KB for typical React stack)
- β‘ Performance β 300k+ messages/second, zero memory leaks
- π Security β Built-in message validation and sanitization
- π Dynamic Routing β Runtime-configurable message routing with transforms and actions
The Web Component "silo problem" solved.
Web Components give you encapsulation, but they're useless if they can't communicate. Without PAN, every component needs custom glue code:
// Without PAN - tightly coupled nightmare β
const search = document.querySelector('search-box');
const results = document.querySelector('results-list');
search.addEventListener('change', (e) => {
results.updateQuery(e.detail.query); // Tight coupling!
});With PAN - loosely coupled, reusable β
// Components just work together via topics
// No custom integration code needed!
<search-box></search-box> <!-- publishes "search:query" -->
<results-list></results-list> <!-- subscribes to "search:query" -->This is why Web Components haven't replaced frameworks - they lacked coordination. PAN fixes that.
npm install @larcjs/core<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the autoloader -->
<script type="module" src="https://unpkg.com/@larcjs/core@1.1.1/src/pan.js"></script>
</head>
<body>
<!-- The pan-bus is automatically created -->
<script>
// Publish a message
document.dispatchEvent(new CustomEvent('pan:publish', {
detail: {
topic: 'greeting.message',
payload: { text: 'Hello, PAN!' }
}
}));
// Subscribe to messages
document.addEventListener('pan:message', (e) => {
if (e.detail.topic === 'greeting.message') {
console.log('Received:', e.detail.payload.text);
}
});
</script>
</body>
</html>import { PanBus } from '@larcjs/core';
// Create a bus instance
const bus = new PanBus();
// Subscribe to a topic
bus.subscribe('user.login', (message) => {
console.log('User logged in:', message.payload);
});
// Publish a message
bus.publish('user.login', { userId: 123, name: 'Alice' });
// Request/reply pattern
const response = await bus.request('user.get', { id: 123 });
console.log('User data:', response);The central message hub that routes all PAN messages.
<pan-bus id="myBus" mirror="false"></pan-bus>Attributes:
mirrorβ Enable cross-tab message mirroring (default: false)debugβ Enable debug logging (default: false)
Simplifies publishing and subscribing for components.
<pan-client id="client"></pan-client>
<script>
const client = document.getElementById('client');
client.subscribe('data.changed', (msg) => {
console.log('Data updated:', msg.payload);
});
client.publish('data.request', { id: 42 });
</script>// Publisher
bus.publish('notifications.new', {
type: 'info',
message: 'Welcome!'
});
// Subscriber
bus.subscribe('notifications.new', (msg) => {
showNotification(msg.payload);
});// Responder
bus.subscribe('user.get', async (msg) => {
const user = await fetchUser(msg.payload.id);
return { ok: true, user };
});
// Requester
const result = await bus.request('user.get', { id: 123 });
if (result.ok) {
console.log('User:', result.user);
}// Publish with retain flag
bus.publish('app.state', { theme: 'dark' }, { retain: true });
// Late subscribers immediately receive the retained message
bus.subscribe('app.state', (msg) => {
applyTheme(msg.payload.theme);
});LARC uses hierarchical topic naming:
${resource}.list.getβ Request list of items${resource}.list.stateβ Current list state (retained)${resource}.item.selectβ User selected an item${resource}.item.getβ Request single item${resource}.item.saveβ Save an item${resource}.item.deleteβ Delete an item${resource}.changedβ Item(s) changed notification${resource}.errorβ Error occurred
Example:
// Request list of products
await bus.request('products.list.get', {});
// Subscribe to product selection
bus.subscribe('products.item.select', (msg) => {
loadProductDetails(msg.payload.id);
});
// Save a product
await bus.request('products.item.save', {
item: { id: 1, name: 'Widget', price: 9.99 }
});NEW: Configure message flows declaratively at runtime! PAN Routes lets you define routing rules that match, transform, and act on messages without hardcoding logic in your components.
Before (Tightly Coupled):
// Hardcoded message handling scattered across components β
bus.subscribe('sensor.temperature', (msg) => {
if (msg.payload.value > 30) {
const alert = { type: 'alert.highTemp', temp: msg.payload.value };
bus.publish('alerts', alert);
console.warn(`High temp: ${alert.temp}Β°C`);
}
});After (Declarative Routes):
// Define routing rules once, reuse everywhere β
pan.routes.add({
name: 'High Temperature Alert',
match: {
type: 'sensor.temperature',
where: { op: 'gt', path: 'payload.value', value: 30 }
},
actions: [
{ type: 'EMIT', message: { type: 'alert.highTemp' }, inherit: ['payload'] },
{ type: 'LOG', level: 'warn', template: 'High temp: {{payload.value}}Β°C' }
]
});<pan-bus enable-routing="true"></pan-bus>// Access routing manager
const routes = window.pan.routes;Match messages by type, topic, tags, or complex predicates:
// Match high-value VIP orders
routes.add({
name: 'VIP Order Priority',
match: {
type: 'order.created',
where: {
op: 'and',
children: [
{ op: 'gte', path: 'payload.total', value: 1000 },
{ op: 'eq', path: 'payload.customerTier', value: 'vip' }
]
}
},
actions: [
{
type: 'EMIT',
message: {
type: 'notification.vip-order',
payload: { priority: 'high', channels: ['email', 'sms'] }
}
},
{
type: 'LOG',
level: 'info',
template: 'π VIP Order: ${{payload.total}}'
}
]
});Pick fields, map values, or apply custom transformations:
// Register custom transform
routes.registerTransform('normalize-email', (email) => {
return email.toLowerCase().trim();
});
// Use in route
routes.add({
name: 'User Email Normalizer',
match: { type: 'user.register' },
transform: {
op: 'map',
path: 'payload.email',
fnId: 'normalize-email'
},
actions: [
{ type: 'EMIT', message: { type: 'user.normalized' }, inherit: ['payload'] }
]
});Chain actions for complex workflows:
routes.add({
name: 'Critical Error Pipeline',
match: {
type: 'error',
where: { op: 'eq', path: 'payload.severity', value: 'critical' }
},
actions: [
{ type: 'LOG', level: 'error', template: 'π¨ CRITICAL: {{payload.message}}' },
{ type: 'EMIT', message: { type: 'alert.critical' }, inherit: ['payload'] },
{ type: 'FORWARD', topic: 'error-tracking.events' },
{ type: 'CALL', handlerId: 'send-slack-alert' }
]
});Add, update, or remove routes on the fly:
// Add route
const route = routes.add({ /* route config */ });
// Update route
routes.update(route.id, { enabled: false });
// Remove route
routes.remove(route.id);
// List all routes
console.table(routes.list());
// Get stats
console.log(routes.getStats());
// {
// routesEvaluated: 1234,
// routesMatched: 456,
// actionsExecuted: 789
// }π Analytics Tracking
routes.add({
name: 'Track User Events',
match: { tagsAny: ['trackable'], type: ['user.click', 'user.view'] },
actions: [{ type: 'FORWARD', topic: 'analytics.events' }]
});π Smart Notifications
routes.add({
name: 'Notification Router',
match: { type: 'notification.send' },
actions: [
{ type: 'CALL', handlerId: 'check-user-preferences' },
{ type: 'EMIT', message: { type: 'notification.queued' } }
]
});π¦ Feature Flags
routes.registerHandler('feature-router', (msg) => {
const flags = getFeatureFlags();
const topic = flags.newUI ? 'ui.v2' : 'ui.v1';
window.pan.bus.publish(topic, msg.payload);
});
routes.add({
name: 'Feature Flag Router',
match: { type: 'app.init' },
actions: [{ type: 'CALL', handlerId: 'feature-router' }]
});π Data Enrichment
routes.registerTransform('enrich-user', async (msg) => {
const userData = await fetchUserProfile(msg.payload.userId);
return {
...msg,
payload: { ...msg.payload, user: userData }
};
});
routes.add({
name: 'Enrich Order Data',
match: { type: 'order.created' },
transform: { op: 'custom', fnId: 'enrich-user' },
actions: [{ type: 'EMIT', message: { type: 'order.enriched' }, inherit: ['payload'] }]
});- π― Separation of Concerns - Routing logic separate from business logic
- π Dynamic Reconfiguration - Change message flows without code changes
- π¦ Reusable Rules - Share and persist routing configurations
- π Debuggable - See all routing rules in one place
- π§ͺ Testable - Test routing rules in isolation
- β‘ Performant - Sub-millisecond message evaluation
See Dynamic Routing Documentation for complete API reference, predicates, transforms, actions, and advanced examples.
Enable the mirror attribute to sync messages across browser tabs:
<pan-bus mirror="true"></pan-bus>Only non-sensitive topics should be mirrored. Use topic filters:
bus.setMirrorFilter((topic) => {
// Don't mirror authentication tokens
return !topic.startsWith('auth.');
});LARC Core is written in pure JavaScript with zero build requirements. TypeScript support is available via the optional @larcjs/core-types package:
npm install @larcjs/core
npm install -D @larcjs/core-typesFull type definitions for all APIs:
import { PanClient } from '@larcjs/core/pan-client.mjs';
import type { PanMessage, SubscribeOptions } from '@larcjs/core-types';
interface UserData {
id: number;
name: string;
}
const client = new PanClient();
// Fully typed publish
client.publish<UserData>({
topic: 'user.updated',
data: { id: 123, name: 'Alice' }
});
// Fully typed subscribe
client.subscribe<UserData>('user.updated', (msg: PanMessage<UserData>) => {
console.log(msg.data.name); // TypeScript knows this is a string!
});Why separate types? We keep runtime code lean (zero dependencies) and let TypeScript users opt-in to types. Best of both worlds!
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Application β
βββββββββββββββ¬ββββββββββββββ¬ββββββββββββββββββββββ€
β Component β Component β Component β
β A β B β C β
ββββββββ¬βββββββ΄βββββββ¬βββββββ΄βββββββ¬βββββββββββββββ
β β β
βββββββββββββββΌββββββββββββββ
β
ββββββββΌβββββββ
β <pan-bus> β β Central Message Hub
ββββββββ¬βββββββ
β
βββββββββββββββΌββββββββββββββ
β β β
ββββββββΌβββββββ βββββΌβββββ ββββββββΌβββββββ
β Worker β β iframe β β Other Tabs β
βββββββββββββββ ββββββββββ βββββββββββββββ
PAN complements React/Vue/Angular - it doesn't replace them.
import { usePanSubscribe, usePanPublish } from '@larcjs/react-adapter';
function Dashboard() {
const theme = usePanSubscribe('theme:current');
const { publish } = usePanPublish();
return (
<div>
{/* React component */}
<button onClick={() => publish('theme:toggle')}>
Toggle Theme
</button>
{/* LARC components respond automatically */}
<pan-card theme={theme}>
<pan-data-table resource="users"></pan-data-table>
</pan-card>
</div>
);
}<script setup>
import { usePanSubscribe, usePanPublish } from '@larcjs/vue-adapter';
const theme = usePanSubscribe('theme:current');
const { publish } = usePanPublish();
</script>
<template>
<div>
<!-- Vue component -->
<button @click="publish('theme:toggle')">Toggle Theme</button>
<!-- LARC components respond automatically -->
<pan-card :theme="theme">
<pan-data-table resource="users"></pan-data-table>
</pan-card>
</div>
</template>Keep your framework for complex UIs. Use LARC for cards, modals, tables, navigation - reduce bundle size by 60%+.
- @larcjs/core-types β TypeScript type definitions (opt-in)
- @larcjs/components β UI components built on LARC Core
- @larcjs/react-adapter β React hooks for PAN messaging
- @larcjs/vue-adapter β Vue composables for PAN messaging
- @larcjs/devtools β Chrome DevTools for debugging PAN messages
- @larcjs/examples β Demo applications and examples
- β Chrome/Edge 90+
- β Firefox 88+
- β Safari 14+
- β Opera 76+
- Throughput: 300,000+ messages/second
- Latency: <1ms per message (local)
- Memory: Zero leaks, constant memory usage
- Bundle size: ~12KB minified
Contributions are welcome! Please see our Contributing Guide.
MIT Β© Chris Robison
- π Documentation
- π¬ Discussions
- π Issue Tracker