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