LARC — Page Area Network (PAN)
Version: 0.1 (Draft) Status: Proposal Editors: (add) License: MIT1. Overview
PAN is a lightweight, build-free coordination protocol and runtime for browser UIs. It provides a page-local message bus so Web Components (and other DOM-based widgets) can discover each other, publish/subscribe to topics, and perform request/response without framework coupling.
1.1 Goals
* Simple to adopt: add a element; components publish/subscribe via DOM events.
* Framework-agnostic: usable from Web Components, React/Vue/Svelte, vanilla JS, iframes.
* Loose coupling: components depend on topic contracts, not concrete peers.
* Inspectable: traffic can be recorded, replayed, and validated.
* No build required; no global singletons required.
1.2 Non‑Goals
* Not a full state management framework. * Not a routing standard (though topics may carry navigation intents). * Not a network protocol; transport is strictly in‑page unless extended.
2. Terminology
* Bus: the orchestrator custom element ; maintains topic registry and relays messages.
* Client: any producer/consumer of PAN messages (custom element, script, iframe).
* Topic: string identifier for a message category, namespaced (e.g., pan:user.update).
* Message: immutable payload published on a topic.
* Session: lifetime of a bus instance in a page/document.
3. Architecture
* A single instance per document is recommended (multiple allowed with explicit scoping).
* Clients communicate with the bus via DOM CustomEvents (primary transport).
* Optional mirrors: BroadcastChannel (cross‑tab), postMessage to sandboxed iframes, SharedWorker cache layer.
<pan-bus id="bus" version="0.1"></pan-bus>
4. Message Model
4.1 Type Definition (TypeScript-flavored)
interface PanMessage<T=unknown> {
topic: string; // e.g., "pan:user.update"
data: T; // JSON-serializable payload
id?: string; // ULID/UUID; assigned by bus if missing
ts?: number; // ms since epoch; assigned by bus if missing
source?: string; // client ID or element tag/id
replyTo?: string; // topic for replies (RR pattern)
correlationId?: string; // to join requests/replies
qos?: 0 | 1; // 0=fire-and-forget, 1=at-least-once w/ ack
retain?: boolean; // retain last message on topic
ttlMs?: number; // soft expiration
headers?: Record<string,string>; // meta: schema, version, trace, etc.
}
4.2 Event Envelopes
All API interactions use bubbling, composed CustomEvent objects so they cross shadow DOM boundaries.
interface PanPublishEvent extends CustomEvent<PanMessage> {
type: 'pan:publish';
}
interface PanSubscribeEvent extends CustomEvent<PanSubscribe> {
type: 'pan:subscribe';
}
interface PanUnsubscribeEvent extends CustomEvent<PanUnsubscribe> {
type: 'pan:unsubscribe';
}
interface PanRequestEvent extends CustomEvent<PanMessage> {
type: 'pan:request'; // expects reply
}
interface PanReplyEvent extends CustomEvent<PanMessage> {
type: 'pan:reply';
}
interface PanHelloEvent extends CustomEvent<PanHello> {
type: 'pan:hello'; // discovery
}
interface PanAckEvent extends CustomEvent<PanAck> {
type: 'pan:ack'; // QoS1 ack from bus→client or client→bus
}
All events must be dispatched with { bubbles: true, composed: true }.
5. Topics & Namespacing
* Hierarchical dotted paths, lowercase kebab tokens: pan:user.update, pan:navigation.goto.
Wildcards (subscription only): pan:user., * (all topics) — optional depending on security policy.
* Versioning: suffix or header, e.g., pan:user.update@2 or headers['schema'] = 'pan:user.update@2'.
Reserved prefixes: pan:$ for control/meta; pan:sys. for bus diagnostics.
6. Discovery
Clients announce themselves and capabilities.
interface PanHello { id?: string; caps?: string[]; wants?: string[]; version?: string }
// Client → Bus
node.dispatchEvent(new CustomEvent('pan:hello', { detail: { id: 'x-foo#1', caps:['ui','fetch'] }, bubbles:true, composed:true }));
// Bus → Client reply on 'pan:$hello.reply'
Bus maintains a registry of clientId → { element, caps } for inspection and targeted messaging.
7. Subscription API
interface PanSubscribe {
clientId?: string; // bus assigns if absent
topics: string[]; // allowed wildcards if policy permits
options?: {
retained?: boolean; // immediately receive last retained message per topic
filter?: Record<string, unknown>; // optional header/data matchers
signal?: AbortSignal; // auto-unsubscribe on abort
}
}
* Bus emits messages to subscribers via a DOM event targeted at the subscriber element:
* Event type: pan:deliver
* detail: PanMessage
* Unsubscribe mirrors subscribe with PanUnsubscribe (same shape, or signal.abort()).
8. Publish & Request/Reply
8.1 Publish
node.dispatchEvent(new CustomEvent('pan:publish', {
detail: { topic:'pan:user.update', data:{ id:'u1', name:'Ada' }, retain:true },
bubbles:true, composed:true
}));
8.2 Request/Reply
// Requester
const correlationId = crypto.randomUUID();
node.dispatchEvent(new CustomEvent('pan:request', {
detail: { topic:'pan:data.get', data:{ key:'users' }, replyTo:'pan:$reply', correlationId },
bubbles:true, composed:true
}));
// Replier (subscriber to pan:data.get)
// on receipt, reply
node.dispatchEvent(new CustomEvent('pan:reply', {
detail: { topic:'pan:$reply', correlationId, data:{ users:[/*...*/] } },
bubbles:true, composed:true
}));
* Bus may provide a convenience RPC wrapper (see §13 Helper API).
9. QoS & Backpressure
* qos:0 (default): deliver best-effort; no ack.
* qos:1: bus requires a pan:ack from each subscribed client; if missing after ackTimeoutMs, it retries or logs.
* Bus implements coalescing per topic and optional rate limits per publisher. Recommended defaults:
* deliverBatchMax=64, deliverIntervalMs=8, maxQueueDepth=10_000 (drop oldest with warning when exceeded).
10. Retained Messages & Snapshots
* If retain:true, bus stores the last message per topic and immediately delivers it to new subscribers (if options.retained).
* Bus exposes snapshot API via control topic pan:sys.snapshot returning { topics: string[], counts, retained }.
11. Schema & Validation
* Each topic should define a JSON Schema ($id = topic@version).
* Bus can validate on publish; invalid messages produce pan:sys.error with { code:'SCHEMA_VIOLATION', details }.
* Suggested headers: headers: { schema: 'pan:user.update@2' }.
12. Security Model
* Trust domains:
* Same-origin components: full access by default.
* Cross-origin iframes: must communicate via a gateway component (see §15) with an allowlist.
* Permissions: optional policy map topic → { publish:[roles], subscribe:[roles] }.
* Sanitization: data must be JSON-serializable; functions/DOM nodes rejected.
* Isolation: sensitive providers can be sandboxed in iframes; gateway validates topics & schemas.
13. Helper API (Reference)
A tiny helper for clients; pure ES module, no build.
class PanClient {
constructor(host: HTMLElement | Document = document, busSelector = 'pan-bus') {}
async ready(): Promise<void> {}
publish<T>(msg: PanMessage<T>): void {}
subscribe(topics: string | string[], handler: (m: PanMessage)=>void, opts?: { retained?: boolean, signal?: AbortSignal }): () => void {}
request<TReq, TRes>(topic: string, data: TReq, opts?: { timeoutMs?: number, schema?: string }): Promise<PanMessage<TRes>> {}
}
Minimal semantics:
* ready() resolves when the bus announces pan:sys.ready.
* subscribe() returns an unsubscribe function and auto-uses an AbortSignal if provided.
* request() handles correlation, temporary replyTo, and timeout.
14. Control & Diagnostics Topics
* pan:sys.ready — fired when bus starts.
* pan:sys.clients — query list of clients/caps.
* pan:sys.snapshot — retained topics & counts.
* pan:sys.log — structured log stream from bus.
* pan:sys.error — standardized errors:
interface PanError { code: string; message: string; cause?: unknown; correlationId?: string }
15. Cross‑Context Bridges (Optional)
* BroadcastChannel('pan') mirror for multi‑tab sync:
Mirrored topics must be declared in bus attr:mirror-topics="pan:user. pan:settings.*".
* Iframe Gateway:
Embedded translates PAN⇆postMessage.
* Gateway validates origin, topic allowlist, and schemas.
* SharedWorker cache for data providers to dedupe network fetches across tabs.
16. Lifecycle
* Bus boot:
1. Initialize registries and retained store
2. Attach listeners for pan:* events at document
3. Emit pan:sys.ready
* Client boot:
1. Wait for pan:sys.ready (or poll for pan-bus)
2. Dispatch pan:hello
3. Subscribe to topics
* Cleanup: clients must unsubscribe or provide AbortSignal; bus purges dead targets on GC signals or delivery failures.
17. Shadow DOM Rules
* All bus-bound events must use { bubbles:true, composed:true }.
* Delivery events target the subscriber element; for closed shadow roots, delivery targets the host element.
18. Versioning & Capability Negotiation
* Semver for topic schemas; clients may advertise caps: ['pan:user.update@^2', 'pan:data.get@~1.3'].
* Bus exposes supports(schemaRange: string): boolean via control request pan:$supports.
19. Performance Guidance
* Prefer coarse topics plus fine-grained fields over chatty micro-topics. * Batch deliveries per animation frame; coalesce duplicate retained updates. * Avoid large payloads (>100KB); use references to caches or streams.
20. Accessibility Guidance
* PAN must not be the only trigger path; all user actions should map to semantic controls. * Announce significant state changes via ARIA‑friendly components, not just PAN traffic.
21. Compliance Tests (CT)
* CT‑01: subscribe receives retained on join when retained:true.
* CT‑02: wildcard subscription respects allowlist.
* CT‑03: request/reply correlation within timeout.
* CT‑04: schema validation rejects and logs error.
* CT‑05: shadow DOM crossing works with composed:true.
22. Minimal Reference Implementation Sketch
<pan-bus id="bus"></pan-bus>
<script type="module">
class PanBus extends HTMLElement {
registry = new Map(); // clientId -> element
subs = new Map(); // topic -> Set(clientId)
retained = new Map(); // topic -> PanMessage
connectedCallback() {
this.addEventListener('pan:publish', this.onPublish, { capture:true });
this.addEventListener('pan:request', this.onRequest, { capture:true });
this.addEventListener('pan:reply', this.onReply, { capture:true });
this.addEventListener('pan:subscribe', this.onSubscribe, { capture:true });
this.addEventListener('pan:unsubscribe', this.onUnsubscribe, { capture:true });
this.addEventListener('pan:hello', this.onHello, { capture:true });
queueMicrotask(()=>document.dispatchEvent(new CustomEvent('pan:sys.ready', { bubbles:true, composed:true })));
}
// ...handlers deliver via target.dispatchEvent(new CustomEvent('pan:deliver', {detail:m}))
}
customElements.define('pan-bus', PanBus);
</script>
23. Example Topics (Starter Set)
* pan:navigation.goto — { href:string, replace?:boolean }
* pan:user.update — { id:string, name?:string, email?:string }
* pan:settings.set — { key:string, value:any }
* pan:data.get / pan:data.put — { key:string, value?:any }
24. Open Questions
* Should the bus support Streams (ReadableStream) as payload references? * Built‑in persistence (IndexedDB) for retained topics? * Formal ACL spec vs pluggable policy hooks?
25. Next Steps
* Publish JSON Schemas for starter topics.
* Ship pan-client helper (1KB gz).
* Build PAN Inspector (traffic console + replay).
* Author compliance test suite (Web Test Runner).
26. CRUD Suite v1 (Draft)
This section specifies a minimal, interoperable contract for CRUD-style components communicating over PAN. It defines topics, message shapes, and component roles so tables, forms, and connectors can be mixed-and-matched.
26.1 Topics
- List request:
${resource}.list.getwithdata?: { page?, size?, sort?, filter? }
replyTo with { items, page?, size?, total? }.
- Provider publishes ${resource}.list.state with { items, page?, size?, total? } and retain:true.
- List state:
${resource}.list.state(retained snapshot for subscribers withretained:true). - Item select:
${resource}.item.selectwith{ id }(view → provider hint; no reply required). - Item get:
${resource}.item.getwith{ id }→ reply{ ok:boolean, item?:object, error?:any }. - Item save:
${resource}.item.savewith{ item:object }→ reply{ ok:boolean, item?:object, error?:any }. - Item delete:
${resource}.item.deletewith{ id }→ reply{ ok:boolean, id?:string|number, error?:any }.
- Providers SHOULD publish an updated
${resource}.list.stateafter save/delete. - Providers SHOULD accept
keyother thanid(attribute or header-driven), but reply payloads MUST include the resolved identifier. - Versioning MAY be conveyed via topic suffix
@Nor headerschema: '.@ '
26.2 Component Roles
- Data Table (
): subscribes to${resource}.list.state(retained), renders tabular rows, and publishes${resource}.item.selecton row click. Attributes:resource,columns, optionalkey. - Form (
): listens for${resource}.item.select, requests${resource}.item.get, and publishes${resource}.item.save/${resource}.item.delete. Attributes:resource,fields, optionalkey. - Mock Provider (
): in-memory storage, optional localStorage persistence; handles CRUD topics and publishes list state. - REST Connector (
): maps CRUD topics to HTTP endpoints. Attributes:base-url, optionallist-path,item-path,update-method,credentials. Accepts child JSON for fetch options.
26.3 Error Semantics
- On failure, providers SHOULD reply
{ ok:false, error }on the original request'sreplyTo;errorMAY includestatus,statusText, and parsed body when applicable. - Providers MAY also emit
*.errorevent topics for ambient error displays.
26.4 Security & Privacy
- Only mirror non-sensitive topics across tabs (e.g., view filters). Do not mirror PII or secrets.
- Keep payload sizes small; consider pagination and server-side filtering.