vapor-chamber 0.2.0 → 0.5.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 (53) hide show
  1. package/README.md +750 -113
  2. package/dist/chamber-vapor.d.ts +48 -0
  3. package/dist/chamber-vapor.d.ts.map +1 -0
  4. package/dist/chamber-vapor.js +62 -0
  5. package/dist/chamber.d.ts +92 -18
  6. package/dist/chamber.d.ts.map +1 -1
  7. package/dist/chamber.js +262 -49
  8. package/dist/command-bus.d.ts +146 -23
  9. package/dist/command-bus.d.ts.map +1 -1
  10. package/dist/command-bus.js +460 -151
  11. package/dist/devtools.d.ts.map +1 -1
  12. package/dist/devtools.js +3 -1
  13. package/dist/directives.d.ts +37 -0
  14. package/dist/directives.d.ts.map +1 -0
  15. package/dist/directives.js +190 -0
  16. package/dist/form.d.ts +72 -0
  17. package/dist/form.d.ts.map +1 -0
  18. package/dist/form.js +159 -0
  19. package/dist/http.d.ts +60 -0
  20. package/dist/http.d.ts.map +1 -0
  21. package/dist/http.js +242 -0
  22. package/dist/iife.d.ts +95 -0
  23. package/dist/iife.d.ts.map +1 -0
  24. package/dist/iife.js +90 -0
  25. package/dist/index.d.ts +45 -5
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +60 -9
  28. package/dist/plugins-core.d.ts +69 -0
  29. package/dist/plugins-core.d.ts.map +1 -0
  30. package/dist/plugins-core.js +210 -0
  31. package/dist/plugins-io.d.ts +100 -0
  32. package/dist/plugins-io.d.ts.map +1 -0
  33. package/dist/plugins-io.js +171 -0
  34. package/dist/plugins.d.ts +8 -44
  35. package/dist/plugins.d.ts.map +1 -1
  36. package/dist/plugins.js +8 -133
  37. package/dist/schema.d.ts +162 -0
  38. package/dist/schema.d.ts.map +1 -0
  39. package/dist/schema.js +266 -0
  40. package/dist/testing.d.ts +31 -7
  41. package/dist/testing.d.ts.map +1 -1
  42. package/dist/testing.js +101 -24
  43. package/dist/transports.d.ts +168 -0
  44. package/dist/transports.d.ts.map +1 -0
  45. package/dist/transports.js +225 -0
  46. package/dist/vapor-chamber.iife.js +1251 -0
  47. package/dist/vapor-chamber.iife.js.map +7 -0
  48. package/dist/vapor-chamber.iife.min.js +2 -0
  49. package/dist/vite-hmr.d.ts +50 -0
  50. package/dist/vite-hmr.d.ts.map +1 -0
  51. package/dist/vite-hmr.js +110 -0
  52. package/package.json +32 -9
  53. package/scripts/build-iife.mjs +47 -0
package/README.md CHANGED
@@ -3,12 +3,40 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- A lightweight command bus designed for <a href="https://github.com/vuejs/vue-vapor">Vue Vapor</a>. ~2KB gzipped. Optional DevTools integration.
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
- 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.
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.
38
+
39
+ **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.
12
40
 
13
41
  **Vapor Chamber** embraces this philosophy: minimal abstraction, direct updates, signal-native reactivity.
14
42
 
@@ -35,17 +63,17 @@ bus.on('cart:add', updateBadge); // now two handlers, hard to trace
35
63
  ```
36
64
  // After — Vapor Chamber
37
65
  // Anywhere in the app
38
- bus.dispatch('cart.add', product, { quantity: 1 });
66
+ bus.dispatch('cartAdd', product, { quantity: 1 });
39
67
 
40
68
  // One place, once:
41
- bus.register('cart.add', (cmd) => {
69
+ bus.register('cartAdd', (cmd) => {
42
70
  cart.items.push(cmd.target);
43
71
  return cart.items;
44
72
  });
45
73
 
46
74
  // Cross-cutting concerns as plugins, not scattered listeners:
47
75
  bus.use(logger());
48
- bus.use(validator({ 'cart.add': (cmd) => cmd.target.id ? null : 'Missing ID' }));
76
+ bus.use(validator({ 'cartAdd': (cmd) => cmd.target.id ? null : 'Missing ID' }));
49
77
  bus.use(analyticsPlugin);
50
78
  ```
51
79
 
@@ -58,7 +86,7 @@ Traditional event systems scatter logic across components. A command bus central
58
86
  ```
59
87
  Event-driven (scattered) Command bus (centralized)
60
88
  ───────────────────────── ─────────────────────────
61
- Component A emits 'add' → dispatch('cart.add', product)
89
+ Component A emits 'add' → dispatch('cartAdd', product)
62
90
  Component B listens... ↓
63
91
  Component C also listens... Handler executes once
64
92
  Who handles what? When? Plugins observe/modify
@@ -66,10 +94,61 @@ Who handles what? When? Plugins observe/modify
66
94
  ```
67
95
 
68
96
  **Benefits:**
69
- - **Semantic actions** - `cart.add` is clearer than `emit('add')`
70
- - **Single handler** - One place to look, debug, test
71
- - **Plugin pipeline** - Cross-cutting concerns (logging, validation, analytics) without cluttering handlers
72
- - **Undo/redo** - Command history is natural when actions are explicit
97
+ - **Semantic actions** `cartAdd` is clearer than `emit('add')`
98
+ - **Single handler** One place to look, debug, test
99
+ - **Plugin pipeline** Cross-cutting concerns (logging, validation, analytics) without cluttering handlers
100
+ - **Undo/redo** Command history is natural when actions are explicit
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 v0.5.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` | — | ⚠️ Requires Vue 3.6 runtime to test |
137
+ | Vue | `directives.ts` | — | ⚠️ Requires Vue DOM environment to test |
138
+ | Build | `devtools.ts` | — | ⚠️ Requires browser DevTools API to test |
139
+ | Build | `vite-hmr.ts` | — | ⚠️ Requires Vite runtime to test |
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
+ ---
73
152
 
74
153
  ## Install
75
154
 
@@ -77,6 +156,8 @@ Who handles what? When? Plugins observe/modify
77
156
  npm install vapor-chamber
78
157
  ```
79
158
 
159
+ **Requirements:** Node.js ≥20.19.0 | Vue ≥3.5.0 (optional peer dep) | Vite 7/8 compatible
160
+
80
161
  ## Quick Start
81
162
 
82
163
  ```typescript
@@ -87,17 +168,17 @@ const bus = createCommandBus();
87
168
  // Add plugins
88
169
  bus.use(logger());
89
170
  bus.use(validator({
90
- 'cart.add': (cmd) => cmd.payload?.quantity > 0 ? null : 'Quantity required'
171
+ 'cartAdd': (cmd) => cmd.payload?.quantity > 0 ? null : 'Quantity required'
91
172
  }));
92
173
 
93
174
  // Register handler
94
- bus.register('cart.add', (cmd) => {
175
+ bus.register('cartAdd', (cmd) => {
95
176
  cart.items.push({ ...cmd.target, quantity: cmd.payload.quantity });
96
177
  return cart.items;
97
178
  });
98
179
 
99
180
  // Dispatch
100
- const result = bus.dispatch('cart.add', product, { quantity: 2 });
181
+ const result = bus.dispatch('cartAdd', product, { quantity: 2 });
101
182
  if (result.ok) {
102
183
  console.log('Added:', result.value);
103
184
  } else {
@@ -105,6 +186,56 @@ if (result.ok) {
105
186
  }
106
187
  ```
107
188
 
189
+ ## Vue 3.6 Vapor Mode
190
+
191
+ Vapor Chamber v0.4.0 is aligned with Vue 3.6 beta. It works in three contexts:
192
+
193
+ ### 1. Pure Vapor App (smallest bundle)
194
+
195
+ ```typescript
196
+ import { createVaporChamberApp, getCommandBus } from 'vapor-chamber';
197
+ import App from './App.vue';
198
+
199
+ // No VDOM runtime — ~10KB baseline
200
+ createVaporChamberApp(App).mount('#app');
201
+ ```
202
+
203
+ ```vue
204
+ <script setup vapor>
205
+ import { useCommand } from 'vapor-chamber';
206
+
207
+ const { dispatch, loading } = useCommand();
208
+ </script>
209
+ ```
210
+
211
+ ### 2. Mixed VDOM + Vapor (gradual migration)
212
+
213
+ ```typescript
214
+ import { createApp } from 'vue';
215
+ import { getVaporInteropPlugin } from 'vapor-chamber';
216
+
217
+ const app = createApp(App);
218
+ const interop = getVaporInteropPlugin();
219
+ if (interop) app.use(interop);
220
+ app.mount('#app');
221
+ ```
222
+
223
+ Now Vapor and VDOM components can nest inside each other. Useful for incremental migration.
224
+
225
+ ### 3. Standard Vue 3 (no Vapor)
226
+
227
+ Everything works without Vapor. The signal shim auto-detects Vue's `ref()` for reactivity. In Vue 3.6+ this is alien-signals backed.
228
+
229
+ ### Vapor Detection
230
+
231
+ ```typescript
232
+ import { isVaporAvailable } from 'vapor-chamber';
233
+
234
+ if (isVaporAvailable()) {
235
+ // Vue 3.6+ with createVaporApp available
236
+ }
237
+ ```
238
+
108
239
  ## Core Concepts
109
240
 
110
241
  ### Commands
@@ -113,27 +244,48 @@ A command has three parts:
113
244
 
114
245
  ```typescript
115
246
  bus.dispatch(
116
- 'cart.add', // action - what to do
247
+ 'cartAdd', // action - what to do
117
248
  product, // target - what to act on
118
249
  { quantity: 2 } // payload - additional data (optional)
119
250
  );
120
251
  ```
121
252
 
253
+ ### Naming Convention
254
+
255
+ Enforce consistent action names at register and dispatch time:
256
+
257
+ ```typescript
258
+ const bus = createCommandBus({
259
+ naming: {
260
+ pattern: /^[a-z][a-zA-Z0-9]+$/, // camelCase
261
+ onViolation: 'throw' // or 'warn' or 'ignore'
262
+ }
263
+ });
264
+
265
+ bus.register('cartAdd', handler); // ✓ passes
266
+ bus.register('cart_add', handler); // ✗ throws
267
+ ```
268
+
122
269
  ### Handlers
123
270
 
124
271
  One handler per action. Returns a value or throws:
125
272
 
126
273
  ```typescript
127
- bus.register('cart.add', (cmd) => {
128
- // cmd.action = 'cart.add'
129
- // cmd.target = product
130
- // cmd.payload = { quantity: 2 }
131
-
274
+ bus.register('cartAdd', (cmd) => {
132
275
  cart.items.push(cmd.target);
133
276
  return cart.items; // becomes result.value
134
277
  });
135
278
  ```
136
279
 
280
+ Register with options for undo support and per-command throttling:
281
+
282
+ ```typescript
283
+ bus.register('cartAdd', addHandler, {
284
+ undo: (cmd) => { cart.items.pop(); },
285
+ throttle: 300, // max once per 300ms per target
286
+ });
287
+ ```
288
+
137
289
  ### Results
138
290
 
139
291
  Every dispatch returns a result:
@@ -169,6 +321,68 @@ bus.use(analyticsPlugin, { priority: 1 }); // runs after validation
169
321
  bus.use(loggerPlugin); // priority 0 (default, runs last)
170
322
  ```
171
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
+
348
+ ### Wildcard Listeners
349
+
350
+ Subscribe to command patterns without being a handler:
351
+
352
+ ```typescript
353
+ // All commands
354
+ bus.on('*', (cmd, result) => analytics.track(cmd.action));
355
+
356
+ // Prefix matching
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
+ ### Request / Response
370
+
371
+ Async request/response pattern with timeout:
372
+
373
+ ```typescript
374
+ // Register a responder
375
+ bus.respond('get_auth_token', async (cmd) => {
376
+ const response = await fetch('/api/token');
377
+ return response.json();
378
+ });
379
+
380
+ // Request with timeout
381
+ const result = await bus.request('get_auth_token', { userId: 42 }, { timeout: 3000 });
382
+ ```
383
+
384
+ Falls back to normal `dispatch()` if no responder is registered.
385
+
172
386
  ## Built-in Plugins
173
387
 
174
388
  | Plugin | Description |
@@ -178,18 +392,23 @@ bus.use(loggerPlugin); // priority 0 (default, runs last)
178
392
  | `history(options?)` | Track command history for undo/redo |
179
393
  | `debounce(actions, wait)` | Delay execution until activity stops |
180
394
  | `throttle(actions, wait)` | Limit execution frequency |
395
+ | `authGuard(options)` | Block protected commands when unauthenticated |
396
+ | `optimistic(handlers)` | Apply optimistic updates, rollback on failure |
397
+ | `retry(options)` | Retry failed async dispatches with backoff |
398
+ | `persist(options)` | Auto-save state to localStorage after commands |
399
+ | `sync(options, bus?)` | Broadcast commands across browser tabs |
181
400
 
182
401
  ### logger
183
402
 
184
403
  ```typescript
185
- bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('cart.') }));
404
+ bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('cart') }));
186
405
  ```
187
406
 
188
407
  ### validator
189
408
 
190
409
  ```typescript
191
410
  bus.use(validator({
192
- 'cart.add': (cmd) => {
411
+ 'cartAdd': (cmd) => {
193
412
  if (!cmd.target?.id) return 'Product must have an ID';
194
413
  return null; // null = valid
195
414
  }
@@ -207,60 +426,245 @@ historyPlugin.redo();
207
426
  historyPlugin.getState(); // { past, future, canUndo, canRedo }
208
427
  ```
209
428
 
429
+ With bus-backed undo (executes inverse handlers):
430
+
431
+ ```typescript
432
+ const historyPlugin = history({ maxSize: 100, bus });
433
+ bus.use(historyPlugin);
434
+
435
+ // If cartAdd was registered with { undo: fn }, calling undo() executes it
436
+ historyPlugin.undo();
437
+ ```
438
+
210
439
  ### debounce
211
440
 
212
441
  ```typescript
213
- bus.use(debounce(['search.query'], 300)); // wait 300ms after last call
442
+ bus.use(debounce(['searchQuery'], 300)); // wait 300ms after last call
214
443
  ```
215
444
 
216
445
  ### throttle
217
446
 
218
447
  ```typescript
219
- bus.use(throttle(['ui.scroll'], 100)); // max once per 100ms
448
+ bus.use(throttle(['uiScroll'], 100)); // max once per 100ms
449
+ ```
450
+
451
+ ### authGuard
452
+
453
+ ```typescript
454
+ bus.use(authGuard({
455
+ isAuthenticated: () => !!user.value,
456
+ protected: ['shopCart', 'shopWishlist'],
457
+ onUnauthenticated: (cmd) => router.push('/login'),
458
+ }));
459
+ ```
460
+
461
+ ### optimistic
462
+
463
+ ```typescript
464
+ bus.use(optimistic({
465
+ 'cartAdd': {
466
+ apply: (cmd) => {
467
+ cartCount.value++;
468
+ return () => { cartCount.value--; }; // rollback function
469
+ }
470
+ }
471
+ }));
472
+ ```
473
+
474
+ ### retry
475
+
476
+ Async plugin that retries failed dispatches with configurable backoff. Install on an `AsyncCommandBus`:
477
+
478
+ ```typescript
479
+ import { createAsyncCommandBus, retry } from 'vapor-chamber';
480
+
481
+ const bus = createAsyncCommandBus();
482
+
483
+ // All actions, exponential backoff (default)
484
+ bus.use(retry({ maxAttempts: 3, baseDelay: 200 }));
485
+
486
+ // Only retry network actions, fixed delay
487
+ bus.use(retry({
488
+ actions: ['api*'],
489
+ maxAttempts: 5,
490
+ baseDelay: 500,
491
+ strategy: 'fixed',
492
+ isRetryable: (err) => err.message !== 'Unauthorized',
493
+ }));
494
+ ```
495
+
496
+ ### persist
497
+
498
+ Auto-save state to localStorage after each successful command. Rehydrate on startup:
499
+
500
+ ```typescript
501
+ import { persist } from 'vapor-chamber';
502
+
503
+ const cartPersist = persist({
504
+ key: 'vc:cart',
505
+ getState: () => cartState.value,
506
+ });
507
+ bus.use(cartPersist);
508
+
509
+ // On app start — rehydrate before rendering
510
+ const saved = cartPersist.load();
511
+ if (saved) cartState.value = saved;
512
+
513
+ // Manual operations
514
+ cartPersist.save(); // force save now
515
+ cartPersist.clear(); // remove from storage
516
+
517
+ // Custom backend (sessionStorage, IndexedDB adapter, etc.)
518
+ bus.use(persist({ key: 'vc:cart', getState, storage: sessionStorage }));
519
+ ```
520
+
521
+ ### sync
522
+
523
+ Broadcast successful commands to all other open tabs via `BroadcastChannel`:
524
+
525
+ ```typescript
526
+ import { sync } from 'vapor-chamber';
527
+
528
+ const tabSync = sync(
529
+ {
530
+ channel: 'vapor-chamber:app',
531
+ filter: (cmd) => cmd.action.startsWith('cart') || cmd.action.startsWith('auth'),
532
+ },
533
+ bus // pass the bus so received messages are re-dispatched locally
534
+ );
535
+
536
+ bus.use(tabSync);
537
+
538
+ // Teardown (component unmount, app destroy)
539
+ tabSync.close();
540
+ tabSync.isOpen(); // → false
541
+ ```
542
+
543
+ ## Transport Layer
544
+
545
+ Send commands to a backend over HTTP, WebSocket, or SSE. Import from `vapor-chamber/transports`
546
+ or directly from `vapor-chamber`:
547
+
548
+ ### createHttpBridge
549
+
550
+ Async plugin that POSTs command envelopes to a backend endpoint. Unhandled commands (no local handler) fall through to the server:
551
+
552
+ ```typescript
553
+ import { createAsyncCommandBus } from 'vapor-chamber';
554
+ import { createHttpBridge } from 'vapor-chamber/transports';
555
+
556
+ const bus = createAsyncCommandBus({ onMissing: 'ignore' });
557
+
558
+ bus.use(createHttpBridge({
559
+ endpoint: '/api/commands',
560
+ csrf: true, // reads XSRF-TOKEN cookie / meta tag automatically
561
+ csrfCookieUrl: '/sanctum/csrf-cookie', // default; set '' to disable the refresh fetch
562
+ retry: 2, // retry up to 2 times on 5xx / 429 / 408
563
+ noRetry: ['paymentCharge', 'orderPlace'], // never retry non-idempotent commands
564
+ timeout: 8000, // ms
565
+ actions: ['order*'], // only forward order* actions; others stay local
566
+ }));
567
+
568
+ const result = await bus.dispatch('orderCreate', { items: cart });
569
+ // → POST /api/commands { command: 'orderCreate', target: { items: ... } }
570
+ ```
571
+
572
+ The backend response shape:
573
+ ```json
574
+ { "state": { "orderId": 42, "status": "pending" } }
575
+ ```
576
+ `result.value` will be the contents of `state`.
577
+
578
+ ### createWsBridge
579
+
580
+ WebSocket transport with auto-reconnect:
581
+
582
+ ```typescript
583
+ import { createWsBridge } from 'vapor-chamber/transports';
584
+
585
+ const ws = createWsBridge({
586
+ url: 'wss://api.example.com/commands',
587
+ actions: ['chat*', 'presence*'],
588
+ timeout: 10_000, // per-message response timeout, ms (default: 10_000)
589
+ maxQueueSize: 100, // max queued messages during disconnect (default: 100)
590
+ reconnect: true, // auto-reconnect on close (default: true)
591
+ maxReconnects: 10, // give up after N reconnect attempts (default: 10)
592
+ });
593
+ bus.use(ws);
594
+ ws.connect();
595
+
596
+ // Lifecycle
597
+ ws.isConnected(); // → boolean
598
+ ws.disconnect(); // intentional close — suppresses reconnect
599
+ ```
600
+
601
+ ### createSseBridge
602
+
603
+ Server-sent events — server pushes commands to the client:
604
+
605
+ ```typescript
606
+ import { createSseBridge } from 'vapor-chamber/transports';
607
+
608
+ bus.use(createSseBridge({
609
+ url: '/api/events',
610
+ }));
611
+ ```
612
+
613
+ ## HTTP Client
614
+
615
+ `postCommand` is exposed for use outside the transport plugin when you need direct HTTP control:
616
+
617
+ ```typescript
618
+ import { postCommand } from 'vapor-chamber';
619
+
620
+ const response = await postCommand('/api/commands', {
621
+ command: 'cartAdd',
622
+ target: product,
623
+ payload: { quantity: 2 },
624
+ }, {
625
+ csrf: true,
626
+ timeout: 5000,
627
+ retry: 2,
628
+ onSessionExpired: (status) => router.push('/login'),
629
+ });
220
630
  ```
221
631
 
222
632
  ## Batch Dispatch
223
633
 
224
- Dispatch multiple commands as a unit. Stops on the first failure:
634
+ Dispatch multiple commands as a unit. Stops on the first failure by default:
225
635
 
226
636
  ```typescript
227
637
  const result = bus.dispatchBatch([
228
- { action: 'cart.add', target: cart, payload: item },
229
- { action: 'totals.update', target: cart },
230
- { action: 'analytics.track', target: session, payload: item },
638
+ { action: 'cartAdd', target: cart, payload: item },
639
+ { action: 'totalsUpdate', target: cart },
640
+ { action: 'analyticsTrack', target: session, payload: item },
231
641
  ]);
232
642
 
233
643
  if (result.ok) {
234
644
  console.log('All succeeded:', result.results);
235
645
  } else {
236
646
  console.error('Stopped at failure:', result.error);
237
- console.log('Partial results:', result.results);
238
647
  }
239
648
  ```
240
649
 
241
- Works on both sync and async buses.
650
+ Use `continueOnError` to run all commands regardless of failures, then check counts:
651
+
652
+ ```typescript
653
+ const result = bus.dispatchBatch(commands, { continueOnError: true });
654
+ console.log(`${result.successCount} of ${result.results.length} succeeded`);
655
+ // result.failCount — how many failed
656
+ // result.results — all CommandResult objects, in order
657
+ ```
242
658
 
243
659
  ## Dead Letter Handling
244
660
 
245
661
  Configure what happens when a command has no registered handler:
246
662
 
247
663
  ```typescript
248
- // Default: returns { ok: false, error }
249
- createCommandBus()
250
-
251
- // Throw instead of returning an error result
252
- createCommandBus({ onMissing: 'throw' })
253
-
254
- // Silently succeed (useful for optional commands)
255
- createCommandBus({ onMissing: 'ignore' })
256
-
257
- // Custom fallback
258
- createCommandBus({
259
- onMissing: (cmd) => {
260
- console.warn(`Unhandled: ${cmd.action}`);
261
- return { ok: true, value: null };
262
- }
263
- })
664
+ createCommandBus() // default: returns { ok: false, error }
665
+ createCommandBus({ onMissing: 'throw' }) // throws the error
666
+ createCommandBus({ onMissing: 'ignore' }) // returns { ok: true, value: undefined }
667
+ createCommandBus({ onMissing: (cmd) => { ... } }) // custom fallback
264
668
  ```
265
669
 
266
670
  ## Async Command Bus
@@ -272,22 +676,22 @@ import { createAsyncCommandBus } from 'vapor-chamber';
272
676
 
273
677
  const bus = createAsyncCommandBus();
274
678
 
275
- bus.register('user.fetch', async (cmd) => {
679
+ bus.register('userFetch', async (cmd) => {
276
680
  const response = await fetch(`/api/users/${cmd.target.id}`);
277
681
  return response.json();
278
682
  });
279
683
 
280
- const result = await bus.dispatch('user.fetch', { id: 123 });
684
+ const result = await bus.dispatch('userFetch', { id: 123 });
281
685
  ```
282
686
 
283
687
  ## Vapor Composables
284
688
 
285
- For Vue Vapor components:
286
-
287
689
  ### useCommand
288
690
 
691
+ Dispatch commands with reactive loading/error state:
692
+
289
693
  ```vue
290
- <script setup>
694
+ <script setup vapor>
291
695
  import { useCommand } from 'vapor-chamber';
292
696
 
293
697
  const { dispatch, loading, lastError } = useCommand();
@@ -299,16 +703,36 @@ const { dispatch, loading, lastError } = useCommand();
299
703
  </template>
300
704
  ```
301
705
 
706
+ ### defineVaporCommand
707
+
708
+ Zero-overhead dispatch for hot paths — no reactive `loading`/`lastError` signals created.
709
+ Ideal for GA4 tracking, scroll events, debounced search, fire-and-forget patterns:
710
+
711
+ ```vue
712
+ <script setup vapor>
713
+ import { defineVaporCommand } from 'vapor-chamber';
714
+
715
+ const { dispatch } = defineVaporCommand('analyticsTrack', (cmd) => {
716
+ gtag('event', cmd.target.event, cmd.target.params);
717
+ });
718
+
719
+ // Fire-and-forget — no reactive overhead in the alien-signals graph
720
+ dispatch({ event: 'page_view', params: { page: '/shop' } });
721
+ </script>
722
+ ```
723
+
302
724
  ### useCommandState
303
725
 
726
+ State managed by commands:
727
+
304
728
  ```vue
305
- <script setup>
729
+ <script setup vapor>
306
730
  import { useCommandState } from 'vapor-chamber';
307
731
 
308
732
  const { state: cart } = useCommandState(
309
733
  { items: [], total: 0 },
310
734
  {
311
- 'cart.add': (state, cmd) => ({
735
+ 'cartAdd': (state, cmd) => ({
312
736
  items: [...state.items, cmd.target],
313
737
  total: state.total + cmd.target.price
314
738
  })
@@ -319,47 +743,146 @@ const { state: cart } = useCommandState(
319
743
 
320
744
  ### useCommandHistory
321
745
 
746
+ Reactive undo/redo:
747
+
322
748
  ```vue
323
- <script setup>
749
+ <script setup vapor>
324
750
  import { useCommandHistory } from 'vapor-chamber';
325
751
 
326
752
  const { canUndo, canRedo, undo, redo } = useCommandHistory({
327
- filter: (cmd) => cmd.action.startsWith('editor.')
753
+ filter: (cmd) => cmd.action.startsWith('editor_')
328
754
  });
329
755
  </script>
330
756
  ```
331
757
 
758
+ ### useCommandGroup
759
+
760
+ 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:
761
+
762
+ ```typescript
763
+ import { useCommandGroup } from 'vapor-chamber';
764
+
765
+ // Cart feature module
766
+ const cart = useCommandGroup('cart');
767
+ cart.register('add', handler); // registers 'cartAdd'
768
+ cart.dispatch('add', product); // dispatches 'cartAdd'
769
+ cart.on('*', listener); // listens to 'cart*'
770
+
771
+ // Orders feature — completely isolated
772
+ const orders = useCommandGroup('orders');
773
+ orders.dispatch('cancel', { id }); // dispatches 'ordersCancel'
774
+
775
+ // Access the namespace
776
+ cart.namespace; // → 'cart'
777
+ ```
778
+
779
+ Auto-cleanup on Vue scope disposal. `dispose()` is also available for manual teardown.
780
+
781
+ ### useCommandError
782
+
783
+ Component-scoped error boundary. Reactively captures all failed command results:
784
+
785
+ ```typescript
786
+ import { useCommandError } from 'vapor-chamber';
787
+
788
+ // Watch all failed commands
789
+ const { errors, latestError, clearErrors } = useCommandError();
790
+
791
+ // Narrow to a subset
792
+ const { latestError } = useCommandError({
793
+ filter: (cmd) => cmd.action.startsWith('cart'),
794
+ });
795
+
796
+ // In template
797
+ // latestError.value?.message
798
+ // errors.value.length
799
+ ```
800
+
801
+ ### createFormBus
802
+
803
+ Reactive form state manager built on the command bus. Per-field validation, dirty tracking, and full plugin pipeline on every form command:
804
+
805
+ ```typescript
806
+ import { createFormBus, logger } from 'vapor-chamber';
807
+
808
+ const form = createFormBus({
809
+ fields: { email: '', password: '' },
810
+ rules: {
811
+ // Sync rule — runs on every set() for live feedback
812
+ email: (v) => v.includes('@') ? null : 'Invalid email',
813
+ password: (v) => v.length >= 8 ? null : 'Too short',
814
+ // Async rule — only awaited on submit() (no UI jank during typing)
815
+ username: async (v) => {
816
+ const taken = await api.isUsernameTaken(v);
817
+ return taken ? 'Username already taken' : null;
818
+ },
819
+ },
820
+ onSubmit: async (values) => await api.login(values),
821
+ });
822
+
823
+ // Attach plugins — logger, throttle, authGuard, etc.
824
+ form.use(logger());
825
+
826
+ // Reactive state
827
+ form.values.value // { email: '', password: '' }
828
+ form.errors.value // { email: 'Invalid email', ... }
829
+ form.isDirty.value // true when any field has changed
830
+ form.isValid.value // true when no errors
831
+ form.isSubmitting.value // true while onSubmit is in flight
832
+
833
+ // Actions
834
+ form.set('email', 'user@example.com'); // updates field + re-runs validation
835
+ form.touch('email'); // marks field as interacted with
836
+ await form.submit(); // validate → onSubmit → returns bool
837
+ form.reset(); // restore initial values
838
+ ```
839
+
840
+ Template usage (Vue 3):
841
+
842
+ ```vue
843
+ <input :value="form.values.value.email"
844
+ @input="form.set('email', $event.target.value)"
845
+ @blur="form.touch('email')" />
846
+ <span v-if="form.touched.value.email && form.errors.value.email">
847
+ {{ form.errors.value.email }}
848
+ </span>
849
+ <button :disabled="!form.isValid.value || form.isSubmitting.value"
850
+ @click="form.submit()">
851
+ Submit
852
+ </button>
853
+ ```
854
+
332
855
  ### useCommandBus
333
856
 
334
- Lightweight composable for the "toolbox" patternimport only when needed, tree-shaken out of builds that don't use it. Returns the shared bus directly:
857
+ Lightweight access to the shared bus — tree-shakeable:
335
858
 
336
859
  ```typescript
337
860
  import { useCommandBus } from 'vapor-chamber';
338
861
 
339
862
  const bus = useCommandBus();
340
- bus.dispatch('cart.add', product, { quantity: 1 });
863
+ bus.dispatch('cartAdd', product, { quantity: 1 });
341
864
  ```
342
865
 
343
- Use `useCommand()` when you need reactive `loading`/`lastError` signals. Use `useCommandBus()` when you just need to dispatch.
866
+ Use `useCommand()` when you need reactive `loading`/`lastError` signals. Use `defineVaporCommand()` for zero-overhead hot paths. Use `useCommandBus()` when you just need to dispatch.
344
867
 
345
868
  ### configureSignal
346
869
 
347
- Inject Vue Vapor's native signal factory once at app setup. Falls back to a built-in shim automatically in non-Vapor environments (standard Vue 3, tests, SSR):
870
+ 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:
348
871
 
349
872
  ```typescript
350
- import { signal } from 'vue-vapor';
873
+ import { ref } from 'vue';
351
874
  import { configureSignal } from 'vapor-chamber';
352
875
 
353
- configureSignal(signal);
876
+ configureSignal(ref); // explicit — usually auto-detected
354
877
  ```
355
878
 
356
879
  ### Testing
357
880
 
358
- `createTestBus()` records all dispatched commands without executing real handlers. Use it to test components that call `dispatch` without wiring up the full application:
881
+ `createTestBus()` records all dispatched commands without executing real handlers:
359
882
 
360
883
  ```typescript
361
- import { createTestBus, setCommandBus } from 'vapor-chamber';
362
- import { describe, it, expect, beforeEach } from 'vitest';
884
+ import { createTestBus, setCommandBus, resetCommandBus } from 'vapor-chamber';
885
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
363
886
 
364
887
  describe('CartButton', () => {
365
888
  let bus: TestBus;
@@ -369,20 +892,39 @@ describe('CartButton', () => {
369
892
  setCommandBus(bus);
370
893
  });
371
894
 
372
- it('dispatches cart.add on click', () => {
895
+ afterEach(() => {
896
+ resetCommandBus();
897
+ });
898
+
899
+ it('dispatches cartAdd on click', () => {
373
900
  // ... render component, click button ...
374
- expect(bus.wasDispatched('cart.add')).toBe(true);
375
- expect(bus.getDispatched('cart.add')[0].cmd.payload).toEqual({ quantity: 1 });
901
+ expect(bus.wasDispatched('cartAdd')).toBe(true);
902
+ expect(bus.getDispatched('cartAdd')[0].cmd.payload).toEqual({ quantity: 1 });
376
903
  });
377
904
  });
378
905
  ```
379
906
 
380
- Register real handlers for actions you want to test deeply:
907
+ **Snapshot & time-travel** replay command sequences for debugging or testing:
381
908
 
382
909
  ```typescript
383
- bus.register('cart.add', (cmd) => {
384
- // real handler logic
385
- });
910
+ const bus = createTestBus();
911
+
912
+ bus.dispatch('login', user);
913
+ bus.dispatch('cartAdd', product, { quantity: 1 });
914
+ bus.dispatch('cartAdd', product2, { quantity: 2 });
915
+ bus.dispatch('checkout', cart);
916
+
917
+ // Immutable snapshot — mutations don't affect bus.recorded
918
+ const snap = bus.snapshot(); // → RecordedDispatch[]
919
+
920
+ // Commands 0..N inclusive (returns Command[])
921
+ bus.travelTo(1); // → [login, cartAdd]
922
+
923
+ // All commands up to last occurrence of 'cartAdd'
924
+ bus.travelToAction('cartAdd'); // → [login, cartAdd, cartAdd]
925
+
926
+ // Out-of-range indices are clamped
927
+ bus.travelTo(999); // → full history
386
928
  ```
387
929
 
388
930
  ### setupDevtools
@@ -398,12 +940,6 @@ setupDevtools(getCommandBus(), app);
398
940
  app.mount('#app');
399
941
  ```
400
942
 
401
- The inspector shows:
402
- - Every dispatched command with its action, target, payload, and result
403
- - Green `ok` / red `error` tags at a glance
404
- - Full detail (value or error message) when a command is selected
405
- - Filterable tree by action name
406
-
407
943
  ## Examples
408
944
 
409
945
  See the [`examples/`](./examples) folder for complete, runnable examples:
@@ -417,11 +953,6 @@ See the [`examples/`](./examples) folder for complete, runnable examples:
417
953
  | [`custom-plugins.ts`](./examples/custom-plugins.ts) | Analytics, auth guard, rate limiter plugins |
418
954
  | [`vue-vapor-component.vue`](./examples/vue-vapor-component.vue) | Full Vue Vapor todo app |
419
955
 
420
- Run TypeScript examples with:
421
- ```bash
422
- npx ts-node examples/shopping-cart.ts
423
- ```
424
-
425
956
  ## API Reference
426
957
 
427
958
  ### Core
@@ -430,64 +961,170 @@ npx ts-node examples/shopping-cart.ts
430
961
  |----------|-------------|
431
962
  | `createCommandBus(options?)` | Create a synchronous command bus |
432
963
  | `createAsyncCommandBus(options?)` | Create an async command bus |
433
- | `createTestBus(options?)` | Create a test bus that records dispatches (see [Testing](#testing)) |
964
+ | `createTestBus(options?)` | Create a test bus that records dispatches |
434
965
 
435
966
  **`CommandBusOptions`**
436
967
 
437
968
  | Option | Type | Default | Description |
438
969
  |--------|------|---------|-------------|
439
- | `onMissing` | `'error' \| 'throw' \| 'ignore' \| fn` | `'error'` | Behavior when no handler is registered for an action |
970
+ | `onMissing` | `'error' \| 'throw' \| 'ignore' \| fn` | `'error'` | Behavior when no handler is registered |
971
+ | `naming` | `{ pattern: RegExp, onViolation?: string }` | — | Enforce naming convention on actions |
440
972
 
441
973
  ### Command Bus Methods
442
974
 
443
975
  | Method | Description |
444
976
  |--------|-------------|
445
977
  | `dispatch(action, target, payload?)` | Execute a command |
446
- | `dispatchBatch(commands[])` | Execute multiple commands; stops on first failure |
447
- | `register(action, handler)` | Register a handler (returns unregister fn) |
448
- | `use(plugin, options?)` | Add a plugin (returns unsubscribe fn). `options.priority` controls order — higher runs first |
449
- | `onAfter(hook)` | Run callback after every command |
978
+ | `dispatchBatch(commands[], options?)` | Execute multiple commands. Returns `{ successCount, failCount, results }` |
979
+ | `register(action, handler, options?)` | Register a handler. Options: `{ undo?, throttle? }` |
980
+ | `use(plugin, options?)` | Add a plugin. `options.priority` controls order |
981
+ | `onBefore(hook)` | Run hook before every command. Throw to cancel dispatch. |
982
+ | `onAfter(hook)` | Run hook after every command |
983
+ | `on(pattern, listener)` | Subscribe to commands matching a pattern (`*`, `prefix*`, exact). Returns unsub. |
984
+ | `once(pattern, listener)` | Like `on()` but auto-unsubscribes after first match |
985
+ | `offAll(pattern?)` | Remove all listeners for a pattern, or all listeners if omitted |
986
+ | `request(action, target, payload?, options?)` | Async request/response with timeout (default 5s) |
987
+ | `respond(action, handler)` | Register a responder for `request()` calls |
988
+ | `hasHandler(action)` | Returns true if a handler is registered for the action |
989
+ | `clear()` | Remove all handlers, plugins, hooks, and listeners |
990
+ | `getUndoHandler(action)` | Get the undo handler for an action (`@internal`) |
450
991
 
451
992
  ### Composables
452
993
 
453
994
  | Composable | Description |
454
995
  |------------|-------------|
455
- | `useCommandBus()` | Get the shared bus lightweight, tree-shakeable |
456
- | `useCommand()` | Dispatch with reactive loading/error state. Returns `dispose()` to clean up registered handlers/plugins |
457
- | `useCommandState(initial, handlers)` | State managed by commands. Returns `dispose()` to unregister handlers |
458
- | `useCommandHistory(options?)` | Reactive undo/redo. Returns `dispose()` to unsubscribe |
459
- | `getCommandBus()` | Get shared bus instance |
996
+ | `useCommand()` | Dispatch with reactive loading/error state |
997
+ | `defineVaporCommand(action, handler, options?)` | Zero-overhead dispatch for hot paths |
998
+ | `useCommandState(initial, handlers)` | State managed by commands |
999
+ | `useCommandHistory(options?)` | Reactive undo/redo |
1000
+ | `useCommandGroup(namespace)` | Namespace isolation prefixes all calls in camelCase |
1001
+ | `useCommandError(options?)` | Reactive error boundary for failed dispatches |
1002
+ | `useCommandBus()` | Get shared bus instance |
1003
+ | `getCommandBus()` | Get shared bus instance (non-composable) |
460
1004
  | `setCommandBus(bus)` | Set shared bus instance |
461
- | `configureSignal(fn)` | Inject a custom signal factory (e.g. Vue Vapor's native `signal`) |
462
- | `setupDevtools(bus, app)` | Connect bus to Vue DevTools. No-ops automatically in production builds |
1005
+ | `resetCommandBus()` | Reset shared bus to null (useful in tests) |
1006
+ | `configureSignal(fn)` | Inject a custom signal factory |
1007
+ | `isVaporAvailable()` | Returns true if Vue 3.6+ Vapor mode is detected |
1008
+ | `createVaporChamberApp(component, props?)` | Create a Vapor app instance (requires Vue 3.6+) |
1009
+ | `getVaporInteropPlugin()` | Returns `vaporInteropPlugin` for mixed trees |
1010
+ | `setupDevtools(bus, app)` | Connect bus to Vue DevTools |
463
1011
 
464
1012
  ## Roadmap
465
1013
 
466
- | Feature | Status |
467
- |---------|--------|
468
- | DevTools integration | Done |
469
- | DevTools production strip (0KB in prod) | ✅ Done |
470
- | Command batching (`dispatchBatch`) | ✅ Done |
471
- | Middleware priority/ordering | ✅ Done |
472
- | Dead letter handling (`onMissing`) | ✅ Done |
473
- | Testing utilities (`createTestBus`) | ✅ Done |
474
- | Persistence plugin (localStorage / IndexedDB) | Planned |
475
- | SSR support | Planned (pending Vue Vapor stabilization) |
1014
+ ### Core target: 100% feature-complete at v1.0
1015
+
1016
+ | Feature | Module | Status | Tests |
1017
+ |---------|--------|--------|-------|
1018
+ | Dispatch / register / unregister | `command-bus` | ✅ v0.1.0 | ✅ 90% coverage |
1019
+ | Plugin pipeline (sync + async) | `command-bus` | v0.1.0 | ✅ 90% coverage |
1020
+ | Plugin priority ordering | `command-bus` | ✅ v0.2.0 | ✅ covered |
1021
+ | `onAfter` hooks | `command-bus` | ✅ v0.2.0 | ✅ covered |
1022
+ | Dead letter handling (`onMissing`) | `command-bus` | v0.2.0 | ✅ covered |
1023
+ | Command batching + `continueOnError` + `successCount`/`failCount` | `command-bus` | v0.6.0 | ✅ covered |
1024
+ | Naming convention enforcement | `command-bus` | ✅ v0.3.0 | ✅ covered |
1025
+ | Wildcard listeners (`on`, `prefix*`) | `command-bus` | ✅ v0.3.0 | ✅ covered |
1026
+ | `once()` — one-shot listener | `command-bus` | ✅ v0.6.0 | ✅ covered |
1027
+ | `offAll(pattern?)` — mass unsubscribe | `command-bus` | ✅ v0.6.0 | ✅ covered |
1028
+ | `onBefore(hook)` — pre-dispatch hook, cancelable | `command-bus` | ✅ v0.6.0 | ✅ covered |
1029
+ | Request / response pattern + timeout | `command-bus` | ✅ v0.3.0 | ✅ covered |
1030
+ | Per-command throttle + undo at register | `command-bus` | ✅ v0.3.0 | ✅ covered |
1031
+ | `bus.hasHandler()` introspection | `command-bus` | ✅ v0.3.0 | ✅ covered |
1032
+ | `bus.clear()` | `command-bus` | ✅ v0.5.0 | ✅ covered |
1033
+ | `BaseBus` structural interface | `command-bus` | ✅ v0.6.0 | ✅ covered |
1034
+ | `commandKey(action, target)` export | `command-bus` | ✅ v0.6.0 | ✅ covered |
1035
+ | SSR isolation (independent bus instances) | `command-bus` | ✅ v0.5.0 | ✅ covered |
1036
+ | `createTestBus` record + assert | `testing` | ✅ v0.2.0 | ✅ 96% coverage |
1037
+ | `createTestBus` snapshot & time-travel | `testing` | ✅ v0.4.3 | ✅ covered |
1038
+ | `TestBus.on()` / `once()` / `offAll()` real implementations | `testing` | ✅ v0.6.0 | ✅ covered |
1039
+
1040
+ ### Plugins — optional, fully implemented
1041
+
1042
+ | Feature | Module | Status | Tests |
1043
+ |---------|--------|--------|-------|
1044
+ | `logger` | `plugins-core` | ✅ v0.1.0 | ✅ 90% coverage |
1045
+ | `validator` | `plugins-core` | ✅ v0.1.0 | ✅ covered |
1046
+ | `history` + bus-backed undo/redo | `plugins-core` | ✅ v0.3.0 | ✅ covered |
1047
+ | `debounce` (stale-closure fix) | `plugins-core` | ✅ v0.3.0 | ✅ covered |
1048
+ | `throttle` | `plugins-core` | ✅ v0.3.0 | ✅ covered |
1049
+ | `authGuard` | `plugins-core` | ✅ v0.3.0 | ✅ covered |
1050
+ | `optimistic` | `plugins-core` | ✅ v0.3.0 | ✅ covered |
1051
+ | `retry` with configurable backoff + glob filter | `plugins-io` | ✅ v0.4.2 | ✅ 88% coverage |
1052
+ | `persist` (localStorage / custom storage) | `plugins-io` | ✅ v0.4.2 | ✅ covered |
1053
+ | `sync` (BroadcastChannel cross-tab) | `plugins-io` | ✅ v0.4.2 | ✅ covered |
1054
+
1055
+ ### Transport layer — optional, fully implemented
1056
+
1057
+ | Feature | Module | Status | Tests |
1058
+ |---------|--------|--------|-------|
1059
+ | `postCommand` — POST with retry, CSRF, timeout, session | `http` | ✅ v0.5.0 | ✅ 80% coverage |
1060
+ | `readCsrfToken` — meta / cookie / hidden input | `http` | ✅ v0.5.0 | ✅ covered |
1061
+ | `HttpError.code` — machine-readable code from response body | `http` | ✅ v0.6.0 | ✅ covered |
1062
+ | 419 vs 401 fix — CSRF expiry ≠ session expiry | `http` | ✅ v0.6.0 | ✅ covered |
1063
+ | `createHttpBridge` — fetch plugin | `transports` | ✅ v0.4.2 | ✅ 91% coverage |
1064
+ | `HttpBridgeOptions.noRetry` — per-action retry disable | `transports` | ✅ v0.6.0 | ✅ covered |
1065
+ | `createWsBridge` — WebSocket plugin + reconnect + bounded queue | `transports` | ✅ v0.6.0 | ✅ covered |
1066
+ | `createSseBridge` — server-push EventSource, accepts `BaseBus` | `transports` | ✅ v0.6.0 | ✅ covered |
1067
+
1068
+ ### Vue composables — optional, requires Vue ≥3.5
1069
+
1070
+ | Feature | Module | Status | Tests |
1071
+ |---------|--------|--------|-------|
1072
+ | `useCommand` — reactive loading/error | `chamber` | ✅ v0.1.0 | ✅ 76% coverage |
1073
+ | `useCommandState` | `chamber` | ✅ v0.2.0 | ✅ covered |
1074
+ | `useCommandHistory` — reactive undo/redo | `chamber` | ✅ v0.2.0 | ✅ covered |
1075
+ | `useCommandGroup` — namespace isolation | `chamber` | ✅ v0.4.1 | ✅ covered |
1076
+ | `useCommandError` — error boundary | `chamber` | ✅ v0.4.1 | ✅ covered |
1077
+ | `getCommandBus` / `setCommandBus` / `resetCommandBus` | `chamber` | ✅ v0.1.0 | ✅ covered |
1078
+ | Signal shim + `configureSignal` | `chamber` | ✅ v0.3.0 | ✅ covered |
1079
+ | `onScopeDispose` lifecycle alignment | `chamber` | ✅ v0.4.0 | ✅ covered |
1080
+ | `isVaporAvailable()` | `chamber` | ✅ v0.4.0 | ✅ covered |
1081
+ | `createVaporChamberApp` / `getVaporInteropPlugin` / `defineVaporCommand` | `chamber-vapor` | ✅ v0.4.0 | ⚠️ requires Vue 3.6 runtime |
1082
+
1083
+ ### Extras — optional, per-feature opt-in
1084
+
1085
+ | Feature | Module | Status | Tests |
1086
+ |---------|--------|--------|-------|
1087
+ | `createFormBus` — reactive form + sync/async validation | `form` | ✅ v0.6.0 | ✅ 99% coverage |
1088
+ | Schema layer — `createSchemaCommandBus`, `toTools`, `synthesize` | `schema` | ✅ v0.5.0 | ✅ 92% coverage |
1089
+ | `SynthesizeOptions.adapter` — custom LLM adapter | `schema` | ✅ v0.6.0 | ✅ covered |
1090
+ | `setupDevtools` — Vue DevTools panel | `devtools` | ✅ v0.4.0 | ⚠️ requires browser DevTools API |
1091
+ | `createDirectivePlugin` — `v-command` directive | `directives` | ✅ v0.5.0 | ⚠️ requires Vue DOM environment |
1092
+ | Vite HMR plugin | `vite-hmr` | ✅ v0.5.0 | ⚠️ requires Vite runtime |
1093
+ | IIFE / CDN bundle | `iife` | ✅ v0.5.0 | 🔧 bundle entry |
1094
+
1095
+ ### v1.0 checklist
1096
+
1097
+ | Item | Status |
1098
+ |------|--------|
1099
+ | Core (`command-bus` + `testing`) at 90%+ coverage | ✅ Done |
1100
+ | All tests green (318/318, 0 failures) | ✅ Done |
1101
+ | Optional modules clearly marked in exports | ✅ Done |
1102
+ | Transport layer fully tested (HTTP + WS + SSE) | ✅ Done |
1103
+ | Plugins fully tested | ✅ Done |
1104
+ | camelCase naming convention locked in | ✅ Done |
1105
+ | `onBefore` / `offAll` / `once` on both buses | ✅ Done (v0.6.0) |
1106
+ | `BaseBus` structural interface for cross-bus utilities | ✅ Done (v0.6.0) |
1107
+ | CSRF / 419 / session-expiry correctness | ✅ Done (v0.6.0) |
1108
+ | Form async validation | ✅ Done (v0.6.0) |
1109
+ | `HttpError.code` structured error codes | ✅ Done (v0.6.0) |
1110
+ | WS queue cap (`maxQueueSize`) | ✅ Done (v0.6.0) |
1111
+ | `synthesize` LLM adapter (proxy / OpenAI support) | ✅ Done (v0.6.0) |
1112
+ | Architectural whitepaper | ✅ Done (v0.6.0) |
1113
+ | `chamber.ts` branch coverage | 🔄 76% → target 85% |
1114
+ | Publish to npm as `vapor-chamber@1.0.0` | ⬜ Pending |
476
1115
 
477
1116
  ## Documentation
478
1117
 
479
- See the [`docs/`](./docs) folder for detailed documentation:
480
-
481
- - [Whitepaper](./docs/whitepaper.md) - Design philosophy and architecture
482
- - [SSR Guide](./docs/ssr.md) - Server-side rendering and hydration
1118
+ See [`docs/whitepaper.md`](./docs/whitepaper.md) for design philosophy, architecture, camelCase naming rationale, Vue 3.6 Vapor alignment, SSR guide, and migration strategy.
483
1119
 
484
1120
  ## Design Goals
485
1121
 
486
- 1. **Minimal** - ~1KB core, no dependencies
487
- 2. **Vapor-native** - Built for signals, not VDOM
488
- 3. **Composable** - Plugins for everything
489
- 4. **Type-safe** - Full TypeScript support
490
- 5. **Predictable** - Sync by default, explicit async
1122
+ 1. **Minimal** ~1KB core, no dependencies
1123
+ 2. **Vapor-native** Built for signals, not VDOM
1124
+ 3. **Composable** Plugins for everything
1125
+ 4. **Type-safe** Full TypeScript support
1126
+ 5. **Predictable** Sync by default, explicit async
1127
+ 6. **Progressive** — Works in VDOM, Vapor, and mixed trees
491
1128
 
492
1129
  ## License
493
1130