vapor-chamber 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +859 -70
  2. package/dist/chamber-vapor.d.ts +72 -0
  3. package/dist/chamber-vapor.d.ts.map +1 -0
  4. package/dist/chamber-vapor.js +112 -0
  5. package/dist/chamber.d.ts +78 -51
  6. package/dist/chamber.d.ts.map +1 -1
  7. package/dist/chamber.js +168 -118
  8. package/dist/command-bus.d.ts +389 -27
  9. package/dist/command-bus.d.ts.map +1 -1
  10. package/dist/command-bus.js +896 -251
  11. package/dist/directives.d.ts +37 -0
  12. package/dist/directives.d.ts.map +1 -0
  13. package/dist/directives.js +223 -0
  14. package/dist/form.d.ts +83 -0
  15. package/dist/form.d.ts.map +1 -0
  16. package/dist/form.js +184 -0
  17. package/dist/http.d.ts +60 -0
  18. package/dist/http.d.ts.map +1 -0
  19. package/dist/http.js +251 -0
  20. package/dist/iife.d.ts +95 -0
  21. package/dist/iife.d.ts.map +1 -0
  22. package/dist/iife.js +90 -0
  23. package/dist/index.d.ts +58 -5
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +82 -10
  26. package/dist/plugins-core.d.ts +127 -0
  27. package/dist/plugins-core.d.ts.map +1 -0
  28. package/dist/plugins-core.js +316 -0
  29. package/dist/plugins-extra.d.ts +116 -0
  30. package/dist/plugins-extra.d.ts.map +1 -0
  31. package/dist/plugins-extra.js +275 -0
  32. package/dist/plugins-io.d.ts +100 -0
  33. package/dist/plugins-io.d.ts.map +1 -0
  34. package/dist/plugins-io.js +171 -0
  35. package/dist/plugins.d.ts +6 -76
  36. package/dist/plugins.d.ts.map +1 -1
  37. package/dist/plugins.js +6 -211
  38. package/dist/schema.d.ts +240 -0
  39. package/dist/schema.d.ts.map +1 -0
  40. package/dist/schema.js +399 -0
  41. package/dist/testing.d.ts +39 -6
  42. package/dist/testing.d.ts.map +1 -1
  43. package/dist/testing.js +184 -29
  44. package/dist/transports.d.ts +184 -0
  45. package/dist/transports.d.ts.map +1 -0
  46. package/dist/transports.js +249 -0
  47. package/dist/utilities.d.ts +100 -0
  48. package/dist/utilities.d.ts.map +1 -0
  49. package/dist/utilities.js +119 -0
  50. package/dist/vapor-chamber.iife.js +1678 -0
  51. package/dist/vapor-chamber.iife.js.map +7 -0
  52. package/dist/vapor-chamber.iife.min.js +2 -0
  53. package/dist/vite-hmr.d.ts +50 -0
  54. package/dist/vite-hmr.d.ts.map +1 -0
  55. package/dist/vite-hmr.js +112 -0
  56. package/package.json +24 -4
  57. package/scripts/build-iife.mjs +47 -0
package/README.md CHANGED
@@ -6,6 +6,32 @@
6
6
  A lightweight command bus designed for <a href="https://github.com/vuejs/vue-vapor">Vue Vapor</a>. ~2KB gzipped. Vue 3.6 Vapor aligned. Optional DevTools integration.
7
7
  </p>
8
8
 
9
+ ## What is Vapor Chamber?
10
+
11
+ Vapor Chamber is a **command bus for Vue 3.6+ Vapor mode**. It gives every user action a single handler, a composable plugin pipeline, and signal-native reactive state — replacing scattered event listeners and prop-drilling with one predictable, testable flow.
12
+
13
+ ```ts
14
+ import { createCommandBus, useCommand } from 'vapor-chamber';
15
+
16
+ const bus = createCommandBus();
17
+
18
+ bus.register('cartAdd', (cmd) => addToCart(cmd.target));
19
+ bus.use(logger());
20
+ bus.use(validator({ cartAdd: (cmd) => cmd.target.id ? null : 'Missing ID' }));
21
+
22
+ // In a component
23
+ const { dispatch, loading, lastError } = useCommand('cartAdd');
24
+ ```
25
+
26
+ - **~2 KB gzipped** — zero runtime dependencies
27
+ - **Framework-agnostic core** — the bus itself has no Vue import
28
+ - **Vue 3.6 Vapor aligned** — signals, `onScopeDispose`, alien-signals internals
29
+ - **Full plugin pipeline** — logger, validator, debounce, throttle, retry, persist, sync, and more
30
+ - **Transport layer** — HTTP bridge, WebSocket bridge, SSE bridge
31
+ - **SSR-safe** — per-request bus isolation, no shared singletons
32
+
33
+ ---
34
+
9
35
  ## What is Vue Vapor?
10
36
 
11
37
  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.
@@ -37,17 +63,17 @@ bus.on('cart:add', updateBadge); // now two handlers, hard to trace
37
63
  ```
38
64
  // After — Vapor Chamber
39
65
  // Anywhere in the app
40
- bus.dispatch('cart_add', product, { quantity: 1 });
66
+ bus.dispatch('cartAdd', product, { quantity: 1 });
41
67
 
42
68
  // One place, once:
43
- bus.register('cart_add', (cmd) => {
69
+ bus.register('cartAdd', (cmd) => {
44
70
  cart.items.push(cmd.target);
45
71
  return cart.items;
46
72
  });
47
73
 
48
74
  // Cross-cutting concerns as plugins, not scattered listeners:
49
75
  bus.use(logger());
50
- bus.use(validator({ 'cart_add': (cmd) => cmd.target.id ? null : 'Missing ID' }));
76
+ bus.use(validator({ 'cartAdd': (cmd) => cmd.target.id ? null : 'Missing ID' }));
51
77
  bus.use(analyticsPlugin);
52
78
  ```
53
79
 
@@ -60,7 +86,7 @@ Traditional event systems scatter logic across components. A command bus central
60
86
  ```
61
87
  Event-driven (scattered) Command bus (centralized)
62
88
  ───────────────────────── ─────────────────────────
63
- Component A emits 'add' → dispatch('cart_add', product)
89
+ Component A emits 'add' → dispatch('cartAdd', product)
64
90
  Component B listens... ↓
65
91
  Component C also listens... Handler executes once
66
92
  Who handles what? When? Plugins observe/modify
@@ -68,11 +94,62 @@ Who handles what? When? Plugins observe/modify
68
94
  ```
69
95
 
70
96
  **Benefits:**
71
- - **Semantic actions** — `cart_add` is clearer than `emit('add')`
97
+ - **Semantic actions** — `cartAdd` is clearer than `emit('add')`
72
98
  - **Single handler** — One place to look, debug, test
73
99
  - **Plugin pipeline** — Cross-cutting concerns (logging, validation, analytics) without cluttering handlers
74
100
  - **Undo/redo** — Command history is natural when actions are explicit
75
101
 
102
+ ## Module Architecture
103
+
104
+ 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.
105
+
106
+ ```
107
+ ┌─────────────────────────────────────────────────────────┐
108
+ │ CORE (zero deps · fully tested · framework-agnostic) │
109
+ │ command-bus.ts · testing.ts │
110
+ └────────────────────────┬────────────────────────────────┘
111
+ │ optional layers (tree-shaken)
112
+ ┌───────────────┼───────────────┐
113
+ ▼ ▼ ▼
114
+ Vue composables Plugins Transport
115
+ chamber.ts plugins-core http.ts
116
+ chamber-vapor.ts plugins-io transports.ts
117
+ │
118
+ ▼
119
+ Extras (per-feature opt-in)
120
+ form.ts · schema.ts · devtools.ts · directives.ts · vite-hmr.ts
121
+ ```
122
+
123
+ ### Coverage & stability at v1.0
124
+
125
+ | Layer | Module | Coverage | Status |
126
+ |-------|--------|----------|--------|
127
+ | **Core** | `command-bus.ts` | 90% | ✅ Stable |
128
+ | **Core** | `testing.ts` | 96% | ✅ Stable |
129
+ | Plugins | `plugins-core.ts` | 90% | ✅ Stable |
130
+ | Plugins | `plugins-io.ts` | 88% | ✅ Stable |
131
+ | Transport | `http.ts` | 80% | ✅ Stable |
132
+ | Transport | `transports.ts` | 91% | ✅ Stable |
133
+ | Vue | `chamber.ts` | 76% | ✅ Stable |
134
+ | Extras | `form.ts` | 99% | ✅ Stable |
135
+ | Extras | `schema.ts` | 92% | ✅ Stable |
136
+ | Vue 3.6 | `chamber-vapor.ts` | ✅ | ✅ Stable (unit-tested without Vue 3.6 runtime) |
137
+ | Vue | `directives.ts` | ✅ | ✅ Stable (unit-tested with DOM stubs) |
138
+ | Build | `devtools.ts` | ✅ | ✅ Stable (unit-tested with mock DevTools API) |
139
+ | Build | `vite-hmr.ts` | ✅ | ✅ Stable (unit-tested without Vite runtime) |
140
+ | Build | `iife.ts` | — | 🔧 Bundle entry, not a public API |
141
+
142
+ Sub-path exports avoid pulling in optional modules:
143
+ ```
144
+ 'vapor-chamber' → core + composables + everything (tree-shaken)
145
+ 'vapor-chamber/transports' → HTTP + WebSocket + SSE bridges only
146
+ 'vapor-chamber/directives' → v-command Vue directive only
147
+ 'vapor-chamber/vite' → Vite HMR plugin only
148
+ 'vapor-chamber/iife' → IIFE bundle
149
+ ```
150
+
151
+ ---
152
+
76
153
  ## Install
77
154
 
78
155
  ```bash
@@ -91,17 +168,17 @@ const bus = createCommandBus();
91
168
  // Add plugins
92
169
  bus.use(logger());
93
170
  bus.use(validator({
94
- 'cart_add': (cmd) => cmd.payload?.quantity > 0 ? null : 'Quantity required'
171
+ 'cartAdd': (cmd) => cmd.payload?.quantity > 0 ? null : 'Quantity required'
95
172
  }));
96
173
 
97
174
  // Register handler
98
- bus.register('cart_add', (cmd) => {
175
+ bus.register('cartAdd', (cmd) => {
99
176
  cart.items.push({ ...cmd.target, quantity: cmd.payload.quantity });
100
177
  return cart.items;
101
178
  });
102
179
 
103
180
  // Dispatch
104
- const result = bus.dispatch('cart_add', product, { quantity: 2 });
181
+ const result = bus.dispatch('cartAdd', product, { quantity: 2 });
105
182
  if (result.ok) {
106
183
  console.log('Added:', result.value);
107
184
  } else {
@@ -111,7 +188,7 @@ if (result.ok) {
111
188
 
112
189
  ## Vue 3.6 Vapor Mode
113
190
 
114
- Vapor Chamber v0.4.0 is aligned with Vue 3.6 beta. It works in three contexts:
191
+ Vapor Chamber v1.0 is aligned with Vue 3.6 beta. It works in three contexts:
115
192
 
116
193
  ### 1. Pure Vapor App (smallest bundle)
117
194
 
@@ -167,7 +244,7 @@ A command has three parts:
167
244
 
168
245
  ```typescript
169
246
  bus.dispatch(
170
- 'cart_add', // action - what to do
247
+ 'cartAdd', // action - what to do
171
248
  product, // target - what to act on
172
249
  { quantity: 2 } // payload - additional data (optional)
173
250
  );
@@ -180,13 +257,13 @@ Enforce consistent action names at register and dispatch time:
180
257
  ```typescript
181
258
  const bus = createCommandBus({
182
259
  naming: {
183
- pattern: /^[a-z][a-z0-9]*(_[a-z][a-z0-9]*)+$/, // snake_case
260
+ pattern: /^[a-z][a-zA-Z0-9]+$/, // camelCase
184
261
  onViolation: 'throw' // or 'warn' or 'ignore'
185
262
  }
186
263
  });
187
264
 
188
- bus.register('cart_add', handler); // ✓ passes
189
- bus.register('cartAdd', handler); // ✗ throws
265
+ bus.register('cartAdd', handler); // ✓ passes
266
+ bus.register('cart_add', handler); // ✗ throws
190
267
  ```
191
268
 
192
269
  ### Handlers
@@ -194,7 +271,7 @@ bus.register('cartAdd', handler); // ✗ throws
194
271
  One handler per action. Returns a value or throws:
195
272
 
196
273
  ```typescript
197
- bus.register('cart_add', (cmd) => {
274
+ bus.register('cartAdd', (cmd) => {
198
275
  cart.items.push(cmd.target);
199
276
  return cart.items; // becomes result.value
200
277
  });
@@ -203,7 +280,7 @@ bus.register('cart_add', (cmd) => {
203
280
  Register with options for undo support and per-command throttling:
204
281
 
205
282
  ```typescript
206
- bus.register('cart_add', addHandler, {
283
+ bus.register('cartAdd', addHandler, {
207
284
  undo: (cmd) => { cart.items.pop(); },
208
285
  throttle: 300, // max once per 300ms per target
209
286
  });
@@ -244,6 +321,30 @@ bus.use(analyticsPlugin, { priority: 1 }); // runs after validation
244
321
  bus.use(loggerPlugin); // priority 0 (default, runs last)
245
322
  ```
246
323
 
324
+ ### Before Hooks
325
+
326
+ Run logic before a command reaches its handler. Throw to cancel — the dispatch returns `{ ok: false }`:
327
+
328
+ ```typescript
329
+ // Global auth gate
330
+ bus.onBefore((cmd) => {
331
+ if (!user.isAuth && protectedActions.includes(cmd.action)) {
332
+ throw new Error('Unauthenticated');
333
+ }
334
+ });
335
+
336
+ // Loading indicator
337
+ bus.onBefore(() => { isLoading.value = true; });
338
+ bus.onAfter(() => { isLoading.value = false; });
339
+ ```
340
+
341
+ On an async bus the hook can be async:
342
+ ```typescript
343
+ asyncBus.onBefore(async (cmd) => {
344
+ await rateLimiter.check(cmd.action);
345
+ });
346
+ ```
347
+
247
348
  ### Wildcard Listeners
248
349
 
249
350
  Subscribe to command patterns without being a handler:
@@ -253,10 +354,154 @@ Subscribe to command patterns without being a handler:
253
354
  bus.on('*', (cmd, result) => analytics.track(cmd.action));
254
355
 
255
356
  // Prefix matching
256
- bus.on('shop_*', (cmd, result) => console.log('Shop event:', cmd.action));
357
+ bus.on('cart*', (cmd, result) => console.log('Cart event:', cmd.action));
358
+
359
+ // Exact match — fires once, then removes itself
360
+ bus.once('cartAdd', (cmd, result) => showConfetti());
361
+
362
+ // Remove all listeners for a pattern
363
+ bus.offAll('cart*');
364
+
365
+ // Remove all listeners
366
+ bus.offAll();
367
+ ```
368
+
369
+ ### Query (Read-Only Dispatch)
370
+
371
+ `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:
372
+
373
+ ```typescript
374
+ // Register a handler that reads data
375
+ bus.register('getUser', (cmd) => db.users.find(cmd.target.id));
376
+
377
+ // Read-only — beforeHooks skipped, handler + plugins + afterHooks fire
378
+ const result = bus.query('getUser', { id: 42 });
379
+ ```
380
+
381
+ This is the CQRS separation: `dispatch()` for commands (writes), `query()` for queries (reads).
382
+
383
+ ### Domain Events (emit)
384
+
385
+ Fire an event that notifies `on()` listeners without requiring a handler and without returning a result. Use for observations (not commands):
386
+
387
+ ```typescript
388
+ // Listen to domain events
389
+ bus.on('orderCreated', (cmd) => analytics.track('order', cmd.target));
390
+ bus.on('order*', (cmd) => audit.log(cmd.action, cmd.target));
391
+
392
+ // Fire — no handler needed, no result
393
+ bus.emit('orderCreated', { orderId: 42, total: 99.50 });
394
+ ```
395
+
396
+ ### Command Metadata
397
+
398
+ Every dispatched command is auto-stamped with `meta`:
399
+
400
+ ```typescript
401
+ bus.onAfter((cmd) => {
402
+ console.log(cmd.meta.id); // unique UUID per dispatch
403
+ console.log(cmd.meta.ts); // Date.now() timestamp
404
+ console.log(cmd.meta.correlationId); // trace ID for command chains
405
+ });
257
406
 
258
- // Exact match
259
- bus.on('cart_add', (cmd, result) => updateBadge());
407
+ // Propagate tracing through payload
408
+ bus.dispatch('orderShip', order, {
409
+ __correlationId: originalCommand.meta.id,
410
+ __causationId: originalCommand.meta.id,
411
+ });
412
+ ```
413
+
414
+ ### Structured Errors (BusError)
415
+
416
+ Every error in vapor-chamber has a machine-readable code, severity, and emitter:
417
+
418
+ ```typescript
419
+ import { BusError } from 'vapor-chamber';
420
+
421
+ const result = bus.dispatch('missing', {});
422
+ if (!result.ok && result.error instanceof BusError) {
423
+ result.error.code; // 'VC_CORE_NO_HANDLER'
424
+ result.error.severity; // 'error'
425
+ result.error.emitter; // 'core'
426
+ result.error.action; // 'missing'
427
+ result.error.context; // { } — extra data (e.g. retryIn for throttle)
428
+ }
429
+ ```
430
+
431
+ 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.
432
+
433
+ ### Bus Introspection
434
+
435
+ `inspectBus()` returns a full topology snapshot — useful for DevTools, debugging, and test assertions:
436
+
437
+ ```typescript
438
+ import { inspectBus } from 'vapor-chamber';
439
+
440
+ const info = inspectBus(bus);
441
+ info.actions; // ['cartAdd', 'cartRemove', ...]
442
+ info.undoActions; // ['cartAdd'] — actions with registered undo handlers
443
+ info.pluginCount; // 3
444
+ info.pluginPriorities; // [10, 5, 0]
445
+ info.sealed; // false
446
+ info.dispatchDepth; // 0 (increments during nested dispatch)
447
+ info.activeTimers; // 0 (throttle timers currently running)
448
+ ```
449
+
450
+ Tree-shakeable — not included in your bundle unless imported. Also available on TestBus: `bus.inspect()`.
451
+
452
+ ### Utilities
453
+
454
+ ```typescript
455
+ import { createChamber, createWorkflow, createReaction } from 'vapor-chamber';
456
+
457
+ // Group handlers under a namespace
458
+ const cart = createChamber('cart', { add: handleAdd, remove: handleRemove });
459
+ cart.install(bus); // Registers: cartAdd, cartRemove
460
+
461
+ // Saga: sequential steps with automatic compensation
462
+ const checkout = createWorkflow([
463
+ { action: 'cartValidate' },
464
+ { action: 'paymentReserve', compensate: 'paymentRelease' },
465
+ { action: 'orderCreate', compensate: 'orderCancel' },
466
+ ]);
467
+ const result = await checkout.run(bus, { cartId }); // compensates on failure
468
+
469
+ // Declarative cross-domain reaction
470
+ createReaction('cartAdd', 'inventoryCheck', {
471
+ when: (cmd, result) => result.ok,
472
+ map: (cmd) => ({ itemId: cmd.payload.itemId }),
473
+ }).install(bus);
474
+ ```
475
+
476
+ ### Extra Plugins
477
+
478
+ ```typescript
479
+ import { cache, circuitBreaker, rateLimit, metrics } from 'vapor-chamber';
480
+
481
+ bus.use(cache({ ttl: 60_000, actions: ['getUser*'] }));
482
+ bus.use(circuitBreaker({ threshold: 5, resetTimeout: 30_000 }));
483
+ bus.use(rateLimit({ max: 10, window: 1000 }));
484
+
485
+ const m = metrics();
486
+ bus.use(m);
487
+ console.log(m.summary()); // { cartAdd: { count: 42, avgMs: 1.2, errorRate: 0.02 } }
488
+ ```
489
+
490
+ ### LLM Integration Schemas
491
+
492
+ ```typescript
493
+ import { ERROR_CODE_REGISTRY, describeErrorCodes, busApiSchema } from 'vapor-chamber';
494
+
495
+ // Include in system prompt so LLMs know all error codes and fixes
496
+ const errorTable = describeErrorCodes();
497
+
498
+ // Include bus API schema so LLMs don't hallucinate methods
499
+ const apiSchema = busApiSchema();
500
+
501
+ // Lookup a specific error code
502
+ import { getErrorEntry } from 'vapor-chamber';
503
+ const entry = getErrorEntry('VC_CORE_NO_HANDLER');
504
+ console.log(entry?.fix); // "Register a handler with bus.register(action, handler)"
260
505
  ```
261
506
 
262
507
  ### Request / Response
@@ -287,18 +532,22 @@ Falls back to normal `dispatch()` if no responder is registered.
287
532
  | `throttle(actions, wait)` | Limit execution frequency |
288
533
  | `authGuard(options)` | Block protected commands when unauthenticated |
289
534
  | `optimistic(handlers)` | Apply optimistic updates, rollback on failure |
535
+ | `optimisticUndo(bus, actions, opts?)` | Auto-rollback via registered undo handlers |
536
+ | `retry(options)` | Retry failed async dispatches with backoff |
537
+ | `persist(options)` | Auto-save state to localStorage after commands |
538
+ | `sync(options, bus?)` | Broadcast commands across browser tabs |
290
539
 
291
540
  ### logger
292
541
 
293
542
  ```typescript
294
- bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('cart_') }));
543
+ bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('cart') }));
295
544
  ```
296
545
 
297
546
  ### validator
298
547
 
299
548
  ```typescript
300
549
  bus.use(validator({
301
- 'cart_add': (cmd) => {
550
+ 'cartAdd': (cmd) => {
302
551
  if (!cmd.target?.id) return 'Product must have an ID';
303
552
  return null; // null = valid
304
553
  }
@@ -322,20 +571,20 @@ With bus-backed undo (executes inverse handlers):
322
571
  const historyPlugin = history({ maxSize: 100, bus });
323
572
  bus.use(historyPlugin);
324
573
 
325
- // If cart_add was registered with { undo: fn }, calling undo() executes it
574
+ // If cartAdd was registered with { undo: fn }, calling undo() executes it
326
575
  historyPlugin.undo();
327
576
  ```
328
577
 
329
578
  ### debounce
330
579
 
331
580
  ```typescript
332
- bus.use(debounce(['search_query'], 300)); // wait 300ms after last call
581
+ bus.use(debounce(['searchQuery'], 300)); // wait 300ms after last call
333
582
  ```
334
583
 
335
584
  ### throttle
336
585
 
337
586
  ```typescript
338
- bus.use(throttle(['ui_scroll'], 100)); // max once per 100ms
587
+ bus.use(throttle(['uiScroll'], 100)); // max once per 100ms
339
588
  ```
340
589
 
341
590
  ### authGuard
@@ -343,7 +592,7 @@ bus.use(throttle(['ui_scroll'], 100)); // max once per 100ms
343
592
  ```typescript
344
593
  bus.use(authGuard({
345
594
  isAuthenticated: () => !!user.value,
346
- protected: ['shop_cart_', 'shop_wishlist_'],
595
+ protected: ['shopCart', 'shopWishlist'],
347
596
  onUnauthenticated: (cmd) => router.push('/login'),
348
597
  }));
349
598
  ```
@@ -352,7 +601,7 @@ bus.use(authGuard({
352
601
 
353
602
  ```typescript
354
603
  bus.use(optimistic({
355
- 'cart_add': {
604
+ 'cartAdd': {
356
605
  apply: (cmd) => {
357
606
  cartCount.value++;
358
607
  return () => { cartCount.value--; }; // rollback function
@@ -361,15 +610,220 @@ bus.use(optimistic({
361
610
  }));
362
611
  ```
363
612
 
613
+ ### optimisticUndo
614
+
615
+ 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:
616
+
617
+ ```typescript
618
+ import { createAsyncCommandBus, optimisticUndo } from 'vapor-chamber';
619
+
620
+ const bus = createAsyncCommandBus();
621
+
622
+ // Register handler with undo
623
+ bus.register('cartAdd', async (cmd) => {
624
+ return await api.addToCart(cmd.target);
625
+ }, {
626
+ undo: (cmd) => api.removeFromCart(cmd.target.id),
627
+ });
628
+
629
+ // Install optimisticUndo — auto-rollback on failure
630
+ bus.use(optimisticUndo(bus, ['cartAdd'], {
631
+ predict: (cmd) => ({ ...cart, items: [...cart.items, cmd.target] }),
632
+ onRollback: (cmd, error) => toast.error(`Rolled back: ${error.message}`),
633
+ onRollbackError: (cmd, undoErr, origErr) => console.error('Undo failed:', undoErr),
634
+ }));
635
+ ```
636
+
637
+ ### retry
638
+
639
+ Async plugin that retries failed dispatches with configurable backoff. Install on an `AsyncCommandBus`:
640
+
641
+ ```typescript
642
+ import { createAsyncCommandBus, retry } from 'vapor-chamber';
643
+
644
+ const bus = createAsyncCommandBus();
645
+
646
+ // All actions, exponential backoff (default)
647
+ bus.use(retry({ maxAttempts: 3, baseDelay: 200 }));
648
+
649
+ // Only retry network actions, fixed delay
650
+ bus.use(retry({
651
+ actions: ['api*'],
652
+ maxAttempts: 5,
653
+ baseDelay: 500,
654
+ strategy: 'fixed',
655
+ isRetryable: (err) => err.message !== 'Unauthorized',
656
+ }));
657
+ ```
658
+
659
+ ### persist
660
+
661
+ Auto-save state to localStorage after each successful command. Rehydrate on startup:
662
+
663
+ ```typescript
664
+ import { persist } from 'vapor-chamber';
665
+
666
+ const cartPersist = persist({
667
+ key: 'vc:cart',
668
+ getState: () => cartState.value,
669
+ });
670
+ bus.use(cartPersist);
671
+
672
+ // On app start — rehydrate before rendering
673
+ const saved = cartPersist.load();
674
+ if (saved) cartState.value = saved;
675
+
676
+ // Manual operations
677
+ cartPersist.save(); // force save now
678
+ cartPersist.clear(); // remove from storage
679
+
680
+ // Custom backend (sessionStorage, IndexedDB adapter, etc.)
681
+ bus.use(persist({ key: 'vc:cart', getState, storage: sessionStorage }));
682
+ ```
683
+
684
+ ### sync
685
+
686
+ Broadcast successful commands to all other open tabs via `BroadcastChannel`:
687
+
688
+ ```typescript
689
+ import { sync } from 'vapor-chamber';
690
+
691
+ const tabSync = sync(
692
+ {
693
+ channel: 'vapor-chamber:app',
694
+ filter: (cmd) => cmd.action.startsWith('cart') || cmd.action.startsWith('auth'),
695
+ },
696
+ bus // pass the bus so received messages are re-dispatched locally
697
+ );
698
+
699
+ bus.use(tabSync);
700
+
701
+ // Teardown (component unmount, app destroy)
702
+ tabSync.close();
703
+ tabSync.isOpen(); // → false
704
+ ```
705
+
706
+ ## Transport Layer
707
+
708
+ Send commands to a backend over HTTP, WebSocket, or SSE. Import from `vapor-chamber/transports`
709
+ or directly from `vapor-chamber`:
710
+
711
+ ### createHttpBridge
712
+
713
+ Async plugin that POSTs command envelopes to a backend endpoint. Unhandled commands (no local handler) fall through to the server:
714
+
715
+ ```typescript
716
+ import { createAsyncCommandBus } from 'vapor-chamber';
717
+ import { createHttpBridge } from 'vapor-chamber/transports';
718
+
719
+ const bus = createAsyncCommandBus({ onMissing: 'ignore' });
720
+
721
+ bus.use(createHttpBridge({
722
+ endpoint: '/api/commands',
723
+ csrf: true, // reads XSRF-TOKEN cookie / meta tag automatically
724
+ csrfCookieUrl: '/sanctum/csrf-cookie', // default; set '' to disable the refresh fetch
725
+ retry: 2, // retry up to 2 times on 5xx / 429 / 408
726
+ noRetry: ['paymentCharge', 'orderPlace'], // never retry non-idempotent commands
727
+ timeout: 8000, // ms
728
+ actions: ['order*'], // only forward order* actions; others stay local
729
+ }));
730
+
731
+ const result = await bus.dispatch('orderCreate', { items: cart });
732
+ // → POST /api/commands { command: 'orderCreate', target: { items: ... } }
733
+ ```
734
+
735
+ **Vapor lifecycle integration** — cancel in-flight requests when a component is disposed:
736
+
737
+ ```typescript
738
+ // In <script setup vapor>:
739
+ const ctrl = new AbortController();
740
+ onScopeDispose(() => ctrl.abort());
741
+
742
+ bus.use(createHttpBridge({
743
+ endpoint: '/api/vc',
744
+ scopeController: ctrl, // all requests cancelled on scope disposal
745
+ }));
746
+ ```
747
+
748
+ The backend response shape:
749
+ ```json
750
+ { "state": { "orderId": 42, "status": "pending" } }
751
+ ```
752
+ `result.value` will be the contents of `state`.
753
+
754
+ ### createWsBridge
755
+
756
+ WebSocket transport with auto-reconnect:
757
+
758
+ ```typescript
759
+ import { createWsBridge } from 'vapor-chamber/transports';
760
+
761
+ const ws = createWsBridge({
762
+ url: 'wss://api.example.com/commands',
763
+ actions: ['chat*', 'presence*'],
764
+ timeout: 10_000, // per-message response timeout, ms (default: 10_000)
765
+ maxQueueSize: 100, // max queued messages during disconnect (default: 100)
766
+ reconnect: true, // auto-reconnect on close (default: true)
767
+ maxReconnects: 10, // give up after N reconnect attempts (default: 10)
768
+ });
769
+ bus.use(ws);
770
+ ws.connect();
771
+
772
+ // Lifecycle
773
+ ws.isConnected(); // → boolean (imperative check)
774
+ ws.connected.value; // → boolean (reactive signal — bindable in templates)
775
+ ws.disconnect(); // intentional close — suppresses reconnect
776
+ ```
777
+
778
+ The `connected` signal is reactive — bind it directly in Vapor or VDOM templates without polling:
779
+
780
+ ```vue
781
+ <template>
782
+ <span v-if="ws.connected.value">🟢 Connected</span>
783
+ <span v-else>🔴 Disconnected</span>
784
+ </template>
785
+ ```
786
+
787
+ ### createSseBridge
788
+
789
+ Server-sent events — server pushes commands to the client:
790
+
791
+ ```typescript
792
+ import { createSseBridge } from 'vapor-chamber/transports';
793
+
794
+ bus.use(createSseBridge({
795
+ url: '/api/events',
796
+ }));
797
+ ```
798
+
799
+ ## HTTP Client
800
+
801
+ `postCommand` is exposed for use outside the transport plugin when you need direct HTTP control:
802
+
803
+ ```typescript
804
+ import { postCommand } from 'vapor-chamber';
805
+
806
+ const response = await postCommand('/api/commands', {
807
+ command: 'cartAdd',
808
+ target: product,
809
+ payload: { quantity: 2 },
810
+ }, {
811
+ csrf: true,
812
+ timeout: 5000,
813
+ retry: 2,
814
+ onSessionExpired: (status) => router.push('/login'),
815
+ });
816
+ ```
817
+
364
818
  ## Batch Dispatch
365
819
 
366
- Dispatch multiple commands as a unit. Stops on the first failure:
820
+ Dispatch multiple commands as a unit. Stops on the first failure by default:
367
821
 
368
822
  ```typescript
369
823
  const result = bus.dispatchBatch([
370
- { action: 'cart_add', target: cart, payload: item },
371
- { action: 'totals_update', target: cart },
372
- { action: 'analytics_track', target: session, payload: item },
824
+ { action: 'cartAdd', target: cart, payload: item },
825
+ { action: 'totalsUpdate', target: cart },
826
+ { action: 'analyticsTrack', target: session, payload: item },
373
827
  ]);
374
828
 
375
829
  if (result.ok) {
@@ -379,6 +833,36 @@ if (result.ok) {
379
833
  }
380
834
  ```
381
835
 
836
+ Use `continueOnError` to run all commands regardless of failures, then check counts:
837
+
838
+ ```typescript
839
+ const result = bus.dispatchBatch(commands, { continueOnError: true });
840
+ console.log(`${result.successCount} of ${result.results.length} succeeded`);
841
+ // result.failCount — how many failed
842
+ // result.results — all CommandResult objects, in order
843
+ ```
844
+
845
+ ### Transactional Batch
846
+
847
+ 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:
848
+
849
+ ```typescript
850
+ bus.register('inventoryReserve', reserveHandler, { undo: releaseHandler });
851
+ bus.register('paymentCharge', chargeHandler, { undo: refundHandler });
852
+ bus.register('orderCreate', createHandler, { undo: cancelHandler });
853
+
854
+ const result = bus.dispatchBatch([
855
+ { action: 'inventoryReserve', target: item },
856
+ { action: 'paymentCharge', target: payment },
857
+ { action: 'orderCreate', target: order },
858
+ ], { transactional: true });
859
+
860
+ if (!result.ok) {
861
+ // paymentCharge failed → inventoryReserve was rolled back
862
+ console.log('Rollbacks:', result.rollbacks); // compensation results
863
+ }
864
+ ```
865
+
382
866
  ## Dead Letter Handling
383
867
 
384
868
  Configure what happens when a command has no registered handler:
@@ -399,12 +883,12 @@ import { createAsyncCommandBus } from 'vapor-chamber';
399
883
 
400
884
  const bus = createAsyncCommandBus();
401
885
 
402
- bus.register('user_fetch', async (cmd) => {
886
+ bus.register('userFetch', async (cmd) => {
403
887
  const response = await fetch(`/api/users/${cmd.target.id}`);
404
888
  return response.json();
405
889
  });
406
890
 
407
- const result = await bus.dispatch('user_fetch', { id: 123 });
891
+ const result = await bus.dispatch('userFetch', { id: 123 });
408
892
  ```
409
893
 
410
894
  ## Vapor Composables
@@ -435,7 +919,7 @@ Ideal for GA4 tracking, scroll events, debounced search, fire-and-forget pattern
435
919
  <script setup vapor>
436
920
  import { defineVaporCommand } from 'vapor-chamber';
437
921
 
438
- const { dispatch } = defineVaporCommand('analytics_track', (cmd) => {
922
+ const { dispatch } = defineVaporCommand('analyticsTrack', (cmd) => {
439
923
  gtag('event', cmd.target.event, cmd.target.params);
440
924
  });
441
925
 
@@ -444,6 +928,29 @@ dispatch({ event: 'page_view', params: { page: '/shop' } });
444
928
  </script>
445
929
  ```
446
930
 
931
+ ### useVaporCommand
932
+
933
+ Full-featured composable for Vapor components — reactive `loading`/`lastError` signals plus `register()` and `on()` with automatic cleanup. Unlike `defineVaporCommand` (fire-and-forget), this tracks reactive state. Unlike `useCommand` (VDOM), this uses no `getCurrentInstance()` — safe in Vapor's scope-based lifecycle:
934
+
935
+ ```vue
936
+ <script setup vapor>
937
+ import { useVaporCommand } from 'vapor-chamber';
938
+
939
+ const { dispatch, register, on, loading, lastError, dispose } = useVaporCommand();
940
+
941
+ // Register a handler scoped to this component
942
+ register('cartAdd', (cmd) => addToCart(cmd.target));
943
+
944
+ // Listen to patterns
945
+ on('cart*', (cmd, result) => console.log('Cart event:', cmd.action));
946
+
947
+ // Dispatch with reactive loading/error tracking
948
+ const result = dispatch('cartAdd', product, { quantity: 1 });
949
+
950
+ // Auto-cleanup via onScopeDispose — or call dispose() manually
951
+ </script>
952
+ ```
953
+
447
954
  ### useCommandState
448
955
 
449
956
  State managed by commands:
@@ -455,7 +962,7 @@ import { useCommandState } from 'vapor-chamber';
455
962
  const { state: cart } = useCommandState(
456
963
  { items: [], total: 0 },
457
964
  {
458
- 'cart_add': (state, cmd) => ({
965
+ 'cartAdd': (state, cmd) => ({
459
966
  items: [...state.items, cmd.target],
460
967
  total: state.total + cmd.target.price
461
968
  })
@@ -478,6 +985,115 @@ const { canUndo, canRedo, undo, redo } = useCommandHistory({
478
985
  </script>
479
986
  ```
480
987
 
988
+ ### useCommandGroup
989
+
990
+ 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:
991
+
992
+ ```typescript
993
+ import { useCommandGroup } from 'vapor-chamber';
994
+
995
+ // Cart feature module
996
+ const cart = useCommandGroup('cart');
997
+ cart.register('add', handler); // registers 'cartAdd'
998
+ cart.dispatch('add', product); // dispatches 'cartAdd'
999
+ cart.on('*', listener); // listens to 'cart*'
1000
+
1001
+ // Orders feature — completely isolated
1002
+ const orders = useCommandGroup('orders');
1003
+ orders.dispatch('cancel', { id }); // dispatches 'ordersCancel'
1004
+
1005
+ // Access the namespace
1006
+ cart.namespace; // → 'cart'
1007
+ ```
1008
+
1009
+ Auto-cleanup on Vue scope disposal. `dispose()` is also available for manual teardown.
1010
+
1011
+ ### useCommandError
1012
+
1013
+ Component-scoped error boundary. Reactively captures all failed command results:
1014
+
1015
+ ```typescript
1016
+ import { useCommandError } from 'vapor-chamber';
1017
+
1018
+ // Watch all failed commands
1019
+ const { errors, latestError, clearErrors } = useCommandError();
1020
+
1021
+ // Narrow to a subset
1022
+ const { latestError } = useCommandError({
1023
+ filter: (cmd) => cmd.action.startsWith('cart'),
1024
+ });
1025
+
1026
+ // In template
1027
+ // latestError.value?.message
1028
+ // errors.value.length
1029
+ ```
1030
+
1031
+ ### createFormBus
1032
+
1033
+ Reactive form state manager built on the command bus. Per-field validation, dirty tracking, and full plugin pipeline on every form command:
1034
+
1035
+ ```typescript
1036
+ import { createFormBus, logger } from 'vapor-chamber';
1037
+
1038
+ const form = createFormBus({
1039
+ fields: { email: '', password: '' },
1040
+ rules: {
1041
+ // Sync rule — runs on every set() for live feedback
1042
+ email: (v) => v.includes('@') ? null : 'Invalid email',
1043
+ password: (v) => v.length >= 8 ? null : 'Too short',
1044
+ // Async rule — only awaited on submit() (no UI jank during typing)
1045
+ username: async (v) => {
1046
+ const taken = await api.isUsernameTaken(v);
1047
+ return taken ? 'Username already taken' : null;
1048
+ },
1049
+ },
1050
+ onSubmit: async (values) => await api.login(values),
1051
+ });
1052
+
1053
+ // Attach plugins — logger, throttle, authGuard, etc.
1054
+ form.use(logger());
1055
+
1056
+ // Reactive state
1057
+ form.values.value // { email: '', password: '' }
1058
+ form.errors.value // { email: 'Invalid email', ... }
1059
+ form.isDirty.value // true when any field has changed
1060
+ form.isValid.value // true when no errors
1061
+ form.isSubmitting.value // true while onSubmit is in flight
1062
+
1063
+ // Actions
1064
+ form.set('email', 'user@example.com'); // updates field + re-runs validation
1065
+ form.touch('email'); // marks field as interacted with
1066
+ await form.submit(); // validate → onSubmit → returns bool
1067
+ form.reset(); // restore initial values
1068
+ ```
1069
+
1070
+ **Headless mode** — skip reactive signal allocations for server-side, batch, or non-UI use:
1071
+
1072
+ ```typescript
1073
+ const form = createFormBus({
1074
+ fields: { email: '', password: '' },
1075
+ rules: { email: (v) => v.includes('@') ? null : 'Invalid email' },
1076
+ onSubmit: async (values) => await api.login(values),
1077
+ reactive: false, // no Vue signals — plain get/set wrappers
1078
+ });
1079
+ // All APIs work identically — values, errors, isDirty, etc. are still readable
1080
+ ```
1081
+
1082
+ Template usage (Vue 3):
1083
+
1084
+ ```vue
1085
+ <input :value="form.values.value.email"
1086
+ @input="form.set('email', $event.target.value)"
1087
+ @blur="form.touch('email')" />
1088
+ <span v-if="form.touched.value.email && form.errors.value.email">
1089
+ {{ form.errors.value.email }}
1090
+ </span>
1091
+ <button :disabled="!form.isValid.value || form.isSubmitting.value"
1092
+ @click="form.submit()">
1093
+ Submit
1094
+ </button>
1095
+ ```
1096
+
481
1097
  ### useCommandBus
482
1098
 
483
1099
  Lightweight access to the shared bus — tree-shakeable:
@@ -486,7 +1102,7 @@ Lightweight access to the shared bus — tree-shakeable:
486
1102
  import { useCommandBus } from 'vapor-chamber';
487
1103
 
488
1104
  const bus = useCommandBus();
489
- bus.dispatch('cart_add', product, { quantity: 1 });
1105
+ bus.dispatch('cartAdd', product, { quantity: 1 });
490
1106
  ```
491
1107
 
492
1108
  Use `useCommand()` when you need reactive `loading`/`lastError` signals. Use `defineVaporCommand()` for zero-overhead hot paths. Use `useCommandBus()` when you just need to dispatch.
@@ -507,9 +1123,8 @@ configureSignal(ref); // explicit — usually auto-detected
507
1123
  `createTestBus()` records all dispatched commands without executing real handlers:
508
1124
 
509
1125
  ```typescript
510
- import { createTestBus, setCommandBus } from 'vapor-chamber';
1126
+ import { createTestBus, setCommandBus, resetCommandBus } from 'vapor-chamber';
511
1127
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
512
- import { resetCommandBus } from 'vapor-chamber';
513
1128
 
514
1129
  describe('CartButton', () => {
515
1130
  let bus: TestBus;
@@ -523,14 +1138,37 @@ describe('CartButton', () => {
523
1138
  resetCommandBus();
524
1139
  });
525
1140
 
526
- it('dispatches cart_add on click', () => {
1141
+ it('dispatches cartAdd on click', () => {
527
1142
  // ... render component, click button ...
528
- expect(bus.wasDispatched('cart_add')).toBe(true);
529
- expect(bus.getDispatched('cart_add')[0].cmd.payload).toEqual({ quantity: 1 });
1143
+ expect(bus.wasDispatched('cartAdd')).toBe(true);
1144
+ expect(bus.getDispatched('cartAdd')[0].cmd.payload).toEqual({ quantity: 1 });
530
1145
  });
531
1146
  });
532
1147
  ```
533
1148
 
1149
+ **Snapshot & time-travel** — replay command sequences for debugging or testing:
1150
+
1151
+ ```typescript
1152
+ const bus = createTestBus();
1153
+
1154
+ bus.dispatch('login', user);
1155
+ bus.dispatch('cartAdd', product, { quantity: 1 });
1156
+ bus.dispatch('cartAdd', product2, { quantity: 2 });
1157
+ bus.dispatch('checkout', cart);
1158
+
1159
+ // Immutable snapshot — mutations don't affect bus.recorded
1160
+ const snap = bus.snapshot(); // → RecordedDispatch[]
1161
+
1162
+ // Commands 0..N inclusive (returns Command[])
1163
+ bus.travelTo(1); // → [login, cartAdd]
1164
+
1165
+ // All commands up to last occurrence of 'cartAdd'
1166
+ bus.travelToAction('cartAdd'); // → [login, cartAdd, cartAdd]
1167
+
1168
+ // Out-of-range indices are clamped
1169
+ bus.travelTo(999); // → full history
1170
+ ```
1171
+
534
1172
  ### setupDevtools
535
1173
 
536
1174
  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:
@@ -566,6 +1204,10 @@ See the [`examples/`](./examples) folder for complete, runnable examples:
566
1204
  | `createCommandBus(options?)` | Create a synchronous command bus |
567
1205
  | `createAsyncCommandBus(options?)` | Create an async command bus |
568
1206
  | `createTestBus(options?)` | Create a test bus that records dispatches |
1207
+ | `inspectBus(bus)` | Returns `BusInspection` snapshot of bus topology (tree-shakeable) |
1208
+ | `unsealBus(bus)` | Unseal a sealed bus (tree-shakeable escape hatch) |
1209
+ | `createCommandPool(size)` | Pre-allocated Command object pool for hot paths |
1210
+ | `commandKey(action, target)` | Stable `action:target` string key for cache integration |
569
1211
 
570
1212
  **`CommandBusOptions`**
571
1213
 
@@ -578,24 +1220,38 @@ See the [`examples/`](./examples) folder for complete, runnable examples:
578
1220
 
579
1221
  | Method | Description |
580
1222
  |--------|-------------|
581
- | `dispatch(action, target, payload?)` | Execute a command |
582
- | `dispatchBatch(commands[])` | Execute multiple commands; stops on first failure |
583
- | `register(action, handler, options?)` | Register a handler. Options: `{ undo?, throttle?, debounce? }` |
1223
+ | `dispatch(action, target, payload?)` | Execute a command (write). Auto-stamps `cmd.meta` |
1224
+ | `query(action, target, payload?)` | Read-only dispatch — skips `onBefore` hooks, runs plugins + handler + afterHooks |
1225
+ | `emit(event, data?)` | Fire a domain event — notifies `on()` listeners, no handler required |
1226
+ | `dispatchBatch(commands[], options?)` | Execute multiple commands. Returns `{ successCount, failCount, results }` |
1227
+ | `register(action, handler, options?)` | Register a handler. Options: `{ undo?, throttle? }` |
584
1228
  | `use(plugin, options?)` | Add a plugin. `options.priority` controls order |
585
- | `onAfter(hook)` | Run callback after every command |
586
- | `on(pattern, listener)` | Subscribe to commands matching a pattern (`*`, `prefix_*`, exact) |
587
- | `request(action, target, options?)` | Async request/response with timeout |
1229
+ | `onBefore(hook)` | Run hook before every command. Throw to cancel dispatch. |
1230
+ | `onAfter(hook)` | Run hook after every command |
1231
+ | `on(pattern, listener)` | Subscribe to commands matching a pattern (`*`, `prefix*`, exact). Returns unsub. |
1232
+ | `once(pattern, listener)` | Like `on()` but auto-unsubscribes after first match |
1233
+ | `offAll(pattern?)` | Remove all listeners for a pattern, or all listeners if omitted |
1234
+ | `request(action, target, payload?, options?)` | Async request/response with timeout (default 5s) |
588
1235
  | `respond(action, handler)` | Register a responder for `request()` calls |
589
- | `getUndoHandler(action)` | Get the undo handler for an action |
1236
+ | `hasHandler(action)` | Returns true if a handler is registered for the action |
1237
+ | `registeredActions()` | Returns `string[]` of all registered action names |
1238
+ | `clear()` | Remove all handlers, plugins, hooks, and listeners |
1239
+ | `seal()` | Freeze bus configuration — rejects register/use/clear after sealing |
1240
+ | `dispose()` | Clean teardown — clears state, cancels timers, marks bus as disposed |
1241
+ | `registeredActions()` | Returns `string[]` of all registered action names |
1242
+ | `getUndoHandler(action)` | Get the undo handler for an action (`@internal`) |
590
1243
 
591
1244
  ### Composables
592
1245
 
593
1246
  | Composable | Description |
594
1247
  |------------|-------------|
595
1248
  | `useCommand()` | Dispatch with reactive loading/error state |
1249
+ | `useVaporCommand()` | Vapor-safe composable with dispatch, register, on, loading/error, auto-cleanup |
596
1250
  | `defineVaporCommand(action, handler, options?)` | Zero-overhead dispatch for hot paths |
597
1251
  | `useCommandState(initial, handlers)` | State managed by commands |
598
1252
  | `useCommandHistory(options?)` | Reactive undo/redo |
1253
+ | `useCommandGroup(namespace)` | Namespace isolation — prefixes all calls in camelCase |
1254
+ | `useCommandError(options?)` | Reactive error boundary for failed dispatches |
599
1255
  | `useCommandBus()` | Get shared bus instance |
600
1256
  | `getCommandBus()` | Get shared bus instance (non-composable) |
601
1257
  | `setCommandBus(bus)` | Set shared bus instance |
@@ -608,33 +1264,166 @@ See the [`examples/`](./examples) folder for complete, runnable examples:
608
1264
 
609
1265
  ## Roadmap
610
1266
 
611
- | Feature | Status |
612
- |---------|--------|
613
- | DevTools integration | ✅ Done |
614
- | DevTools production strip (0KB in prod) | ✅ Done |
615
- | Command batching (`dispatchBatch`) | ✅ Done |
616
- | Middleware priority/ordering | ✅ Done |
617
- | Dead letter handling (`onMissing`) | ✅ Done |
618
- | Testing utilities (`createTestBus`) | ✅ Done |
619
- | Naming convention enforcement | ✅ Done (v0.3.0) |
620
- | Wildcard listeners (`on`) | ✅ Done (v0.3.0) |
621
- | Request/response pattern | ✅ Done (v0.3.0) |
622
- | Per-command throttle/undo at register | ✅ Done (v0.3.0) |
623
- | Auth guard plugin | ✅ Done (v0.3.0) |
624
- | Optimistic update plugin | ✅ Done (v0.3.0) |
625
- | Vue 3.6 Vapor alignment | ✅ Done (v0.4.0) |
626
- | `defineVaporCommand` zero-overhead composable | ✅ Done (v0.4.0) |
627
- | `onScopeDispose` lifecycle alignment | ✅ Done (v0.4.0) |
628
- | Persistence plugin (localStorage / IndexedDB) | Planned |
629
- | SSR support | Planned (pending Vue Vapor stabilization) |
1267
+ ### Core — target: 100% feature-complete at v1.0
1268
+
1269
+ | Feature | Module | Status | Tests |
1270
+ |---------|--------|--------|-------|
1271
+ | Dispatch / register / unregister | `command-bus` | ✅ v0.1.0 | ✅ 90% coverage |
1272
+ | Plugin pipeline (sync + async) | `command-bus` | ✅ v0.1.0 | ✅ 90% coverage |
1273
+ | Plugin priority ordering | `command-bus` | ✅ v0.2.0 | ✅ covered |
1274
+ | `onAfter` hooks | `command-bus` | ✅ v0.2.0 | ✅ covered |
1275
+ | Dead letter handling (`onMissing`) | `command-bus` | ✅ v0.2.0 | ✅ covered |
1276
+ | Command batching + `continueOnError` + `successCount`/`failCount` | `command-bus` | ✅ v0.6.0 | ✅ covered |
1277
+ | Naming convention enforcement | `command-bus` | ✅ v0.3.0 | ✅ covered |
1278
+ | Wildcard listeners (`on`, `prefix*`) | `command-bus` | ✅ v0.3.0 | ✅ covered |
1279
+ | `once()` — one-shot listener | `command-bus` | ✅ v0.6.0 | ✅ covered |
1280
+ | `offAll(pattern?)` — mass unsubscribe | `command-bus` | ✅ v0.6.0 | ✅ covered |
1281
+ | `onBefore(hook)` — pre-dispatch hook, cancelable | `command-bus` | ✅ v0.6.0 | ✅ covered |
1282
+ | Request / response pattern + timeout | `command-bus` | ✅ v0.3.0 | ✅ covered |
1283
+ | Per-command throttle + undo at register | `command-bus` | ✅ v0.3.0 | ✅ covered |
1284
+ | `bus.hasHandler()` introspection | `command-bus` | ✅ v0.3.0 | ✅ covered |
1285
+ | `bus.clear()` | `command-bus` | ✅ v0.5.0 | ✅ covered |
1286
+ | `BaseBus` structural interface | `command-bus` | ✅ v0.6.0 | ✅ covered |
1287
+ | `query()` — CQRS read-only dispatch (skips beforeHooks) | `command-bus` | ✅ v1.0 | ✅ covered |
1288
+ | `emit()` — domain events (no handler, no result) | `command-bus` | ✅ v1.0 | ✅ covered |
1289
+ | `Command.meta` — auto-stamped id, ts, correlationId, causationId | `command-bus` | ✅ v1.0 | ✅ covered |
1290
+ | `registeredActions()` — introspection | `command-bus` | ✅ v1.0 | ✅ covered |
1291
+ | `commandKey(action, target)` export | `command-bus` | ✅ v0.6.0 | ✅ covered |
1292
+ | `BusError` structured error class (code, severity, emitter) | `command-bus` | ✅ v1.0 | ✅ covered |
1293
+ | `inspectBus(bus)` — tree-shakeable topology introspection | `command-bus` | ✅ v1.0 | ✅ covered |
1294
+ | `bus.seal()` / `unsealBus(bus)` — freeze configuration | `command-bus` | ✅ v1.0 | ✅ covered |
1295
+ | `bus.dispose()` — clean teardown with timer cancellation | `command-bus` | ✅ v1.0 | ✅ covered |
1296
+ | `createCommandPool(size)` — pre-allocated object pool | `command-bus` | ✅ v1.0 | ✅ covered |
1297
+ | Transactional batch with undo rollback | `command-bus` | ✅ v1.0 | ✅ covered |
1298
+ | Recursion depth guard (max 10) | `command-bus` | ✅ v1.0 | ✅ covered |
1299
+ | V8 optimizations (monomorphic shapes, index loops, extracted try/catch) | `command-bus` | ✅ v1.0 | ✅ bench |
1300
+ | SSR isolation (independent bus instances) | `command-bus` | ✅ v0.5.0 | ✅ covered |
1301
+ | `createTestBus` record + assert | `testing` | ✅ v0.2.0 | ✅ 96% coverage |
1302
+ | `createTestBus` snapshot & time-travel | `testing` | ✅ v0.4.3 | ✅ covered |
1303
+ | `TestBus.on()` / `once()` / `offAll()` real implementations | `testing` | ✅ v0.6.0 | ✅ covered |
1304
+
1305
+ ### Plugins — optional, fully implemented
1306
+
1307
+ | Feature | Module | Status | Tests |
1308
+ |---------|--------|--------|-------|
1309
+ | `logger` | `plugins-core` | ✅ v0.1.0 | ✅ 90% coverage |
1310
+ | `validator` | `plugins-core` | ✅ v0.1.0 | ✅ covered |
1311
+ | `history` + bus-backed undo/redo | `plugins-core` | ✅ v0.3.0 | ✅ covered |
1312
+ | `debounce` (stale-closure fix) | `plugins-core` | ✅ v0.3.0 | ✅ covered |
1313
+ | `throttle` | `plugins-core` | ✅ v0.3.0 | ✅ covered |
1314
+ | `authGuard` | `plugins-core` | ✅ v0.3.0 | ✅ covered |
1315
+ | `optimistic` | `plugins-core` | ✅ v0.3.0 | ✅ covered |
1316
+ | `optimisticUndo` — auto-rollback via registered undo handlers | `plugins-core` | ✅ v1.0 | ✅ covered |
1317
+ | `retry` with configurable backoff + glob filter | `plugins-io` | ✅ v0.4.2 | ✅ 88% coverage |
1318
+ | `persist` (localStorage / custom storage) | `plugins-io` | ✅ v0.4.2 | ✅ covered |
1319
+ | `sync` (BroadcastChannel cross-tab) | `plugins-io` | ✅ v0.4.2 | ✅ covered |
1320
+ | `cache` — LRU query result caching with TTL + glob filter | `plugins-extra` | ✅ v1.0 | ✅ covered |
1321
+ | `circuitBreaker` — per-action closed/open/half-open resilience | `plugins-extra` | ✅ v1.0 | ✅ covered |
1322
+ | `rateLimit` — per-action sliding window limiter | `plugins-extra` | ✅ v1.0 | ✅ covered |
1323
+ | `metrics` — lightweight telemetry (count, duration, errorRate) | `plugins-extra` | ✅ v1.0 | ✅ covered |
1324
+
1325
+ ### Utilities — optional, tree-shaken
1326
+
1327
+ | Feature | Module | Status | Tests |
1328
+ |---------|--------|--------|-------|
1329
+ | `createChamber` — declarative namespace grouping | `utilities` | ✅ v1.0 | ✅ covered |
1330
+ | `createWorkflow` — saga pattern with compensation | `utilities` | ✅ v1.0 | ✅ covered |
1331
+ | `createReaction` — declarative cross-domain rules | `utilities` | ✅ v1.0 | ✅ covered |
1332
+
1333
+ ### Transport layer — optional, fully implemented
1334
+
1335
+ | Feature | Module | Status | Tests |
1336
+ |---------|--------|--------|-------|
1337
+ | `postCommand` — POST with retry, CSRF, timeout, session | `http` | ✅ v0.5.0 | ✅ 80% coverage |
1338
+ | `readCsrfToken` — meta / cookie / hidden input | `http` | ✅ v0.5.0 | ✅ covered |
1339
+ | `HttpError.code` — machine-readable code from response body | `http` | ✅ v0.6.0 | ✅ covered |
1340
+ | 419 vs 401 fix — CSRF expiry ≠ session expiry | `http` | ✅ v0.6.0 | ✅ covered |
1341
+ | `createHttpBridge` — fetch plugin | `transports` | ✅ v0.4.2 | ✅ 91% coverage |
1342
+ | `HttpBridgeOptions.noRetry` — per-action retry disable | `transports` | ✅ v0.6.0 | ✅ covered |
1343
+ | `HttpBridgeOptions.scopeController` — Vapor lifecycle abort | `transports` | ✅ v0.6.0 | ✅ covered |
1344
+ | `createWsBridge` — WebSocket plugin + reconnect + bounded queue | `transports` | ✅ v0.6.0 | ✅ covered |
1345
+ | `WsBridge.connected` — reactive signal for connection state | `transports` | ✅ v0.6.0 | ✅ covered |
1346
+ | `createSseBridge` — server-push EventSource, accepts `BaseBus` | `transports` | ✅ v0.6.0 | ✅ covered |
1347
+
1348
+ ### Vue composables — optional, requires Vue ≥3.5
1349
+
1350
+ | Feature | Module | Status | Tests |
1351
+ |---------|--------|--------|-------|
1352
+ | `useCommand` — reactive loading/error | `chamber` | ✅ v0.1.0 | ✅ 76% coverage |
1353
+ | `useCommandState` | `chamber` | ✅ v0.2.0 | ✅ covered |
1354
+ | `useCommandHistory` — reactive undo/redo | `chamber` | ✅ v0.2.0 | ✅ covered |
1355
+ | `useCommandGroup` — namespace isolation | `chamber` | ✅ v0.4.1 | ✅ covered |
1356
+ | `useCommandError` — error boundary | `chamber` | ✅ v0.4.1 | ✅ covered |
1357
+ | `getCommandBus` / `setCommandBus` / `resetCommandBus` | `chamber` | ✅ v0.1.0 | ✅ covered |
1358
+ | Signal shim + `configureSignal` | `chamber` | ✅ v0.3.0 | ✅ covered |
1359
+ | `onScopeDispose` lifecycle alignment | `chamber` | ✅ v0.4.0 | ✅ covered |
1360
+ | `isVaporAvailable()` | `chamber` | ✅ v0.4.0 | ✅ covered |
1361
+ | `createVaporChamberApp` / `getVaporInteropPlugin` / `defineVaporCommand` | `chamber-vapor` | ✅ v0.4.0 | ✅ covered |
1362
+ | `useVaporCommand` — Vapor-safe reactive composable | `chamber-vapor` | ✅ v0.6.0 | ✅ covered |
1363
+ | `tryAutoCleanup` dev warning (no scope/instance) | `chamber` | ✅ v0.6.0 | ✅ covered |
1364
+ | `waitForVueDetection()` — async Vue probe | `chamber` | ✅ v0.6.0 | ✅ covered |
1365
+
1366
+ ### Extras — optional, per-feature opt-in
1367
+
1368
+ | Feature | Module | Status | Tests |
1369
+ |---------|--------|--------|-------|
1370
+ | `createFormBus` — reactive form + sync/async validation | `form` | ✅ v0.6.0 | ✅ 99% coverage |
1371
+ | `FormBus` headless mode (`reactive: false`) | `form` | ✅ v0.6.0 | ✅ covered |
1372
+ | Schema layer — `createSchemaCommandBus`, `toTools`, `synthesize` | `schema` | ✅ v0.5.0 | ✅ 92% coverage |
1373
+ | Schema auto-validation (`schemaValidator` auto-installed) | `schema` | ✅ v1.0 | ✅ covered |
1374
+ | `SynthesizeOptions.adapter` — custom LLM adapter | `schema` | ✅ v0.6.0 | ✅ covered |
1375
+ | `ERROR_CODE_REGISTRY` — structured error lookup table | `schema` | ✅ v1.0 | ✅ covered |
1376
+ | `busApiSchema()` — JSON schema of bus API for LLM prompts | `schema` | ✅ v1.0 | ✅ covered |
1377
+ | `describeErrorCodes()` — plain-text error table for LLM system prompts | `schema` | ✅ v1.0 | ✅ covered |
1378
+ | `setupDevtools` — Vue DevTools panel | `devtools` | ✅ v0.4.0 | ✅ covered |
1379
+ | `createDirectivePlugin` — `v-command` directive + Vapor compat warning | `directives` | ✅ v0.6.0 | ✅ covered |
1380
+ | Vite HMR plugin (+ `.vapor.vue` support) | `vite-hmr` | ✅ v0.6.0 | ✅ covered |
1381
+ | IIFE / CDN bundle | `iife` | ✅ v0.5.0 | 🔧 bundle entry |
1382
+
1383
+ ### v1.0 checklist
1384
+
1385
+ | Item | Status |
1386
+ |------|--------|
1387
+ | Core (`command-bus` + `testing`) at 90%+ coverage | ✅ Done |
1388
+ | All tests green (466/466, 0 failures) | ✅ Done |
1389
+ | Optional modules clearly marked in exports | ✅ Done |
1390
+ | Transport layer fully tested (HTTP + WS + SSE) | ✅ Done |
1391
+ | Plugins fully tested | ✅ Done |
1392
+ | camelCase naming convention locked in | ✅ Done |
1393
+ | `onBefore` / `offAll` / `once` on both buses | ✅ Done (v0.6.0) |
1394
+ | `query()` — CQRS read-only dispatch | ✅ Done (v1.0) |
1395
+ | `emit()` — domain events (no handler required) | ✅ Done (v1.0) |
1396
+ | `Command.meta` — auto-stamped id, timestamp, tracing | ✅ Done (v1.0) |
1397
+ | `registeredActions()` — introspection | ✅ Done (v1.0) |
1398
+ | `TestBus.onBefore` fires for real | ✅ Done (v1.0) |
1399
+ | `BaseBus` structural interface for cross-bus utilities | ✅ Done (v0.6.0) |
1400
+ | V8 engine optimizations (monomorphic shapes, no .slice() in hot paths) | ✅ Done (v1.0) |
1401
+ | `BusError` structured error class with codes, severity, emitter | ✅ Done (v1.0) |
1402
+ | `ERROR_CODE_REGISTRY` + `busApiSchema()` for LLM integration | ✅ Done (v1.0) |
1403
+ | `createChamber`, `createWorkflow`, `createReaction` utilities | ✅ Done (v1.0) |
1404
+ | `cache`, `circuitBreaker`, `rateLimit`, `metrics` plugins | ✅ Done (v1.0) |
1405
+ | LLM-friendly naming (`TargetOf`/`PayloadOf`/`ResultOf`) + `@example` JSDoc | ✅ Done (v1.0) |
1406
+ | Self-correcting error messages with fix suggestions | ✅ Done (v1.0) |
1407
+ | CSRF / 419 / session-expiry correctness | ✅ Done (v0.6.0) |
1408
+ | Form async validation | ✅ Done (v0.6.0) |
1409
+ | `HttpError.code` structured error codes | ✅ Done (v0.6.0) |
1410
+ | WS queue cap (`maxQueueSize`) | ✅ Done (v0.6.0) |
1411
+ | `synthesize` LLM adapter (proxy / OpenAI support) | ✅ Done (v0.6.0) |
1412
+ | Transactional batch with undo rollback | ✅ Done (v1.0) |
1413
+ | `optimisticUndo` plugin — auto-rollback via undo handlers | ✅ Done (v1.0) |
1414
+ | Schema auto-validation (`schemaValidator` auto-installed) | ✅ Done (v1.0) |
1415
+ | `inspectBus()` — tree-shakeable bus topology introspection | ✅ Done (v1.0) |
1416
+ | `bus.seal()` / `unsealBus()` — freeze configuration | ✅ Done (v1.0) |
1417
+ | `bus.dispose()` — clean teardown | ✅ Done (v1.0) |
1418
+ | `createCommandPool` — object pool for hot paths | ✅ Done (v1.0) |
1419
+ | Recursion depth guard (max 10) | ✅ Done (v1.0) |
1420
+ | Architectural whitepaper | ✅ Done (v0.6.0) |
1421
+ | `chamber.ts` branch coverage | 🔄 76% → target 85% |
1422
+ | Publish to npm as `vapor-chamber@1.0.0` | ⬜ Pending |
630
1423
 
631
1424
  ## Documentation
632
1425
 
633
- See the [`docs/`](./docs) folder for detailed documentation:
634
-
635
- - [Whitepaper](./docs/whitepaper.md) — Design philosophy and architecture
636
- - [Vue 3.6 Vapor Alignment](./docs/whitepaper-vue36.md) — Alien-signals, Vapor mode, and migration strategy
637
- - [SSR Guide](./docs/ssr.md) — Server-side rendering and hydration
1426
+ See [`docs/whitepaper.md`](./docs/whitepaper.md) for design philosophy, architecture, camelCase naming rationale, Vue 3.6 Vapor alignment, SSR guide, and migration strategy.
638
1427
 
639
1428
  ## Design Goals
640
1429