vapor-chamber 0.4.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.
- package/README.md +549 -68
- package/dist/chamber-vapor.d.ts +48 -0
- package/dist/chamber-vapor.d.ts.map +1 -0
- package/dist/chamber-vapor.js +62 -0
- package/dist/chamber.d.ts +68 -51
- package/dist/chamber.d.ts.map +1 -1
- package/dist/chamber.js +100 -92
- package/dist/command-bus.d.ts +118 -28
- package/dist/command-bus.d.ts.map +1 -1
- package/dist/command-bus.js +407 -250
- package/dist/directives.d.ts +37 -0
- package/dist/directives.d.ts.map +1 -0
- package/dist/directives.js +190 -0
- package/dist/form.d.ts +72 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +159 -0
- package/dist/http.d.ts +60 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +242 -0
- package/dist/iife.d.ts +95 -0
- package/dist/iife.d.ts.map +1 -0
- package/dist/iife.js +90 -0
- package/dist/index.d.ts +45 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +59 -10
- package/dist/plugins-core.d.ts +69 -0
- package/dist/plugins-core.d.ts.map +1 -0
- package/dist/plugins-core.js +210 -0
- package/dist/plugins-io.d.ts +100 -0
- package/dist/plugins-io.d.ts.map +1 -0
- package/dist/plugins-io.js +171 -0
- package/dist/plugins.d.ts +6 -76
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +6 -211
- package/dist/schema.d.ts +162 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +266 -0
- package/dist/testing.d.ts +29 -4
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +76 -21
- package/dist/transports.d.ts +168 -0
- package/dist/transports.d.ts.map +1 -0
- package/dist/transports.js +225 -0
- package/dist/vapor-chamber.iife.js +1251 -0
- package/dist/vapor-chamber.iife.js.map +7 -0
- package/dist/vapor-chamber.iife.min.js +2 -0
- package/dist/vite-hmr.d.ts +50 -0
- package/dist/vite-hmr.d.ts.map +1 -0
- package/dist/vite-hmr.js +110 -0
- package/package.json +23 -3
- 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('
|
|
66
|
+
bus.dispatch('cartAdd', product, { quantity: 1 });
|
|
41
67
|
|
|
42
68
|
// One place, once:
|
|
43
|
-
bus.register('
|
|
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({ '
|
|
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('
|
|
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** — `
|
|
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 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
|
+
---
|
|
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
|
-
'
|
|
171
|
+
'cartAdd': (cmd) => cmd.payload?.quantity > 0 ? null : 'Quantity required'
|
|
95
172
|
}));
|
|
96
173
|
|
|
97
174
|
// Register handler
|
|
98
|
-
bus.register('
|
|
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('
|
|
181
|
+
const result = bus.dispatch('cartAdd', product, { quantity: 2 });
|
|
105
182
|
if (result.ok) {
|
|
106
183
|
console.log('Added:', result.value);
|
|
107
184
|
} else {
|
|
@@ -167,7 +244,7 @@ A command has three parts:
|
|
|
167
244
|
|
|
168
245
|
```typescript
|
|
169
246
|
bus.dispatch(
|
|
170
|
-
'
|
|
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-
|
|
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('
|
|
189
|
-
bus.register('
|
|
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('
|
|
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('
|
|
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,16 @@ 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('
|
|
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());
|
|
257
361
|
|
|
258
|
-
//
|
|
259
|
-
bus.
|
|
362
|
+
// Remove all listeners for a pattern
|
|
363
|
+
bus.offAll('cart*');
|
|
364
|
+
|
|
365
|
+
// Remove all listeners
|
|
366
|
+
bus.offAll();
|
|
260
367
|
```
|
|
261
368
|
|
|
262
369
|
### Request / Response
|
|
@@ -287,18 +394,21 @@ Falls back to normal `dispatch()` if no responder is registered.
|
|
|
287
394
|
| `throttle(actions, wait)` | Limit execution frequency |
|
|
288
395
|
| `authGuard(options)` | Block protected commands when unauthenticated |
|
|
289
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 |
|
|
290
400
|
|
|
291
401
|
### logger
|
|
292
402
|
|
|
293
403
|
```typescript
|
|
294
|
-
bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('
|
|
404
|
+
bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('cart') }));
|
|
295
405
|
```
|
|
296
406
|
|
|
297
407
|
### validator
|
|
298
408
|
|
|
299
409
|
```typescript
|
|
300
410
|
bus.use(validator({
|
|
301
|
-
'
|
|
411
|
+
'cartAdd': (cmd) => {
|
|
302
412
|
if (!cmd.target?.id) return 'Product must have an ID';
|
|
303
413
|
return null; // null = valid
|
|
304
414
|
}
|
|
@@ -322,20 +432,20 @@ With bus-backed undo (executes inverse handlers):
|
|
|
322
432
|
const historyPlugin = history({ maxSize: 100, bus });
|
|
323
433
|
bus.use(historyPlugin);
|
|
324
434
|
|
|
325
|
-
// If
|
|
435
|
+
// If cartAdd was registered with { undo: fn }, calling undo() executes it
|
|
326
436
|
historyPlugin.undo();
|
|
327
437
|
```
|
|
328
438
|
|
|
329
439
|
### debounce
|
|
330
440
|
|
|
331
441
|
```typescript
|
|
332
|
-
bus.use(debounce(['
|
|
442
|
+
bus.use(debounce(['searchQuery'], 300)); // wait 300ms after last call
|
|
333
443
|
```
|
|
334
444
|
|
|
335
445
|
### throttle
|
|
336
446
|
|
|
337
447
|
```typescript
|
|
338
|
-
bus.use(throttle(['
|
|
448
|
+
bus.use(throttle(['uiScroll'], 100)); // max once per 100ms
|
|
339
449
|
```
|
|
340
450
|
|
|
341
451
|
### authGuard
|
|
@@ -343,7 +453,7 @@ bus.use(throttle(['ui_scroll'], 100)); // max once per 100ms
|
|
|
343
453
|
```typescript
|
|
344
454
|
bus.use(authGuard({
|
|
345
455
|
isAuthenticated: () => !!user.value,
|
|
346
|
-
protected: ['
|
|
456
|
+
protected: ['shopCart', 'shopWishlist'],
|
|
347
457
|
onUnauthenticated: (cmd) => router.push('/login'),
|
|
348
458
|
}));
|
|
349
459
|
```
|
|
@@ -352,7 +462,7 @@ bus.use(authGuard({
|
|
|
352
462
|
|
|
353
463
|
```typescript
|
|
354
464
|
bus.use(optimistic({
|
|
355
|
-
'
|
|
465
|
+
'cartAdd': {
|
|
356
466
|
apply: (cmd) => {
|
|
357
467
|
cartCount.value++;
|
|
358
468
|
return () => { cartCount.value--; }; // rollback function
|
|
@@ -361,15 +471,173 @@ bus.use(optimistic({
|
|
|
361
471
|
}));
|
|
362
472
|
```
|
|
363
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
|
+
});
|
|
630
|
+
```
|
|
631
|
+
|
|
364
632
|
## Batch Dispatch
|
|
365
633
|
|
|
366
|
-
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:
|
|
367
635
|
|
|
368
636
|
```typescript
|
|
369
637
|
const result = bus.dispatchBatch([
|
|
370
|
-
{ action: '
|
|
371
|
-
{ action: '
|
|
372
|
-
{ action: '
|
|
638
|
+
{ action: 'cartAdd', target: cart, payload: item },
|
|
639
|
+
{ action: 'totalsUpdate', target: cart },
|
|
640
|
+
{ action: 'analyticsTrack', target: session, payload: item },
|
|
373
641
|
]);
|
|
374
642
|
|
|
375
643
|
if (result.ok) {
|
|
@@ -379,6 +647,15 @@ if (result.ok) {
|
|
|
379
647
|
}
|
|
380
648
|
```
|
|
381
649
|
|
|
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
|
+
```
|
|
658
|
+
|
|
382
659
|
## Dead Letter Handling
|
|
383
660
|
|
|
384
661
|
Configure what happens when a command has no registered handler:
|
|
@@ -399,12 +676,12 @@ import { createAsyncCommandBus } from 'vapor-chamber';
|
|
|
399
676
|
|
|
400
677
|
const bus = createAsyncCommandBus();
|
|
401
678
|
|
|
402
|
-
bus.register('
|
|
679
|
+
bus.register('userFetch', async (cmd) => {
|
|
403
680
|
const response = await fetch(`/api/users/${cmd.target.id}`);
|
|
404
681
|
return response.json();
|
|
405
682
|
});
|
|
406
683
|
|
|
407
|
-
const result = await bus.dispatch('
|
|
684
|
+
const result = await bus.dispatch('userFetch', { id: 123 });
|
|
408
685
|
```
|
|
409
686
|
|
|
410
687
|
## Vapor Composables
|
|
@@ -435,7 +712,7 @@ Ideal for GA4 tracking, scroll events, debounced search, fire-and-forget pattern
|
|
|
435
712
|
<script setup vapor>
|
|
436
713
|
import { defineVaporCommand } from 'vapor-chamber';
|
|
437
714
|
|
|
438
|
-
const { dispatch } = defineVaporCommand('
|
|
715
|
+
const { dispatch } = defineVaporCommand('analyticsTrack', (cmd) => {
|
|
439
716
|
gtag('event', cmd.target.event, cmd.target.params);
|
|
440
717
|
});
|
|
441
718
|
|
|
@@ -455,7 +732,7 @@ import { useCommandState } from 'vapor-chamber';
|
|
|
455
732
|
const { state: cart } = useCommandState(
|
|
456
733
|
{ items: [], total: 0 },
|
|
457
734
|
{
|
|
458
|
-
'
|
|
735
|
+
'cartAdd': (state, cmd) => ({
|
|
459
736
|
items: [...state.items, cmd.target],
|
|
460
737
|
total: state.total + cmd.target.price
|
|
461
738
|
})
|
|
@@ -478,6 +755,103 @@ const { canUndo, canRedo, undo, redo } = useCommandHistory({
|
|
|
478
755
|
</script>
|
|
479
756
|
```
|
|
480
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
|
+
|
|
481
855
|
### useCommandBus
|
|
482
856
|
|
|
483
857
|
Lightweight access to the shared bus — tree-shakeable:
|
|
@@ -486,7 +860,7 @@ Lightweight access to the shared bus — tree-shakeable:
|
|
|
486
860
|
import { useCommandBus } from 'vapor-chamber';
|
|
487
861
|
|
|
488
862
|
const bus = useCommandBus();
|
|
489
|
-
bus.dispatch('
|
|
863
|
+
bus.dispatch('cartAdd', product, { quantity: 1 });
|
|
490
864
|
```
|
|
491
865
|
|
|
492
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.
|
|
@@ -507,9 +881,8 @@ configureSignal(ref); // explicit — usually auto-detected
|
|
|
507
881
|
`createTestBus()` records all dispatched commands without executing real handlers:
|
|
508
882
|
|
|
509
883
|
```typescript
|
|
510
|
-
import { createTestBus, setCommandBus } from 'vapor-chamber';
|
|
884
|
+
import { createTestBus, setCommandBus, resetCommandBus } from 'vapor-chamber';
|
|
511
885
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
512
|
-
import { resetCommandBus } from 'vapor-chamber';
|
|
513
886
|
|
|
514
887
|
describe('CartButton', () => {
|
|
515
888
|
let bus: TestBus;
|
|
@@ -523,14 +896,37 @@ describe('CartButton', () => {
|
|
|
523
896
|
resetCommandBus();
|
|
524
897
|
});
|
|
525
898
|
|
|
526
|
-
it('dispatches
|
|
899
|
+
it('dispatches cartAdd on click', () => {
|
|
527
900
|
// ... render component, click button ...
|
|
528
|
-
expect(bus.wasDispatched('
|
|
529
|
-
expect(bus.getDispatched('
|
|
901
|
+
expect(bus.wasDispatched('cartAdd')).toBe(true);
|
|
902
|
+
expect(bus.getDispatched('cartAdd')[0].cmd.payload).toEqual({ quantity: 1 });
|
|
530
903
|
});
|
|
531
904
|
});
|
|
532
905
|
```
|
|
533
906
|
|
|
907
|
+
**Snapshot & time-travel** — replay command sequences for debugging or testing:
|
|
908
|
+
|
|
909
|
+
```typescript
|
|
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
|
|
928
|
+
```
|
|
929
|
+
|
|
534
930
|
### setupDevtools
|
|
535
931
|
|
|
536
932
|
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:
|
|
@@ -579,14 +975,19 @@ See the [`examples/`](./examples) folder for complete, runnable examples:
|
|
|
579
975
|
| Method | Description |
|
|
580
976
|
|--------|-------------|
|
|
581
977
|
| `dispatch(action, target, payload?)` | Execute a command |
|
|
582
|
-
| `dispatchBatch(commands[])` | Execute multiple commands
|
|
583
|
-
| `register(action, handler, options?)` | Register a handler. Options: `{ undo?, throttle
|
|
978
|
+
| `dispatchBatch(commands[], options?)` | Execute multiple commands. Returns `{ successCount, failCount, results }` |
|
|
979
|
+
| `register(action, handler, options?)` | Register a handler. Options: `{ undo?, throttle? }` |
|
|
584
980
|
| `use(plugin, options?)` | Add a plugin. `options.priority` controls order |
|
|
585
|
-
| `
|
|
586
|
-
| `
|
|
587
|
-
| `
|
|
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) |
|
|
588
987
|
| `respond(action, handler)` | Register a responder for `request()` calls |
|
|
589
|
-
| `
|
|
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`) |
|
|
590
991
|
|
|
591
992
|
### Composables
|
|
592
993
|
|
|
@@ -596,6 +997,8 @@ See the [`examples/`](./examples) folder for complete, runnable examples:
|
|
|
596
997
|
| `defineVaporCommand(action, handler, options?)` | Zero-overhead dispatch for hot paths |
|
|
597
998
|
| `useCommandState(initial, handlers)` | State managed by commands |
|
|
598
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 |
|
|
599
1002
|
| `useCommandBus()` | Get shared bus instance |
|
|
600
1003
|
| `getCommandBus()` | Get shared bus instance (non-composable) |
|
|
601
1004
|
| `setCommandBus(bus)` | Set shared bus instance |
|
|
@@ -608,33 +1011,111 @@ See the [`examples/`](./examples) folder for complete, runnable examples:
|
|
|
608
1011
|
|
|
609
1012
|
## Roadmap
|
|
610
1013
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
|
614
|
-
|
|
615
|
-
|
|
|
616
|
-
|
|
|
617
|
-
|
|
|
618
|
-
|
|
|
619
|
-
|
|
|
620
|
-
|
|
|
621
|
-
|
|
|
622
|
-
|
|
|
623
|
-
|
|
|
624
|
-
|
|
|
625
|
-
|
|
|
626
|
-
|
|
|
627
|
-
|
|
|
628
|
-
|
|
|
629
|
-
|
|
|
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 |
|
|
630
1115
|
|
|
631
1116
|
## Documentation
|
|
632
1117
|
|
|
633
|
-
See
|
|
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
|
|
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.
|
|
638
1119
|
|
|
639
1120
|
## Design Goals
|
|
640
1121
|
|