Home / books / backup / building-with-larc-original-20251226 / chapter-24-integration-components

Integration Components

In which we bridge the gap between LARC applications and the outside world—REST APIs, GraphQL servers, WebSocket streams, and Server-Sent Events—without losing our composure or our data

Every modern web application is, at heart, an integration problem. You're not building a standalone fortress; you're building a trading post that speaks multiple languages, accepts multiple currencies, and somehow keeps track of what goes in and what goes out. Your frontend needs to talk to REST APIs, subscribe to real-time WebSocket feeds, execute GraphQL queries, and listen to Server-Sent Event streams—often simultaneously.

LARC's integration components solve this problem by providing declarative, PAN-bus-connected adapters for external data sources. They transform HTTP requests, WebSocket events, and SSE streams into PAN messages, and PAN messages back into network requests. The result is a clean architectural boundary: your application components remain blissfully unaware of whether their data comes from REST, GraphQL, or a carrier pigeon.

This chapter provides comprehensive API documentation for four integration components:

  • pan-data-connector: REST API integration with full CRUD support
  • pan-graphql-connector: GraphQL query and mutation bridge
  • pan-websocket: Bidirectional WebSocket communication
  • pan-sse: Server-Sent Events streaming
Each section follows the same structure: overview, usage guidance, installation, attribute/method/event reference, complete examples, and troubleshooting. Think of this chapter as your field guide to connecting LARC applications to the wider internet ecosystem.

pan-data-connector

Overview

pan-data-connector is a declarative REST API bridge that maps PAN bus topics to HTTP endpoints. It implements the standard CRUD pattern—list, get, create, update, delete—using fetch() and publishes responses as retained PAN messages. This allows components to request data via topics without knowing anything about HTTP methods, URL construction, or response handling.

The connector listens for request topics like ${resource}.list.get and ${resource}.item.save, performs the appropriate HTTP request, and publishes state updates to ${resource}.list.state and ${resource}.item.state.${id}. All state messages are retained, so late-subscribing components receive the most recent data immediately.

When to Use

Use pan-data-connector when:
  • Working with RESTful APIs that follow standard CRUD patterns
  • You want declarative data fetching without writing fetch() calls in every component
  • You need automatic state synchronization across multiple components
  • You're building admin interfaces, CRUD applications, or data management tools
  • Your API uses predictable URL patterns (e.g., /api/users, /api/users/:id)
Don't use pan-data-connector when:
  • Your API doesn't follow REST conventions (use custom fetch() or build a specialized connector)
  • You need fine-grained control over request timing and caching
  • Your endpoints use non-standard HTTP methods or complex request patterns
  • You're working with GraphQL (use pan-graphql-connector instead)

Installation and Setup

Include the component module and add it to your HTML:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script type="module" src="/ui/pan-bus.mjs"></script>
  <script type="module" src="/ui/pan-data-connector.mjs"></script>
</head>
<body>
  <pan-bus></pan-bus>

  <!-- Simple configuration -->
  <pan-data-connector
    resource="users"
    base-url="https://api.example.com">
  </pan-data-connector>

  <!-- Your application -->
</body>
</html>

For APIs requiring authentication headers:

<pan-data-connector
  resource="users"
  base-url="https://api.example.com"
  credentials="include">
  <script type="application/json">
    {
      "headers": {
        "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
        "X-API-Version": "2023-01"
      }
    }
  </script>
</pan-data-connector>

Attributes

| Attribute | Type | Default | Description | |-----------|------|---------|-------------| | resource | String | "items" | Logical resource name. Used as the topic prefix (e.g., users creates topics like users.list.get). | | base-url | String | "" | Base URL for API endpoints. Trailing slashes are automatically removed. | | key | String | "id" | The field name used as the unique identifier for items. | | list-path | String | "/${resource}" | URL path template for list operations. Override for non-standard endpoints. | | item-path | String | "/${resource}/:id" | URL path template for single-item operations. The :id placeholder is replaced with the actual ID. | | update-method | String | "PUT" | HTTP method for updates. Use "PATCH" for partial updates. | | credentials | String | "" | Fetch credentials mode: "include", "same-origin", or "omit". |

Example configurations:
<!-- Non-standard paths -->
<pan-data-connector
  resource="products"
  base-url="https://shop.example.com"
  list-path="/v2/catalog/products"
  item-path="/v2/catalog/products/:id">
</pan-data-connector>

<!-- UUID-based API -->
<pan-data-connector
  resource="orders"
  base-url="/api"
  key="uuid"
  update-method="PATCH">
</pan-data-connector>

<!-- Complex authentication -->
<pan-data-connector resource="documents" base-url="/api/v1">
  <script type="application/json">
    {
      "headers": {
        "Authorization": "Bearer ${TOKEN}",
        "X-Tenant-ID": "acme-corp",
        "Accept": "application/vnd.api+json"
      }
    }
  </script>
</pan-data-connector>

Topics

The connector listens to and publishes messages on the following topics:

#### Subscribed Topics (Requests)

${resource}.list.get

Fetches the list of items. Query parameters can be passed in the message data.

Request payload:

{
  // Optional: any query parameters
  page: 1,
  limit: 20,
  filter: 'active'
}

${resource}.item.get

Fetches a single item by ID.

Request payload:

{
  id: 123
}
// Or simply: 123

${resource}.item.save

Creates a new item (if no ID) or updates an existing item.

Request payload:

{
  item: {
    id: 123,  // Optional; omit for creation
    name: "New Product",
    price: 29.99
  }
}
// Or simply: { id: 123, name: "...", price: 29.99 }

${resource}.item.delete

Deletes an item by ID.

Request payload:

{
  id: 123
}
// Or simply: 123

#### Published Topics (Responses)

${resource}.list.state (retained)

Published after successful list fetch. Contains the current list of items.

Payload:

{
  items: [
    { id: 1, name: "Product A", price: 19.99 },
    { id: 2, name: "Product B", price: 29.99 }
  ]
}

${resource}.item.state.${id} (retained)

Published after successful item fetch or save. Contains the current item state.

Payload:

{
  item: {
    id: 123,
    name: "Product C",
    price: 39.99,
    updatedAt: "2024-01-15T10:30:00Z"
  }
}

For deletions, a non-retained deletion notification is published:

{
  id: 123,
  deleted: true
}

#### Reply Topics

If the request includes replyTo and correlationId fields, the connector publishes a response to the reply topic:

Success response:

{
  ok: true,
  items: [...],  // For list operations
  item: {...}    // For item operations
}

Error response:

{
  ok: false,
  error: {
    status: 404,
    statusText: "Not Found",
    body: { message: "Item not found" }
  }
}

Authentication Integration

pan-data-connector automatically integrates with LARC's authentication system. It subscribes to auth.internal.state (retained) and automatically injects Authorization: Bearer ${token} headers when a token is available.

This means you can configure authentication once in pan-auth-provider, and all connectors automatically include credentials:

<pan-auth-provider
  storage="local"
  token-key="app_token">
</pan-auth-provider>

<!-- This connector will automatically use the auth token -->
<pan-data-connector
  resource="users"
  base-url="https://api.example.com">
</pan-data-connector>

Complete Examples

#### Basic CRUD Application

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script type="module" src="/ui/pan-bus.mjs"></script>
  <script type="module" src="/ui/pan-data-connector.mjs"></script>
</head>
<body>
  <pan-bus></pan-bus>

  <pan-data-connector
    resource="todos"
    base-url="/api">
  </pan-data-connector>

  <div id="app"></div>

  <script type="module">
    const bus = document.querySelector('pan-bus');

    // Subscribe to list state
    bus.subscribe('todos.list.state', (msg) => {
      const todos = msg.data.items;
      renderTodoList(todos);
    });

    // Fetch initial list
    bus.publish('todos.list.get', {});

    function renderTodoList(todos) {
      const app = document.getElementById('app');
      app.innerHTML = `
        <h1>Todo List</h1>
        <ul>
          ${todos.map(todo => `
            <li>
              ${todo.title}
              <button onclick="completeTodo(${todo.id})">Done</button>
              <button onclick="deleteTodo(${todo.id})">Delete</button>
            </li>
          `).join('')}
        </ul>
        <form onsubmit="addTodo(event)">
          <input type="text" id="newTodo" placeholder="New todo...">
          <button type="submit">Add</button>
        </form>
      `;
    }

    window.addTodo = (event) => {
      event.preventDefault();
      const input = document.getElementById('newTodo');
      const title = input.value.trim();

      if (!title) return;

      bus.publish('todos.item.save', {
        item: { title, completed: false }
      });

      input.value = '';
    };

    window.completeTodo = (id) => {
      // Fetch current state, update, and save
      const unsub = bus.subscribe(`todos.item.state.${id}`, (msg) => {
        const todo = msg.data.item;
        bus.publish('todos.item.save', {
          item: { ...todo, completed: true }
        });
        unsub();
      }, { retained: true });

      bus.publish('todos.item.get', { id });
    };

    window.deleteTodo = (id) => {
      if (confirm('Delete this todo?')) {
        bus.publish('todos.item.delete', { id });
      }
    };
  </script>
</body>
</html>

#### Request-Response Pattern

For operations that need explicit confirmation:

const bus = document.querySelector('pan-bus');

async function saveUser(userData) {
  return new Promise((resolve, reject) => {
    const correlationId = `save-${Date.now()}`;
    const replyTo = `app.reply.${correlationId}`;

    // Subscribe to reply
    const unsub = bus.subscribe(replyTo, (msg) => {
      unsub();
      if (msg.data.ok) {
        resolve(msg.data.item);
      } else {
        reject(new Error(msg.data.error.body?.message || 'Save failed'));
      }
    });

    // Send request with reply routing
    bus.publish('users.item.save', {
      item: userData,
      replyTo,
      correlationId
    });
  });
}

// Usage
try {
  const savedUser = await saveUser({ name: 'Alice', email: 'alice@example.com' });
  console.log('User saved:', savedUser);
} catch (error) {
  console.error('Failed to save user:', error);
}

#### Query Parameters and Filtering

// Paginated list with filters
bus.publish('products.list.get', {
  page: 2,
  limit: 20,
  category: 'electronics',
  minPrice: 100,
  maxPrice: 1000,
  sort: 'price:asc'
});

// The connector converts this to:
// GET /api/products?page=2&limit=20&category=electronics&minPrice=100&maxPrice=1000&sort=price%3Aasc

Related Components

  • pan-bus: Required for message routing
  • pan-auth-provider: Automatic authentication header injection
  • pan-store: Can be used to cache connector state in memory
  • pan-idb: Can persist connector state to IndexedDB for offline support

Common Issues and Solutions

#### Issue: CORS Errors

Symptom: Browser console shows "Access-Control-Allow-Origin" errors. Solution: Configure your server to include proper CORS headers, or use a proxy during development:
// Development proxy in Vite config
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

#### Issue: Stale Data After Updates

Symptom: List doesn't reflect changes after creating/updating items. Solution: The connector automatically refreshes the list after save/delete operations. If you need manual refresh:
bus.publish('users.list.get', {});

#### Issue: 401 Unauthorized Errors

Symptom: Requests fail with 401 status after initial success. Solution: Ensure your auth token is being refreshed. The connector automatically picks up new tokens from auth.internal.state:
// When token is refreshed
bus.publish('auth.internal.state', {
  authenticated: true,
  token: newToken,
  user: { id: 123, name: 'Alice' }
}, { retain: true });

#### Issue: Slow Performance with Large Lists

Symptom: UI freezes when loading large datasets. Solution: Implement pagination and avoid loading all items at once:
// Load in pages
const PAGE_SIZE = 50;
let currentPage = 1;

function loadNextPage() {
  bus.publish('items.list.get', {
    page: currentPage,
    limit: PAGE_SIZE
  });
  currentPage++;
}

pan-graphql-connector

Overview

pan-graphql-connector bridges LARC's PAN bus to GraphQL APIs. It maps the same CRUD topic patterns as pan-data-connector but executes GraphQL queries and mutations instead of REST calls. You define your GraphQL operations as child