Skip to content
/ core Public

LARC Core - Lightweight Asynchronous Relay Core: The PAN (Page Area Network) messaging bus implementation

License

Notifications You must be signed in to change notification settings

larcjs/core

Repository files navigation

LARC Core

Version License Status Tests npm

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.

Features

  • πŸš€ 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

Why PAN Messaging?

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.

Quick Start

Installation

npm install @larcjs/core

CDN Usage (No Build Required)

<!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>

Module Usage

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);

Core Components

<pan-bus>

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)

<pan-client>

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>

Message Patterns

Publish/Subscribe

// Publisher
bus.publish('notifications.new', {
  type: 'info',
  message: 'Welcome!'
});

// Subscriber
bus.subscribe('notifications.new', (msg) => {
  showNotification(msg.payload);
});

Request/Reply

// 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);
}

Retained Messages (State)

// 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);
});

Topic Conventions

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 }
});

Dynamic Message Routing πŸ”₯

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.

Why Routing Changes Everything

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' }
  ]
});

Enable Routing

<pan-bus enable-routing="true"></pan-bus>
// Access routing manager
const routes = window.pan.routes;

Powerful Matching

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}}'
    }
  ]
});

Transform Messages

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'] }
  ]
});

Multiple Actions

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' }
  ]
});

Runtime Configuration

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
// }

Real-World Use Cases

πŸ” 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'] }]
});

Why This Matters

  • 🎯 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

Learn More

See Dynamic Routing Documentation for complete API reference, predicates, transforms, actions, and advanced examples.

Cross-Tab Communication

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.');
});

TypeScript Support

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-types

Full 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!

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Application                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Component  β”‚  Component  β”‚     Component       β”‚
β”‚      A      β”‚      B      β”‚         C           β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚             β”‚             β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
              β”‚  <pan-bus>  β”‚  ← Central Message Hub
              β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚             β”‚             β”‚
β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
β”‚   Worker    β”‚ β”‚ iframe β”‚ β”‚ Other Tabs  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Use With Your Framework

PAN complements React/Vue/Angular - it doesn't replace them.

React Example

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>
  );
}

Vue Example

<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%+.

Related Packages

Documentation

Browser Support

  • βœ… Chrome/Edge 90+
  • βœ… Firefox 88+
  • βœ… Safari 14+
  • βœ… Opera 76+

Performance

  • Throughput: 300,000+ messages/second
  • Latency: <1ms per message (local)
  • Memory: Zero leaks, constant memory usage
  • Bundle size: ~12KB minified

Contributing

Contributions are welcome! Please see our Contributing Guide.

License

MIT Β© Chris Robison

Support