vapor-chamber
v0.1.0
Published
Lightweight command bus for Vue Vapor - plugins, hooks, undo/redo
Maintainers
Readme
Vapor Chamber
A lightweight command bus designed for Vue Vapor. ~1KB core.
What is Vue Vapor?
Vue Vapor is Vue's upcoming 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.
Vapor Chamber embraces this philosophy: minimal abstraction, direct updates, signal-native reactivity.
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('cart.add', product)
Component B listens... ↓
Component C also listens... Handler executes once
Who handles what? When? Plugins observe/modify
Result returnedBenefits:
- Semantic actions -
cart.addis clearer thanemit('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
Install
npm install vapor-chamberQuick Start
import { createCommandBus, logger, validator } from 'vapor-chamber';
const bus = createCommandBus();
// Add plugins
bus.use(logger());
bus.use(validator({
'cart.add': (cmd) => cmd.payload?.quantity > 0 ? null : 'Quantity required'
}));
// Register handler
bus.register('cart.add', (cmd) => {
cart.items.push({ ...cmd.target, quantity: cmd.payload.quantity });
return cart.items;
});
// Dispatch
const result = bus.dispatch('cart.add', product, { quantity: 2 });
if (result.ok) {
console.log('Added:', result.value);
} else {
console.error('Failed:', result.error);
}Core Concepts
Commands
A command has three parts:
bus.dispatch(
'cart.add', // action - what to do
product, // target - what to act on
{ quantity: 2 } // payload - additional data (optional)
);Handlers
One handler per action. Returns a value or throws:
bus.register('cart.add', (cmd) => {
// cmd.action = 'cart.add'
// cmd.target = product
// cmd.payload = { quantity: 2 }
cart.items.push(cmd.target);
return cart.items; // becomes result.value
});Results
Every dispatch returns a result:
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:
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 in order: first added = outermost wrapper.
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 |
logger
bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('cart.') }));validator
bus.use(validator({
'cart.add': (cmd) => {
if (!cmd.target?.id) return 'Product must have an ID';
return null; // null = valid
}
}));history
const historyPlugin = history({ maxSize: 100 });
bus.use(historyPlugin);
historyPlugin.undo();
historyPlugin.redo();
historyPlugin.getState(); // { past, future, canUndo, canRedo }debounce
bus.use(debounce(['search.query'], 300)); // wait 300ms after last callthrottle
bus.use(throttle(['ui.scroll'], 100)); // max once per 100msAsync Command Bus
For async handlers (API calls, IndexedDB, etc.):
import { createAsyncCommandBus } from 'vapor-chamber';
const bus = createAsyncCommandBus();
bus.register('user.fetch', async (cmd) => {
const response = await fetch(`/api/users/${cmd.target.id}`);
return response.json();
});
const result = await bus.dispatch('user.fetch', { id: 123 });Vapor Composables
For Vue Vapor components:
useCommand
<script setup>
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>useCommandState
<script setup>
import { useCommandState } from 'vapor-chamber';
const { state: cart } = useCommandState(
{ items: [], total: 0 },
{
'cart.add': (state, cmd) => ({
items: [...state.items, cmd.target],
total: state.total + cmd.target.price
})
}
);
</script>useCommandHistory
<script setup>
import { useCommandHistory } from 'vapor-chamber';
const { canUndo, canRedo, undo, redo } = useCommandHistory({
filter: (cmd) => cmd.action.startsWith('editor.')
});
</script>Examples
See the examples/ folder for complete, runnable examples:
| Example | Description |
|---------|-------------|
| shopping-cart.ts | Cart with validation, history, and undo/redo |
| form-validation.ts | Form validation with error handling |
| async-api.ts | Async handlers with retry plugin |
| realtime-search.ts | Debounced search queries |
| custom-plugins.ts | Analytics, auth guard, rate limiter plugins |
| vue-vapor-component.vue | Full Vue Vapor todo app |
Run TypeScript examples with:
npx ts-node examples/shopping-cart.tsAPI Reference
Core
| Function | Description |
|----------|-------------|
| createCommandBus() | Create a synchronous command bus |
| createAsyncCommandBus() | Create an async command bus |
Command Bus Methods
| Method | Description |
|--------|-------------|
| dispatch(action, target, payload?) | Execute a command |
| register(action, handler) | Register a handler (returns unregister fn) |
| use(plugin) | Add a plugin (returns unsubscribe fn) |
| onAfter(hook) | Run callback after every command |
Composables
| Composable | Description |
|------------|-------------|
| useCommand() | Dispatch with reactive loading/error state |
| useCommandState(initial, handlers) | State managed by commands |
| useCommandHistory(options?) | Reactive undo/redo |
| getCommandBus() | Get shared bus instance |
| setCommandBus(bus) | Set shared bus instance |
Documentation
See the docs/ folder for detailed documentation:
- Whitepaper - Design philosophy and architecture
Design Goals
- Minimal - ~1KB core, no dependencies
- Vapor-native - Built for signals, not VDOM
- Composable - Plugins for everything
- Type-safe - Full TypeScript support
- Predictable - Sync by default, explicit async
