<p align="center">
  <img src="assets/vapor-chamber.png" alt="Vapor Chamber">
</p>

<p align="center">
  A command bus built for <a href="https://github.com/vuejs/vue-vapor">Vue Vapor</a> — a ~4 KB brotli dispatch core with opt-in batteries, each 0 KB until imported. Vue 3.6 Vapor aligned. LGPL-2.1.
</p>

## What is Vapor Chamber?

Vapor Chamber is a **command bus for Vue 3.6+ Vapor mode**. Every user action gets a single handler, a composable plugin pipeline, and signal-native reactive state — replacing scattered event listeners and prop-drilling with one predictable, testable flow.

```ts
import { getCommandBus, useCommand } from 'vapor-chamber';

const bus = getCommandBus();

bus.register('cartAdd', (cmd) => addToCart(cmd.target));
bus.use(logger());
bus.use(validator({ cartAdd: (cmd) => cmd.target.id ? null : 'Missing ID' }));

// In a component — same shared bus, with reactive state
const { dispatch, loading, lastError } = useCommand();
dispatch('cartAdd', { id: product.id });
```

**What's in the can** — a small core, and batteries you only pay for if you import them:

| | |
|---|---|
| **Core** (the bus) | dispatch/query/emit, plugin pipeline, wildcard listeners — framework-agnostic, no Vue import; `createCommandBus` tree-shakes to ~3.6 KB brotli (measured sizes: [docs/BUNDLE-SIZES.md](docs/BUNDLE-SIZES.md)) |
| **Vue composables** | `useCommand`, `useCommandState`, shared state, Vapor-mode (`defineVaporCommand`) and full Vapor wrappers |
| **Plugins** (opt-in) | logger, validator, history (undo/redo), debounce, throttle, retry, persist, cross-tab sync, serialize, idempotent, auth guard |
| **Transports** (opt-in) | HTTP bridge, WebSocket, SSE, Laravel Echo/Reverb |
| **Extras** (opt-in, own subpath each) | SSR dehydrate/rehydrate, form bus, HTTP client, streaming JSON parser, schema validation, transitions bridge, devtools, Vite HMR, testing utilities |

- **Vue 3.6.0-beta.17 aligned** — signals, `onScopeDispose`, `getCurrentScope`, alien-signals internals; tracked per beta (see CHANGELOG)
- **One runtime dependency** (`alien-signals`); `sideEffects: false` — unimported modules tree-shake to zero
- **IIFE builds for no-bundler pages** — full ~10 KB / core & elements ~7 KB brotli (exact, always-current per-export table: [docs/BUNDLE-SIZES.md](docs/BUNDLE-SIZES.md), generated by `npm run size:doc`)
- **SSR support** — dehydrate on the server, replay on the client (see `vapor-chamber/ssr`; the shared-bus helper is per-process — concurrent SSR servers should pass a per-request bus explicitly)

## Install

```bash
npm install vapor-chamber        # npm registry (releases may lag the repo)

# or install straight from the repo — the authoritative source while Vue 3.6
# is in beta (a `prepare` script builds it on install):
npm install github:lucianofedericopereira/vapor-chamber
```

Requires Node ≥20.19. Vue is an **optional** peer dep (≥3.5 for composables, ≥3.6.0-beta.17 for the full Vapor surface) — the core bus runs without Vue entirely.

**Using Astro?** See [examples/exo-astro](examples/exo-astro) — a declarative directive set
(`v-scope`, `v-command`, `v-bind-text`, `v-show`) over the vapor-chamber bus for coordinating
independent Astro page sections, with `onMissing: 'buffer'` (+ `bufferTTL`) so sections can
dispatch before their handlers hydrate.

---

## What is Vue Vapor?

Vue Vapor is Vue's compilation strategy that eliminates the Virtual DOM. Instead of diffing virtual trees, Vapor compiles templates to direct DOM operations using **signals** — reactive primitives that update only what changed.

**As of Vue 3.6 beta**, Vapor mode is feature-complete for all stable APIs. The reactivity engine has been rewritten atop [alien-signals](https://github.com/stackblitz/alien-signals), delivering ~14% less memory and faster dependency tracking. `ref()` is now a signal internally.

**Vapor Chamber** embraces this philosophy: minimal abstraction, direct updates, signal-native reactivity.

## Migrating from Vue 3 emitters

If you're already using Vue 3's `emit` / `eventBus` pattern, here's the before and after:

```
// Before — Vue 3 emitter
// cart.vue
emit('cart:add', product);

// App.vue
bus.on('cart:add', (product) => {
  cart.items.push(product);
  analytics.track('add');
  validate(product);  // where does this live?
});

// ProductList.vue — also listens?
bus.on('cart:add', updateBadge);  // now two handlers, hard to trace
```

```
// After — Vapor Chamber
// Anywhere in the app
bus.dispatch('cartAdd', product, { quantity: 1 });

// One place, once:
bus.register('cartAdd', (cmd) => {
  cart.items.push(cmd.target);
  return cart.items;
});

// Cross-cutting concerns as plugins, not scattered listeners:
bus.use(logger());
bus.use(validator({ 'cartAdd': (cmd) => cmd.target.id ? null : 'Missing ID' }));
bus.use(analyticsPlugin);
```

**The key difference:** `emit` is fire-and-forget with many listeners. `dispatch` has one handler and a composable plugin pipeline — one place to look, debug, and test.

## Why a Command Bus?

Traditional event systems scatter logic across components. A command bus centralizes it:

```
Event-driven (scattered)          Command bus (centralized)
─────────────────────────         ─────────────────────────
Component A emits 'add'    →      dispatch('cartAdd', product)
Component B listens...            ↓
Component C also listens...       Handler executes once
Who handles what? When?           Plugins observe/modify
                                  Result returned
```

**Benefits:**
- **Semantic actions** — `cartAdd` is clearer than `emit('add')`
- **Single handler** — One place to look, debug, test
- **Plugin pipeline** — Cross-cutting concerns (logging, validation, analytics) without cluttering handlers
- **Undo/redo** — Command history is natural when actions are explicit

## Module Architecture

vapor-chamber is built in layers. The **core** is framework-agnostic, has zero dependencies, and is the only part required for v1.0. Everything else is optional and tree-shaken when not imported.

```
┌─────────────────────────────────────────────────────────┐
│  CORE  (zero deps · fully tested · framework-agnostic)  │
│  command-bus.ts  ·  testing.ts                          │
└────────────────────────┬────────────────────────────────┘
                         │ optional layers (tree-shaken)
         ┌───────────────┼───────────────┐
         ▼               ▼               ▼
   Vue composables    Plugins        Transport
   chamber.ts         plugins-core   http.ts
   chamber-vapor.ts   plugins-io     transports.ts
         │
         ▼
   Extras (per-feature opt-in)
   form.ts · schema.ts · devtools.ts · directives.ts · vite-hmr.ts
```

### Coverage & stability

Overall **~97% statements / ~91% branches / ~98% lines** across **884 tests** (47 files). The
dispatch core is at **100% line + branch + function** coverage. Per-module **line** coverage below;
run `npm run test:coverage` for live, authoritative numbers.

| Layer | Module | Lines | Status |
|-------|--------|-------|--------|
| **Core** | `command-bus.ts` | 100% | ✅ Stable |
| **Core** | `testing.ts` | test harness — excluded by design | ✅ Stable |
| Plugins | `plugins-core.ts` | 100% | ✅ Stable |
| Plugins | `plugins-io.ts` | 100% | ✅ Stable |
| Transport | `http.ts` | 100% | ✅ Stable |
| Transport | `transports.ts` | 100% | ✅ Stable |
| Vue | `chamber.ts` | ~96% lines / ~87% branch | ✅ Stable |
| Extras | `form.ts` | ~92% | ✅ Stable |
| Extras | `schema.ts` | 100% | ✅ Stable |
| Vue 3.6 | `chamber-vapor.ts` | ✅ | ✅ Stable (unit-tested without Vue 3.6 runtime) |
| Vue | `directives.ts` | ✅ | ✅ Stable (unit-tested with DOM stubs) |
| Build | `devtools.ts` | ✅ | ✅ Stable (unit-tested with mock DevTools API) |
| Build | `vite-hmr.ts` | ✅ | ✅ Stable (unit-tested without Vite runtime) |
| Build | `iife.ts` | — | 🔧 Bundle entry, not a public API |

Sub-path exports avoid pulling in optional modules:
```
'vapor-chamber'             → core + composables + everything (tree-shaken)
'vapor-chamber/transports'  → HTTP + WebSocket + SSE bridges only
'vapor-chamber/directives'  → v-command Vue directive only
'vapor-chamber/vite'        → Vite HMR plugin only
'vapor-chamber/transitions'   → View Transitions API helpers only
'vapor-chamber/ssr'           → SSR dehydrate/replay helpers only
'vapor-chamber/fast-lane'     → minimal-allocation dispatcher for real-real-hot loops (game ticks, trading data, audio, scroll). Not a bus — see docs/performance.md.
'vapor-chamber/observable'    → Symbol.observable interop — RxJS / xstream / callbag adapter
'vapor-chamber/standard-schema'→ Standard Schema v1 validator plugin (Zod / Valibot / ArkType compatible)
'vapor-chamber/alien-signals' → connector to use alien-signals as the underlying reactive primitive (non-Vue contexts)
'vapor-chamber/reactive'      → opt-in DEEP reactivity: deepSignal + useDeepCommandState (nested-mutation tracking; the core signal() is shallow+fast by default)
'vapor-chamber/outbox'        → offline outbox: durable queue + ordered replay with Idempotency-Key (PWA / flaky-network e-commerce)
'vapor-chamber/mcp'           → zero-dep MCP server from your schema bus — agents drive your commands (whitelist + meta.origin='agent')
'vapor-chamber/iife'          → IIFE bundle (full)
'vapor-chamber/iife-core'     → IIFE bundle (no Vapor custom-element, no Suspense paths)
'vapor-chamber/iife-elements' → IIFE bundle (core + Vapor custom-element)
```

### IIFE / CDN variants

Three `<script>`-tag drop-ins, sized for distinct deployment shapes. Pick by
audience, not by feature checklist.

| Variant   | Audience                                     | File                                  | Min   | Brotli | Gzip   |
|-----------|----------------------------------------------|---------------------------------------|-------|--------|--------|
| core      | Sprinkled JS on server-rendered pages (Blade, Rails, Django, .NET MVC, WordPress). You dispatch user actions to a backend over HTTP. | `vapor-chamber-core.iife.js`     | 24.5 KB | 7.0 KB  | 7.8 KB  |
| elements  | Embeddable widgets (chat bubbles, checkout buttons, third-party drop-ins). You ship a `<vc-widget>` custom element. | `vapor-chamber-elements.iife.js` | 26.0 KB | 7.4 KB  | 8.3 KB  |
| full      | SPAs that grew big enough to want everything (realtime, undo/redo, persistence, full Vapor surface). | `vapor-chamber.iife.js`              | 35.4 KB | 10.2 KB | 11.3 KB |

**What's in each variant**

| Surface                                                  | core | elements | full |
|----------------------------------------------------------|:----:|:--------:|:----:|
| Bus (`createCommandBus`, `createAsyncCommandBus`)        | ✅   | ✅       | ✅   |
| `createApp()`, `connect()` one-liner                     | ✅   | ✅       | ✅   |
| HTTP transport (`http`)                                  | ✅   | ✅       | ✅   |
| Light plugins (logger, validator, debounce, throttle, retry, authGuard) | ✅   | ✅       | ✅   |
| `defineVaporCustomElement`, `defineWidget()`             | ❌   | ✅       | ✅   |
| WebSocket / SSE (`ws`, `sse`)                            | ❌   | ❌       | ✅   |
| Heavy plugins (persist, sync, history, optimistic)       | ❌   | ❌       | ✅   |
| `mount()`                                                | ❌   | ❌       | ✅   |
| Full Vapor (`defineVaporComponent`, async/Suspense)      | ❌   | ❌       | ✅   |

**Examples**

```html
<!-- core: dispatch over HTTP, with CSRF auto-wired -->
<script src=".../vapor-chamber-core.iife.min.js"></script>
<script>
  const { dispatch } = VaporChamber.connect({ endpoint: '/api/vc' });
  document.getElementById('add').addEventListener('click',
    () => dispatch('cartAdd', { id: 42 }));
</script>
```

```html
<!-- elements: register a custom-element widget in one call -->
<script src=".../vapor-chamber-elements.iife.min.js"></script>
<script>
  VaporChamber.defineWidget('vc-cart', {
    props: { sku: String },
    setup(props) { return () => h('span', `SKU ${props.sku}`); }
  });
</script>
<vc-cart sku="ABC-123"></vc-cart>
```

Sizes above are **minified brotli** (q=11) / gzip (-9); brotli is what hits the wire on
modern CDNs. The dispatch core alone (`createCommandBus`, tree-shaken) is **~3.6 KB brotli**.
These figures — and the per-export ESM sizes — are regenerated and **CI-verified fresh** in
**[docs/BUNDLE-SIZES.md](docs/BUNDLE-SIZES.md)** (the always-current source of truth, via
`npm run size:doc`); `npm run size:check` fails CI on any regression past the budget. The build
is `scripts/build.mjs` (Vite 8 / rolldown).

**Size by version** (IIFE brotli — `full` / `core` / `elements`):

| Version | full | core | elements | Notes |
|---------|-----:|-----:|---------:|-------|
| v1.2.0 | 8.7 | 6.1 | 6.4 | audience-split baseline |
| v1.6.0 (beta.15) | 10.4 | 7.1 | 7.5 | +realtime / undo-redo / `onMissing:'buffer'` / Echo |
| v1.7.0 (unreleased, beta.17) | **10.2** | **7.0** | **7.4** | Vite 8 / rolldown minifier — **smaller, no regression** |

The v1.2 → v1.6 growth tracked added capabilities (see CHANGELOG per version); the latest move
is a **decrease**. Verified each release against the prior baseline via `npm run size:check`.

> **Variant contents are not under semver before v2.0.** While Vue 3.6 is in
> beta, the lib reserves the right to move APIs between IIFE variants. ESM
> consumers (the `vapor-chamber` main entry) get the full surface and are
> unaffected. See [ROADMAP.md](https://github.com/lucianofedericopereira/vapor-chamber/blob/main/ROADMAP.md).

---

## Requirements

Node.js ≥20.19.0 | Vue ≥3.5.0 (composables) or ≥3.6.0-beta.17 (full Vapor) — optional peer dep | Vite ≥5.0.0 + `@vitejs/plugin-vue` ≥5.0.0 (only required for the `vapor-chamber/vite` HMR plugin and Vapor SFC support)

This package is **ESM-only** — no CJS build. Node ≥20 `import`, bundlers, and
`<script type="module">` all work; for classic `<script>` tags use the
[IIFE variants](#iife--cdn-variants). (Install instructions: [Install](#install).)

> **Beta tracking:** this lib follows Vue 3.6 while it is in beta. The Vapor
> wrappers are transitional surfaces that will realign once Vue 3.6 ships
> stable; the command composables are now consolidated into a single
> Vapor-safe `useCommand`. See [ROADMAP.md](https://github.com/lucianofedericopereira/vapor-chamber/blob/main/ROADMAP.md)
> for what is stable today, what is transitional, and the v1.3 / v2 plan.
>
> **Performance & tuning:** see [docs/performance.md](https://github.com/lucianofedericopereira/vapor-chamber/blob/main/docs/performance.md)
> for what's optimized by default, the consumer-facing tuning knobs
> (`persist({ coalesce: true })`, `configureUid`, `configureSignal`), variant
> selection guide, and a benchmark snapshot.
>
> **Laravel integration:** see [docs/integrations/laravel.md](https://github.com/lucianofedericopereira/vapor-chamber/blob/main/docs/integrations/laravel.md)
> for the full backend deliverables list (route, controller, action classes,
> CSRF flows, Sanctum, Inertia coexistence, Filament panels, Reverb
> realtime, queued commands). Runnable PHP companions live in
> [examples/laravel-backend/](https://github.com/lucianofedericopereira/vapor-chamber/tree/main/examples/laravel-backend).
>
> **Routing (in-box subpath):** the [`vapor-chamber/router`](docs/router.md)
> subpath is a router for Vue 3.6 over Laravel Blade: one thin server
> catch-all, path = navigation, query = state, route tables and data loaders
> delivered as data. Loading is pluggable via a loader SPI — the in-box
> `vapor-chamber/router-fetch` preset covers plain-JSON backends, and any other
> backend convention is a preset you supply. The bus keeps owning writes
> (commands); the router owns reads (URL-addressed data). Inertia/vue-router
> coexistence notes below remain valid for apps using those instead.
>
> **API reference:** generate locally with `npm run docs` (TypeDoc → `docs/api/`).
> The generated site is `.gitignore`d so it stays fresh per release. Hosting it
> publicly (e.g. via GitHub Pages or Netlify) is on the v1.3 ROADMAP.

## Quick Start

```typescript
import { createCommandBus, logger, validator } from 'vapor-chamber';

const bus = createCommandBus();

// Add plugins
bus.use(logger());
bus.use(validator({
  'cartAdd': (cmd) => cmd.payload?.quantity > 0 ? null : 'Quantity required'
}));

// Register handler
bus.register('cartAdd', (cmd) => {
  cart.items.push({ ...cmd.target, quantity: cmd.payload.quantity });
  return cart.items;
});

// Dispatch
const result = bus.dispatch('cartAdd', product, { quantity: 2 });
if (result.ok) {
  console.log('Added:', result.value);
} else {
  console.error('Failed:', result.error);
}
```

### Define each command once — the typed contract

One schema literal is the single source of truth for types, validation, the
backend, and AI tools:

```ts
// commands.ts
import { defineSchema, createSchemaCommandBus, setCommandBus, type CommandsOf } from 'vapor-chamber';

export const schema = defineSchema({
  cartAdd: {
    description: 'Add a product to the cart',
    target:  { id: 'number', name: 'string' },
    payload: { qty: 'number' },
    result:  { count: 'number', total: 'number' },
  },
});

setCommandBus(createSchemaCommandBus(schema));   // typed dispatch + runtime validation

declare module 'vapor-chamber' {                 // typed useCommand() everywhere
  interface GlobalCommands extends CommandsOf<typeof schema> {}
}
```

From the same schema: `bus.toTools()` (Anthropic/OpenAI), `vapor-chamber/mcp`
(agents drive your commands over MCP, whitelisted, stamped `meta.origin`),
and `node scripts/generate-laravel.mjs commands.mjs` (Laravel config registry +
action-class stubs with validation rules). Misspell an action or a field in a
component and it's a compile error, not a runtime 404.

### Gotcha: module-scope signals before Vue boots

Vue detection is asynchronous. A `signal()` / `useCommandState()` created at
**module scope** (before `createApp` runs) may be created before Vue is
detected — it stays a plain `{ value }` object and never becomes reactive
(a one-shot dev-mode warning fires when this happens). Two safe options:

```ts
// 1. Create reactive state inside components / after app boot (usual case), or
// 2. await detection explicitly for module-scope state:
import { waitForVueDetection, signal } from 'vapor-chamber';
await waitForVueDetection();
export const count = signal(0); // now Vue-reactive
```

## Vue 3.6 Vapor Mode

Vapor Chamber v1.0 is aligned with Vue 3.6 beta. It works in three contexts:

### 1. Pure Vapor App (smallest bundle)

```typescript
import { createVaporChamberApp, getCommandBus } from 'vapor-chamber';
import App from './App.vue';

// No VDOM runtime — ~10KB baseline
createVaporChamberApp(App).mount('#app');
```

```vue
<script setup vapor>
import { useCommand } from 'vapor-chamber';

const { dispatch, loading } = useCommand();
</script>
```

### 2. Mixed VDOM + Vapor (gradual migration)

```typescript
import { createApp } from 'vue';
import { getVaporInteropPlugin } from 'vapor-chamber';

const app = createApp(App);
const interop = getVaporInteropPlugin();
if (interop) app.use(interop);
app.mount('#app');
```

Now Vapor and VDOM components can nest inside each other. Useful for incremental migration.

### 3. Standard Vue 3 (no Vapor)

Everything works without Vapor. The signal shim auto-detects Vue's `ref()` for reactivity. In Vue 3.6+ this is alien-signals backed.

### Vapor Detection

```typescript
import { isVaporAvailable } from 'vapor-chamber';

if (isVaporAvailable()) {
  // Vue 3.6+ with createVaporApp available
}
```

## Core Concepts

### Commands

A command has three parts:

```typescript
bus.dispatch(
  'cartAdd',      // action - what to do
  product,         // target - what to act on
  { quantity: 2 }  // payload - additional data (optional)
);
```

### Naming Convention

Enforce consistent action names at register and dispatch time:

```typescript
const bus = createCommandBus({
  naming: {
    pattern: /^[a-z][a-zA-Z0-9]+$/,  // camelCase
    onViolation: 'throw'  // or 'warn' or 'ignore'
  }
});

bus.register('cartAdd', handler);       // ✓ passes
bus.register('cart_add', handler);      // ✗ throws
```

### Handlers

One handler per action. Returns a value or throws:

```typescript
bus.register('cartAdd', (cmd) => {
  cart.items.push(cmd.target);
  return cart.items; // becomes result.value
});
```

Register with options for undo support and per-command throttling:

```typescript
bus.register('cartAdd', addHandler, {
  undo: (cmd) => { cart.items.pop(); },
  throttle: 300,  // max once per 300ms per target
});
```

### Results

Every dispatch returns a result:

```typescript
type CommandResult = {
  ok: boolean;     // success or failure
  value?: any;     // handler return value (if ok)
  error?: Error;   // error thrown (if not ok)
};
```

### Plugins

Plugins wrap handlers. They can modify commands, short-circuit execution, observe results, or transform output:

```typescript
const timingPlugin: Plugin = (cmd, next) => {
  const start = Date.now();
  const result = next();  // call next plugin or handler
  console.log(`${cmd.action} took ${Date.now() - start}ms`);
  return result;
};

bus.use(timingPlugin);
```

Plugins execute by priority (highest first), then registration order for equal priorities:

```typescript
bus.use(validatorPlugin, { priority: 10 }); // runs first
bus.use(analyticsPlugin, { priority: 1 });  // runs after validation
bus.use(loggerPlugin);                       // priority 0 (default, runs last)
```

### Before Hooks

Run logic before a command reaches its handler. Throw to cancel — the dispatch returns `{ ok: false }`:

```typescript
// Global auth gate
bus.onBefore((cmd) => {
  if (!user.isAuth && protectedActions.includes(cmd.action)) {
    throw new Error('Unauthenticated');
  }
});

// Loading indicator
bus.onBefore(() => { isLoading.value = true; });
bus.onAfter(()  => { isLoading.value = false; });
```

On an async bus the hook can be async:
```typescript
asyncBus.onBefore(async (cmd) => {
  await rateLimiter.check(cmd.action);
});
```

### Wildcard Listeners

Subscribe to command patterns without being a handler:

```typescript
// All commands
bus.on('*', (cmd, result) => analytics.track(cmd.action));

// Prefix matching
bus.on('cart*', (cmd, result) => console.log('Cart event:', cmd.action));

// Exact match — fires once, then removes itself
bus.once('cartAdd', (cmd, result) => showConfetti());

// Remove all listeners for a pattern
bus.offAll('cart*');

// Remove all listeners
bus.offAll();
```

### Query (Read-Only Dispatch)

`query()` is like `dispatch()` but skips `onBefore` hooks — reads don't trigger mutation gates (auth checks, loading spinners, optimistic updates). Plugins and `onAfter` hooks still fire:

```typescript
// Register a handler that reads data
bus.register('getUser', (cmd) => db.users.find(cmd.target.id));

// Read-only — beforeHooks skipped, handler + plugins + afterHooks fire
const result = bus.query('getUser', { id: 42 });
```

This is the CQRS separation: `dispatch()` for commands (writes), `query()` for queries (reads).

### Domain Events (emit)

Fire an event that notifies `on()` listeners without requiring a handler and without returning a result. Use for observations (not commands):

```typescript
// Listen to domain events
bus.on('orderCreated', (cmd) => analytics.track('order', cmd.target));
bus.on('order*', (cmd) => audit.log(cmd.action, cmd.target));

// Fire — no handler needed, no result
bus.emit('orderCreated', { orderId: 42, total: 99.50 });
```

### Command Metadata

Every dispatched command is auto-stamped with `meta`:

```typescript
bus.onAfter((cmd) => {
  console.log(cmd.meta.id);             // unique id per dispatch (counter-based; UUID via configureUid)
  console.log(cmd.meta.ts);             // Date.now() timestamp
  console.log(cmd.meta.correlationId);  // trace ID for command chains
});

// Propagate tracing through payload
bus.dispatch('orderShip', order, {
  __correlationId: originalCommand.meta.id,
  __causationId: originalCommand.meta.id,
});
```

### Structured Errors (BusError)

Every error in vapor-chamber has a machine-readable code, severity, and emitter:

```typescript
import { BusError } from 'vapor-chamber';

const result = bus.dispatch('missing', {});
if (!result.ok && result.error instanceof BusError) {
  result.error.code;     // 'VC_CORE_NO_HANDLER'
  result.error.severity; // 'error'
  result.error.emitter;  // 'core'
  result.error.action;   // 'missing'
  result.error.context;  // { } — extra data (e.g. retryIn for throttle)
}
```

All codes: `VC_CORE_NO_HANDLER`, `VC_CORE_THROTTLED`, `VC_CORE_REQUEST_TIMEOUT`, `VC_PLUGIN_CIRCUIT_OPEN`, `VC_PLUGIN_RATE_LIMITED`, etc. Use `ERROR_CODE_REGISTRY` for a complete lookup table with fix suggestions.

### Bus Introspection

`inspectBus()` returns a full topology snapshot — useful for DevTools, debugging, and test assertions:

```typescript
import { inspectBus } from 'vapor-chamber';

const info = inspectBus(bus);
info.actions;          // ['cartAdd', 'cartRemove', ...]
info.undoActions;      // ['cartAdd'] — actions with registered undo handlers
info.pluginCount;      // 3
info.pluginPriorities; // [10, 5, 0]
info.sealed;           // false
info.dispatchDepth;    // 0 (increments during nested dispatch)
info.activeTimers;     // 0 (throttle timers currently running)
```

Tree-shakeable — not included in your bundle unless imported. Also available on TestBus: `bus.inspect()`.

### Utilities

```typescript
import { createChamber, createWorkflow, createReaction } from 'vapor-chamber';

// Group handlers under a namespace
const cart = createChamber('cart', { add: handleAdd, remove: handleRemove });
cart.install(bus); // Registers: cartAdd, cartRemove

// Saga: sequential steps with automatic compensation
const checkout = createWorkflow([
  { action: 'cartValidate' },
  { action: 'paymentReserve', compensate: 'paymentRelease' },
  { action: 'orderCreate',    compensate: 'orderCancel' },
]);
const result = await checkout.run(bus, { cartId }); // compensates on failure

// Declarative cross-domain reaction
createReaction('cartAdd', 'inventoryCheck', {
  when: (cmd, result) => result.ok,
  map: (cmd) => ({ itemId: cmd.payload.itemId }),
}).install(bus);
```

### Extra Plugins

```typescript
import { cache, circuitBreaker, rateLimit, metrics } from 'vapor-chamber';

bus.use(cache({ ttl: 60_000, actions: ['getUser*'] }));
bus.use(circuitBreaker({ threshold: 5, resetTimeout: 30_000 }));
bus.use(rateLimit({ max: 10, window: 1000 }));

const m = metrics();
bus.use(m);
console.log(m.summary()); // { cartAdd: { count: 42, avgMs: 1.2, errorRate: 0.02 } }
```

### supersede

Auto-cancels the previous in-flight dispatch for the same key when a new one starts — the stale request is genuinely aborted via `AbortController`, not just ignored on arrival. Ideal for type-ahead search, autosave, or any rapidly re-fired command where only the latest matters. Async plugin:

```typescript
import { createAsyncCommandBus, supersede } from 'vapor-chamber';

const bus = createAsyncCommandBus();

// Default key = commandKey(action, target) — the same default `idempotent`
// uses, so distinct actions never collide and are never silently dropped.
bus.use(supersede());

// Restrict to specific actions, or derive a custom key:
bus.use(supersede({
  actions: ['searchQuery', 'draftSave'],           // glob patterns; default: all
  key: (cmd) => `${cmd.action}:${cmd.target?.id ?? ''}`,  // return null/undefined to skip
}));
```

Because the bridges forward `cmd.signal` into their outbound `fetch`, a superseded HTTP request is cancelled at the network layer, not merely discarded.

### LLM Integration Schemas

```typescript
import { ERROR_CODE_REGISTRY, describeErrorCodes, busApiSchema } from 'vapor-chamber';

// Include in system prompt so LLMs know all error codes and fixes
const errorTable = describeErrorCodes();

// Include bus API schema so LLMs don't hallucinate methods
const apiSchema = busApiSchema();

// Lookup a specific error code
import { getErrorEntry } from 'vapor-chamber';
const entry = getErrorEntry('VC_CORE_NO_HANDLER');
console.log(entry?.fix); // "Register a handler with bus.register(action, handler)"
```

### Request / Response

Async request/response pattern with timeout:

```typescript
// Register a responder
bus.respond('get_auth_token', async (cmd) => {
  const response = await fetch('/api/token');
  return response.json();
});

// Request with timeout
const result = await bus.request('get_auth_token', { userId: 42 }, { timeout: 3000 });
```

Falls back to normal `dispatch()` if no responder is registered.

## Built-in Plugins

| Plugin | Description |
|--------|-------------|
| `logger(options?)` | Log commands to console |
| `validator(rules)` | Validate commands before execution |
| `history(options?)` | Track command history for undo/redo |
| `debounce(actions, wait)` | Delay execution until activity stops |
| `throttle(actions, wait)` | Limit execution frequency |
| `authGuard(options)` | Block protected commands when unauthenticated |
| `optimistic(handlers)` | Apply optimistic updates, rollback on failure |
| `optimisticUndo(bus, actions, opts?)` | Auto-rollback via registered undo handlers |
| `retry(options)` | Retry failed async dispatches with backoff |
| `persist(options)` | Auto-save state to localStorage after commands |
| `sync(options, bus?)` | Broadcast commands across browser tabs |

### logger

```typescript
bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('cart') }));
```

### validator

```typescript
bus.use(validator({
  'cartAdd': (cmd) => {
    if (!cmd.target?.id) return 'Product must have an ID';
    return null;  // null = valid
  }
}));
```

### history

```typescript
const historyPlugin = history({ maxSize: 100 });
bus.use(historyPlugin);

historyPlugin.undo();
historyPlugin.redo();
historyPlugin.getState(); // { past, future, canUndo, canRedo }
```

With bus-backed undo (executes inverse handlers):

```typescript
const historyPlugin = history({ maxSize: 100, bus });
bus.use(historyPlugin);

// If cartAdd was registered with { undo: fn }, calling undo() executes it
historyPlugin.undo();
```

### debounce

```typescript
bus.use(debounce(['searchQuery'], 300)); // wait 300ms after last call
```

### throttle

```typescript
bus.use(throttle(['uiScroll'], 100)); // max once per 100ms
```

### authGuard

```typescript
bus.use(authGuard({
  isAuthenticated: () => !!user.value,
  protected: ['shopCart', 'shopWishlist'],
  onUnauthenticated: (cmd) => router.push('/login'),
}));
```

### optimistic

```typescript
bus.use(optimistic({
  'cartAdd': {
    apply: (cmd) => {
      cartCount.value++;
      return () => { cartCount.value--; };  // rollback function
    }
  }
}));
```

### optimisticUndo

Automatic rollback using registered undo handlers. When a dispatch fails, executes the undo handler registered via `register(action, handler, { undo })`. Works on both sync and async buses:

```typescript
import { createAsyncCommandBus, optimisticUndo } from 'vapor-chamber';

const bus = createAsyncCommandBus();

// Register handler with undo
bus.register('cartAdd', async (cmd) => {
  return await api.addToCart(cmd.target);
}, {
  undo: (cmd) => api.removeFromCart(cmd.target.id),
});

// Install optimisticUndo — auto-rollback on failure
bus.use(optimisticUndo(bus, ['cartAdd'], {
  predict: (cmd) => ({ ...cart, items: [...cart.items, cmd.target] }),
  onRollback: (cmd, error) => toast.error(`Rolled back: ${error.message}`),
  onRollbackError: (cmd, undoErr, origErr) => console.error('Undo failed:', undoErr),
}));
```

### retry

Async plugin that retries failed dispatches with configurable backoff. Install on an `AsyncCommandBus`:

```typescript
import { createAsyncCommandBus, retry } from 'vapor-chamber';

const bus = createAsyncCommandBus();

// All actions, exponential backoff (default)
bus.use(retry({ maxAttempts: 3, baseDelay: 200 }));

// Only retry network actions, fixed delay
bus.use(retry({
  actions: ['api*'],
  maxAttempts: 5,
  baseDelay: 500,
  strategy: 'fixed',
  isRetryable: (err) => err.message !== 'Unauthorized',
}));
```

### persist

Auto-save state to localStorage after each successful command. Rehydrate on startup:

```typescript
import { persist } from 'vapor-chamber';

const cartPersist = persist({
  key: 'vc:cart',
  getState: () => cartState.value,
});
bus.use(cartPersist);

// On app start — rehydrate before rendering
const saved = cartPersist.load();
if (saved) cartState.value = saved;

// Manual operations
cartPersist.save();   // force save now
cartPersist.clear();  // remove from storage

// Custom backend (sessionStorage, IndexedDB adapter, etc.)
bus.use(persist({ key: 'vc:cart', getState, storage: sessionStorage }));
```

### sync

Broadcast successful commands to all other open tabs via `BroadcastChannel`:

```typescript
import { sync } from 'vapor-chamber';

const tabSync = sync(
  {
    channel: 'vapor-chamber:app',
    filter: (cmd) => cmd.action.startsWith('cart') || cmd.action.startsWith('auth'),
  },
  bus // pass the bus so received messages are re-dispatched locally
);

bus.use(tabSync);

// Teardown (component unmount, app destroy)
tabSync.close();
tabSync.isOpen(); // → false
```

## Transport Layer

Send commands to a backend over HTTP, WebSocket, or SSE. Import from `vapor-chamber/transports`
or directly from `vapor-chamber`:

### createHttpBridge

Async plugin that POSTs command envelopes to a backend endpoint. Unhandled commands (no local handler) fall through to the server:

```typescript
import { createAsyncCommandBus } from 'vapor-chamber';
import { createHttpBridge } from 'vapor-chamber/transports';

const bus = createAsyncCommandBus({ onMissing: 'ignore' });

bus.use(createHttpBridge({
  endpoint: '/api/commands',
  csrf: true,                                    // reads XSRF-TOKEN cookie / meta tag automatically
  csrfCookieUrl: '/sanctum/csrf-cookie',         // default; set '' to disable the refresh fetch
  retry: 2,                                      // retry up to 2 times on 5xx / 429 / 408
  noRetry: ['paymentCharge', 'orderPlace'],      // never retry non-idempotent commands
  timeout: 8000,                                 // ms
  actions: ['order*'],                           // only forward order* actions; others stay local
}));

const result = await bus.dispatch('orderCreate', { items: cart });
// → POST /api/commands  { command: 'orderCreate', target: { items: ... } }
```

**Vapor lifecycle integration** — cancel in-flight requests when a component is disposed:

```typescript
// In <script setup vapor>:
const ctrl = new AbortController();
onScopeDispose(() => ctrl.abort());

bus.use(createHttpBridge({
  endpoint: '/api/vc',
  scopeController: ctrl,  // all requests cancelled on scope disposal
}));
```

The backend response shape:
```json
{ "state": { "orderId": 42, "status": "pending" } }
```
`result.value` will be the contents of `state`.

### createBatchingHttpBridge

Same backend contract as `createHttpBridge` — CSRF, retry, timeout, and session-expiry all reuse the same request path — but every command dispatched within a batching window is coalesced into a single `POST`, then matched back to each caller by id. The coalescing is invisible to the call site: each `dispatch()` still resolves with its own result.

```typescript
import { createAsyncCommandBus } from 'vapor-chamber';
import { createBatchingHttpBridge } from 'vapor-chamber/transports';

const bus = createAsyncCommandBus();

bus.use(createBatchingHttpBridge({
  endpoint: '/api/vc/batch',
  csrf: true,
  window: 'microtask',   // default: coalesce every dispatch in the same tick — zero added latency
  // window: 20,          // or hold the queue open N ms to also catch separate ticks
}));

// Two dispatches issued in the same tick → ONE HTTP round trip:
bus.dispatch('formSet', { field: 'email' }, { value: 'a@b.com' });
bus.dispatch('cartAdd', product, { quantity: 2 });
```

Request / response shape (matched by `id`):
```json
// → POST /api/vc/batch
{ "commands": [{ "id": "c1", "command": "formSet", "target": { "field": "email" }, "payload": { "value": "a@b.com" } }] }
// ←
{ "results": [{ "id": "c1", "ok": true, "state": {} }] }
```

### createWsBridge

WebSocket transport with auto-reconnect:

```typescript
import { createWsBridge } from 'vapor-chamber/transports';

const ws = createWsBridge({
  url: 'wss://api.example.com/commands',
  actions: ['chat*', 'presence*'],
  timeout: 10_000,       // per-message response timeout, ms (default: 10_000)
  maxQueueSize: 100,     // max queued messages during disconnect (default: 100)
  reconnect: true,       // auto-reconnect on close (default: true)
  maxReconnects: 10,     // give up after N reconnect attempts (default: 10)
});
bus.use(ws);
ws.connect();

// Lifecycle
ws.isConnected();     // → boolean (imperative check)
ws.connected.value;   // → boolean (reactive signal — bindable in templates)
ws.disconnect();      // intentional close — suppresses reconnect
```

The `connected` signal is reactive — bind it directly in Vapor or VDOM templates without polling:

```vue
<template>
  <span v-if="ws.connected.value">🟢 Connected</span>
  <span v-else>🔴 Disconnected</span>
</template>
```

### createSseBridge

Server-sent events — server pushes commands to the client:

```typescript
import { createSseBridge } from 'vapor-chamber/transports';

bus.use(createSseBridge({
  url: '/api/events',
}));
```

### createEchoBridge

Laravel Echo / Reverb realtime — subscribe public/private/presence channels and
route each broadcast to `bus.emit()`. You pass your own Echo instance, so the
library never imports `laravel-echo`:

```typescript
import { createEchoBridge } from 'vapor-chamber/transports';

const realtime = createEchoBridge({
  echo, // your configured laravel-echo / Reverb instance
  channels: [
    { name: `user.${userId}`, type: 'private',  events: ['OrderShipped'] },
    { name: 'lobby',          type: 'presence', events: ['MessagePosted'] },
  ],
});
realtime.install(bus);   // OrderShipped → bus.emit('OrderShipped', payload)
// realtime.teardown();  // on unmount / route change
```

## HTTP Client

Two levels, same underlying retry/timeout/CSRF machinery (`src/http.ts`):

- **`postCommand`** — the single-purpose POST helper `createHttpBridge` builds
  on. Use it directly when you need one-off HTTP control outside the
  transport plugin.
- **`createHttpClient`** — a full multi-method client (GET/POST/PUT/PATCH/
  DELETE) with interceptors, LRU caching, request dedup, safe mode, and file
  download — for any HTTP need in an app already using vapor-chamber,
  command-bus or not.

```typescript
import { postCommand } from 'vapor-chamber';

const response = await postCommand('/api/commands', {
  command: 'cartAdd',
  target: product,
  payload: { quantity: 2 },
}, {
  csrf: true,
  timeout: 5000,
  retry: 2,
  onSessionExpired: (status) => router.push('/login'),
});
```

### createHttpClient

```typescript
import { createHttpClient } from 'vapor-chamber';

const http = createHttpClient({ baseURL: '/api', csrf: true });

const users = await http.get('/users', { params: { page: 1 } });
await http.post('/cart', { itemId: 1, qty: 2 });
await http.put('/cart/1', { qty: 5 });
await http.delete('/cart/1');

// Safe mode — never throws
const result = await http.safe.post('/login', credentials);
if (result.error) console.log(result.error.message);

// File download
await http.download('/export/csv', 'products.csv');

// Interceptors
http.interceptors.request.use((config) => {
  config.headers = { ...config.headers, 'X-Custom': '1' };
  return config;
});

// Scoped instance sharing the same interceptors
const adminHttp = http.create({ baseURL: '/admin/api', headers: { 'X-Admin': 'true' } });
```

Retry (`retry`, default 2 for GET/0 for mutations) covers 5xx/429/408 *and*
timeouts — a timeout competes for the same retry budget as a bad response
instead of always failing on the first attempt. `TimeoutError` is distinct
from a caller-triggered `AbortError` (`isCancel`-style checks are unaffected
by retries).

**Caching (GET only)** — `cache: true` for a flat TTL, or an object for more:

```typescript
// Stale-while-revalidate: a hit past `ttl` but inside `ttl + staleTtl` is
// served instantly (`stale: true`) while a background fetch refreshes it.
const res = await http.get('/dashboard/stats', { cache: { ttl: 30_000, staleTtl: 5 * 60_000 } });
if (res.revalidation) {
  const fresh = await res.revalidation; // push into your own state when it lands
}

// serveStaleOnError: a *transient* failure (timeout/network/5xx — see
// classifyError) with a retained entry — even past its stale window —
// resolves instead of rejecting, e.g. { data, stale: true, servedOnError: true, error }.
// Business errors (4xx) are never masked this way.
await http.get('/dashboard/stats', { cache: { ttl: 30_000, serveStaleOnError: true } });
```

**`silent`** — per-request opt-out for a host-provided global error handler
(fire-and-forget telemetry, background prefetch shouldn't surface UI noise):

```typescript
await http.post('/analytics/beacon', payload, { silent: true }).catch((e) => {
  e.silent; // true — a global handler can check this and skip the toast
});
```

`classifyError(error)` (from `vapor-chamber/http-errors` or the `http.ts`
module) is the one named transience rule behind `serveStaleOnError`:
`transient = timeout || no response || status >= 500` — a 4xx is always a
business error, never transient, no matter how tempting it is to retry a
flaky-looking 429.

## Streaming JSON Parser

`vapor-chamber/stream-parser` — a dependency-free incremental JSON parser for
progressively consuming a streamed `fetch()`/SSE response body without
buffering the whole payload (LLM/AI streaming completions, large exports).
Subpath-only; adds nothing to the IIFE bundles.

```typescript
import { createStreamParser } from 'vapor-chamber/stream-parser';

const parser = createStreamParser({
  onValue: (key, value, path) => console.log(key, value, path),
});
const response = await fetch('/api/stream');
await parser.stream(response);
```

## Batch Dispatch

Dispatch multiple commands as a unit. Stops on the first failure by default:

```typescript
const result = bus.dispatchBatch([
  { action: 'cartAdd',       target: cart, payload: item },
  { action: 'totalsUpdate',  target: cart },
  { action: 'telemetryLog',  target: session, payload: item },
]);

if (result.ok) {
  console.log('All succeeded:', result.results);
} else {
  console.error('Stopped at failure:', result.error);
}
```

Use `continueOnError` to run all commands regardless of failures, then check counts:

```typescript
const result = bus.dispatchBatch(commands, { continueOnError: true });
console.log(`${result.successCount} of ${result.results.length} succeeded`);
// result.failCount — how many failed
// result.results   — all CommandResult objects, in order
```

### Transactional Batch

Use `transactional: true` for all-or-nothing batch execution. If any command fails, all previously successful commands are rolled back using their registered undo handlers:

```typescript
bus.register('inventoryReserve', reserveHandler, { undo: releaseHandler });
bus.register('paymentCharge', chargeHandler, { undo: refundHandler });
bus.register('orderCreate', createHandler, { undo: cancelHandler });

const result = bus.dispatchBatch([
  { action: 'inventoryReserve', target: item },
  { action: 'paymentCharge',    target: payment },
  { action: 'orderCreate',      target: order },
], { transactional: true });

if (!result.ok) {
  // paymentCharge failed → inventoryReserve was rolled back
  console.log('Rollbacks:', result.rollbacks); // compensation results
}
```

## Dead Letter Handling

Configure what happens when a command has no registered handler:

```typescript
createCommandBus()                                    // default: returns { ok: false, error }
createCommandBus({ onMissing: 'throw' })              // throws the error
createCommandBus({ onMissing: 'ignore' })             // returns { ok: true, value: undefined }
createCommandBus({ onMissing: (cmd) => { ... } })     // custom fallback
```

## Async Command Bus

For async handlers (API calls, IndexedDB, etc.):

```typescript
import { createAsyncCommandBus } from 'vapor-chamber';

const bus = createAsyncCommandBus();

bus.register('userFetch', async (cmd) => {
  const response = await fetch(`/api/users/${cmd.target.id}`);
  return response.json();
});

const result = await bus.dispatch('userFetch', { id: 123 });
```

## Vapor Composables

### useCommand

The single command composable — reactive `loading`/`lastError` signals plus `register()`, `on()`, and `emit()` with automatic cleanup. It uses no `getCurrentInstance()`, so it is Vapor-safe: the same API works in `<script setup vapor>` and VDOM components alike.

```vue
<script setup vapor>
import { useCommand } from 'vapor-chamber';

const { dispatch, loading, lastError } = useCommand();
</script>

<template>
  <button @click="dispatch('save', doc)" :disabled="loading.value">Save</button>
  <p v-if="lastError.value">{{ lastError.value.message }}</p>
</template>
```

Need the full surface? `register()`, `on()`, `emit()`, and `dispose()` are part of the same composable:

```vue
<script setup vapor>
import { useCommand } from 'vapor-chamber';

const { dispatch, register, on, emit, loading, lastError, dispose } = useCommand();

// Register a handler scoped to this component
register('cartAdd', (cmd) => addToCart(cmd.target));

// Listen to patterns
on('cart*', (cmd, result) => console.log('Cart event:', cmd.action));

// Dispatch with reactive loading/error tracking
const result = dispatch('cartAdd', product, { quantity: 1 });

// Emit a domain event (no handler required, no result)
emit('cartChanged', { count: 1 });

// Auto-cleanup via onScopeDispose — or call dispose() manually
</script>
```

### defineVaporCommand

Zero-overhead dispatch for hot paths — no reactive `loading`/`lastError` signals created.
Ideal for fire-and-forget patterns: telemetry events, scroll-position sampling,
debounced search, autosave — anywhere reactive loading state would be wasted overhead:

```vue
<script setup vapor>
import { defineVaporCommand } from 'vapor-chamber';

const { dispatch } = defineVaporCommand('telemetryEvent', (cmd) => {
  // forward to whatever metrics / analytics SDK you use
  sendMetric(cmd.target.name, cmd.target.params);
});

// Fire-and-forget — no reactive overhead in the alien-signals graph
dispatch({ event: 'page_view', params: { page: '/shop' } });
</script>
```

### useCommandState

State managed by commands:

```vue
<script setup vapor>
import { useCommandState } from 'vapor-chamber';

const { state: cart } = useCommandState(
  { items: [], total: 0 },
  {
    'cartAdd': (state, cmd) => ({
      items: [...state.items, cmd.target],
      total: state.total + cmd.target.price
    })
  }
);
</script>
```

### useCommandHistory

Reactive undo/redo:

```vue
<script setup vapor>
import { useCommandHistory } from 'vapor-chamber';

const { canUndo, canRedo, undo, redo } = useCommandHistory({
  filter: (cmd) => cmd.action.startsWith('editor_')
});
</script>
```

### useCommandGroup

Namespace isolation for large apps and multi-team projects. All calls are automatically prefixed in camelCase — prevents action name collisions when composing multiple feature modules:

```typescript
import { useCommandGroup } from 'vapor-chamber';

// Cart feature module
const cart = useCommandGroup('cart');
cart.register('add', handler);        // registers 'cartAdd'
cart.dispatch('add', product);        // dispatches 'cartAdd'
cart.on('*', listener);               // listens to 'cart*'

// Orders feature — completely isolated
const orders = useCommandGroup('orders');
orders.dispatch('cancel', { id });    // dispatches 'ordersCancel'

// Access the namespace
cart.namespace;  // → 'cart'
```

Auto-cleanup on Vue scope disposal. `dispose()` is also available for manual teardown.

### useCommandError

Component-scoped error boundary. Reactively captures all failed command results:

```typescript
import { useCommandError } from 'vapor-chamber';

// Watch all failed commands
const { errors, latestError, clearErrors } = useCommandError();

// Narrow to a subset
const { latestError } = useCommandError({
  filter: (cmd) => cmd.action.startsWith('cart'),
});

// In template
// latestError.value?.message
// errors.value.length
```

### createFormBus

Reactive form state manager built on the command bus. Per-field validation, dirty tracking, and full plugin pipeline on every form command:

```typescript
import { createFormBus, logger } from 'vapor-chamber';

const form = createFormBus({
  fields: { email: '', password: '' },
  rules: {
    // Sync rule — runs on every set() for live feedback
    email:    (v) => v.includes('@') ? null : 'Invalid email',
    password: (v) => v.length >= 8   ? null : 'Too short',
    // Async rule — only awaited on submit() (no UI jank during typing)
    username: async (v) => {
      const taken = await api.isUsernameTaken(v);
      return taken ? 'Username already taken' : null;
    },
  },
  onSubmit: async (values) => await api.login(values),
});

// Attach plugins — logger, throttle, authGuard, etc.
form.use(logger());

// Reactive state
form.values.value        // { email: '', password: '' }
form.errors.value        // { email: 'Invalid email', ... }
form.isDirty.value       // true when any field has changed
form.isValid.value       // true when no errors
form.isSubmitting.value  // true while onSubmit is in flight

// Actions
form.set('email', 'user@example.com');  // updates field + re-runs validation
form.touch('email');                     // marks field as interacted with
await form.submit();                     // validate → onSubmit → returns bool
form.reset();                            // restore initial values
```

**Headless mode** — skip reactive signal allocations for server-side, batch, or non-UI use:

```typescript
const form = createFormBus({
  fields: { email: '', password: '' },
  rules: { email: (v) => v.includes('@') ? null : 'Invalid email' },
  onSubmit: async (values) => await api.login(values),
  reactive: false,  // no Vue signals — plain get/set wrappers
});
// All APIs work identically — values, errors, isDirty, etc. are still readable
```

Template usage (Vue 3):

```vue
<input :value="form.values.value.email"
       @input="form.set('email', $event.target.value)"
       @blur="form.touch('email')" />
<span v-if="form.touched.value.email && form.errors.value.email">
  {{ form.errors.value.email }}
</span>
<button :disabled="!form.isValid.value || form.isSubmitting.value"
        @click="form.submit()">
  Submit
</button>
```

### useCommandBus

Lightweight access to the shared bus — tree-shakeable:

```typescript
import { useCommandBus } from 'vapor-chamber';

const bus = useCommandBus();
bus.dispatch('cartAdd', product, { quantity: 1 });
```

Use `useCommand()` when you need reactive `loading`/`lastError` signals. Use `defineVaporCommand()` for zero-overhead hot paths. Use `useCommandBus()` when you just need to dispatch.

### configureSignal

Inject a custom signal factory. In Vue 3.6+, `ref()` is auto-detected and backed by alien-signals — calling `configureSignal` is only needed for custom signal implementations:

```typescript
import { ref } from 'vue';
import { configureSignal } from 'vapor-chamber';

configureSignal(ref); // explicit — usually auto-detected
```

### Testing

`createTestBus()` records all dispatched commands without executing real handlers:

```typescript
import { createTestBus, setCommandBus, resetCommandBus } from 'vapor-chamber';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';

describe('CartButton', () => {
  let bus: TestBus;

  beforeEach(() => {
    bus = createTestBus();
    setCommandBus(bus);
  });

  afterEach(() => {
    resetCommandBus();
  });

  it('dispatches cartAdd on click', () => {
    // ... render component, click button ...
    expect(bus.wasDispatched('cartAdd')).toBe(true);
    expect(bus.getDispatched('cartAdd')[0].cmd.payload).toEqual({ quantity: 1 });
  });
});
```

**Snapshot & time-travel** — replay command sequences for debugging or testing:

```typescript
const bus = createTestBus();

bus.dispatch('login', user);
bus.dispatch('cartAdd', product, { quantity: 1 });
bus.dispatch('cartAdd', product2, { quantity: 2 });
bus.dispatch('checkout', cart);

// Immutable snapshot — mutations don't affect bus.recorded
const snap = bus.snapshot(); // → RecordedDispatch[]

// Commands 0..N inclusive (returns Command[])
bus.travelTo(1);                        // → [login, cartAdd]

// All commands up to last occurrence of 'cartAdd'
bus.travelToAction('cartAdd');          // → [login, cartAdd, cartAdd]

// Out-of-range indices are clamped
bus.travelTo(999);                      // → full history
```

### setupDevtools

Connect a bus to Vue DevTools. Adds a **Commands** timeline layer and a **Vapor Chamber** inspector panel. Requires `@vue/devtools-api` — silently no-ops if not installed:

```typescript
import { createApp } from 'vue';
import { getCommandBus } from 'vapor-chamber';
import { setupDevtools } from 'vapor-chamber/devtools';   // opt-in subpath

const app = createApp(App);
setupDevtools(getCommandBus(), app);
app.mount('#app');
```

## Examples

See [`examples/`](https://github.com/lucianofedericopereira/vapor-chamber/tree/main/examples) — the [examples index](https://github.com/lucianofedericopereira/vapor-chamber/tree/main/examples) lists all of them. **Runnable full-project apps:**

| App | What it shows |
|-----|---------------|
| [`vapor-sfc`](https://github.com/lucianofedericopereira/vapor-chamber/tree/main/examples/vapor-sfc) | `<script setup vapor>` SFC tree — `useCommand` / `defineVaporCommand` / `useSharedCommandState` |
| [`vapor-island-cart`](https://github.com/lucianofedericopereira/vapor-chamber/tree/main/examples/vapor-island-cart) | Light-DOM Vapor custom-element islands coordinating through one bus |
| [`exo-astro`](https://github.com/lucianofedericopereira/vapor-chamber/tree/main/examples/exo-astro) | Declarative directives for Astro — dispatch *before* hydration |
| [`laravel-app`](https://github.com/lucianofedericopereira/vapor-chamber/tree/main/examples/laravel-app) | Verified Laravel app (13.x): Blade + core IIFE + real CSRF (419/401) |

**Single-file snippets** (plus `feature-*` / `pattern-*` files — see the [index](https://github.com/lucianofedericopereira/vapor-chamber/tree/main/examples)):

| Example | Description |
|---------|-------------|
| [`shopping-cart.ts`](https://github.com/lucianofedericopereira/vapor-chamber/blob/main/examples/shopping-cart.ts) | Cart with validation, history, and undo/redo |
| [`form-validation.ts`](https://github.com/lucianofedericopereira/vapor-chamber/blob/main/examples/form-validation.ts) | Form validation with error handling |
| [`async-api.ts`](https://github.com/lucianofedericopereira/vapor-chamber/blob/main/examples/async-api.ts) | Async handlers with retry plugin |
| [`realtime-search.ts`](https://github.com/lucianofedericopereira/vapor-chamber/blob/main/examples/realtime-search.ts) | Debounced search queries |
| [`custom-plugins.ts`](https://github.com/lucianofedericopereira/vapor-chamber/blob/main/examples/custom-plugins.ts) | Analytics, auth guard, rate limiter plugins |
| [`vue-vapor-component.vue`](https://github.com/lucianofedericopereira/vapor-chamber/blob/main/examples/vue-vapor-component.vue) | Full Vue Vapor todo app |

## API Reference

### Core

| Function | Description |
|----------|-------------|
| `createCommandBus(options?)` | Create a synchronous command bus |
| `createAsyncCommandBus(options?)` | Create an async command bus |
| `createTestBus(options?)` | Create a test bus that records dispatches |
| `inspectBus(bus)` | Returns `BusInspection` snapshot of bus topology (tree-shakeable) |
| `unsealBus(bus)` | Unseal a sealed bus (tree-shakeable escape hatch) |
| `createCommandPool(size)` | Pre-allocated Command object pool for hot paths |
| `commandKey(action, target)` | Stable `action:target` string key for cache integration |

**`CommandBusOptions`**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `onMissing` | `'error' \| 'throw' \| 'ignore' \| fn` | `'error'` | Behavior when no handler is registered |
| `naming` | `{ pattern: RegExp, onViolation?: string }` | — | Enforce naming convention on actions |

### Command Bus Methods

| Method | Description |
|--------|-------------|
| `dispatch(action, target, payload?)` | Execute a command (write). Auto-stamps `cmd.meta` |
| `query(action, target, payload?)` | Read-only dispatch — skips `onBefore` hooks, runs plugins + handler + afterHooks |
| `emit(event, data?)` | Fire a domain event — notifies `on()` listeners, no handler required |
| `dispatchBatch(commands[], options?)` | Execute multiple commands. Returns `{ successCount, failCount, results }` |
| `register(action, handler, options?)` | Register a handler. Options: `{ undo?, throttle? }` |
| `use(plugin, options?)` | Add a plugin. `options.priority` controls order |
| `onBefore(hook)` | Run hook before every command. Throw to cancel dispatch. |
| `onAfter(hook)` | Run hook after every command |
| `on(pattern, listener)` | Subscribe to commands matching a pattern (`*`, `prefix*`, exact). Returns unsub. |
| `once(pattern, listener)` | Like `on()` but auto-unsubscribes after first match |
| `offAll(pattern?)` | Remove all listeners for a pattern, or all listeners if omitted |
| `request(action, target, payload?, options?)` | Async request/response with timeout (default 5s) |
| `respond(action, handler)` | Register a responder for `request()` calls |
| `hasHandler(action)` | Returns true if a handler is registered for the action |
| `registeredActions()` | Returns `string[]` of all registered action names |
| `clear()` | Remove all handlers, plugins, hooks, and listeners |
| `seal()` | Freeze bus configuration — rejects register/use/clear after sealing |
| `dispose()` | Clean teardown — clears state, cancels timers, marks bus as disposed |
| `getUndoHandler(action)` | Get the undo handler for an action (`@internal`) |

### Composables

| Composable | Description |
|------------|-------------|
| `useCommand()` | Vapor-safe composable: dispatch + register/on/emit + reactive loading/error state (per-call signals), auto-cleanup on scope disposal |
| `useSharedCommandState(options?)` | Aggregate `isAnyLoading` + `errors` ring buffer, **shared** across every subscriber on the same bus. Use for toolbars, status bars, global spinners. Saves ~2 signal nodes per subscribing component. |
| `defineVaporCommand(action, handler, options?)` | Zero-overhead dispatch for hot paths |
| `useCommandState(initial, handlers)` | State managed by commands |
| `useCommandHistory(options?)` | Reactive undo/redo |
| `useCommandGroup(namespace)` | Namespace isolation — prefixes all calls in camelCase |
| `useCommandError(options?)` | Reactive error boundary for failed dispatches |
| `useCommandBus()` | Get shared bus instance |
| `getCommandBus()` | Get shared bus instance (non-composable) |
| `setCommandBus(bus)` | Set shared bus instance |
| `resetCommandBus()` | Reset shared bus to null (useful in tests) |
| `configureSignal(fn)` | Inject a custom signal factory |
| `isVaporAvailable()` | Returns true if Vue 3.6+ Vapor mode is detected |
| `createVaporChamberApp(component, props?)` | Create a Vapor app instance (requires Vue 3.6+) |
| `getVaporInteropPlugin()` | Returns `vaporInteropPlugin` for mixed trees |
| `setupDevtools(bus, app)` | Connect bus to Vue DevTools — import from `vapor-chamber/devtools`, needs the `@vue/devtools-api` peer |

## Roadmap

The full feature matrix (per-module status, versions, and test coverage) and the
forward plan live in [ROADMAP.md](https://github.com/lucianofedericopereira/vapor-chamber/blob/main/ROADMAP.md)
— the single source of truth, shipped with the npm package.

## Documentation

See [`docs/whitepaper.md`](https://github.com/lucianofedericopereira/vapor-chamber/blob/main/docs/whitepaper.md) for design philosophy, architecture, camelCase naming rationale, Vue 3.6 Vapor alignment, SSR guide, and migration strategy.

## Design Goals

1. **Minimal** — ~3.6 KB brotli core (measured: [docs/BUNDLE-SIZES.md](docs/BUNDLE-SIZES.md)); the core has zero runtime dependencies — `alien-signals` is opt-in and never auto-bundled
2. **Vapor-native** — Built for signals, not VDOM
3. **Composable** — Plugins for everything
4. **Type-safe** — Full TypeScript support
5. **Predictable** — Sync by default, explicit async
6. **Progressive** — Works in VDOM, Vapor, and mixed trees

## License

[GNU Lesser General Public License v2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html)
