vapor-chamber 0.1.0 → 0.4.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 +396 -47
- package/dist/chamber.d.ts +87 -5
- package/dist/chamber.d.ts.map +1 -1
- package/dist/chamber.js +270 -29
- package/dist/command-bus.d.ts +80 -18
- package/dist/command-bus.d.ts.map +1 -1
- package/dist/command-bus.js +272 -43
- package/dist/devtools.d.ts +33 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +155 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/plugins.d.ts +37 -3
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +96 -12
- package/dist/testing.d.ts +43 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +148 -0
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -1,13 +1,58 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/vapor-chamber.png" alt="Vapor Chamber">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<p align="center">
|
|
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
|
+
</p>
|
|
4
8
|
|
|
5
9
|
## What is Vue Vapor?
|
|
6
10
|
|
|
7
|
-
Vue Vapor is Vue's
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
**As of Vue 3.6 beta**, Vapor mode is feature-complete for all stable APIs. The reactivity engine has been rewritten atop [alien-signals](https://github.com/stackblitz/alien-signals), delivering ~14% less memory and faster dependency tracking. `ref()` is now a signal internally.
|
|
8
14
|
|
|
9
15
|
**Vapor Chamber** embraces this philosophy: minimal abstraction, direct updates, signal-native reactivity.
|
|
10
16
|
|
|
17
|
+
## Migrating from Vue 3 emitters
|
|
18
|
+
|
|
19
|
+
If you're already using Vue 3's `emit` / `eventBus` pattern, here's the before and after:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
// Before — Vue 3 emitter
|
|
23
|
+
// cart.vue
|
|
24
|
+
emit('cart:add', product);
|
|
25
|
+
|
|
26
|
+
// App.vue
|
|
27
|
+
bus.on('cart:add', (product) => {
|
|
28
|
+
cart.items.push(product);
|
|
29
|
+
analytics.track('add');
|
|
30
|
+
validate(product); // where does this live?
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ProductList.vue — also listens?
|
|
34
|
+
bus.on('cart:add', updateBadge); // now two handlers, hard to trace
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
// After — Vapor Chamber
|
|
39
|
+
// Anywhere in the app
|
|
40
|
+
bus.dispatch('cart_add', product, { quantity: 1 });
|
|
41
|
+
|
|
42
|
+
// One place, once:
|
|
43
|
+
bus.register('cart_add', (cmd) => {
|
|
44
|
+
cart.items.push(cmd.target);
|
|
45
|
+
return cart.items;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Cross-cutting concerns as plugins, not scattered listeners:
|
|
49
|
+
bus.use(logger());
|
|
50
|
+
bus.use(validator({ 'cart_add': (cmd) => cmd.target.id ? null : 'Missing ID' }));
|
|
51
|
+
bus.use(analyticsPlugin);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**The key difference:** `emit` is fire-and-forget with many listeners. `dispatch` has one handler and a composable plugin pipeline — one place to look, debug, and test.
|
|
55
|
+
|
|
11
56
|
## Why a Command Bus?
|
|
12
57
|
|
|
13
58
|
Traditional event systems scatter logic across components. A command bus centralizes it:
|
|
@@ -15,7 +60,7 @@ Traditional event systems scatter logic across components. A command bus central
|
|
|
15
60
|
```
|
|
16
61
|
Event-driven (scattered) Command bus (centralized)
|
|
17
62
|
───────────────────────── ─────────────────────────
|
|
18
|
-
Component A emits 'add' → dispatch('
|
|
63
|
+
Component A emits 'add' → dispatch('cart_add', product)
|
|
19
64
|
Component B listens... ↓
|
|
20
65
|
Component C also listens... Handler executes once
|
|
21
66
|
Who handles what? When? Plugins observe/modify
|
|
@@ -23,10 +68,10 @@ Who handles what? When? Plugins observe/modify
|
|
|
23
68
|
```
|
|
24
69
|
|
|
25
70
|
**Benefits:**
|
|
26
|
-
- **Semantic actions**
|
|
27
|
-
- **Single handler**
|
|
28
|
-
- **Plugin pipeline**
|
|
29
|
-
- **Undo/redo**
|
|
71
|
+
- **Semantic actions** — `cart_add` is clearer than `emit('add')`
|
|
72
|
+
- **Single handler** — One place to look, debug, test
|
|
73
|
+
- **Plugin pipeline** — Cross-cutting concerns (logging, validation, analytics) without cluttering handlers
|
|
74
|
+
- **Undo/redo** — Command history is natural when actions are explicit
|
|
30
75
|
|
|
31
76
|
## Install
|
|
32
77
|
|
|
@@ -34,6 +79,8 @@ Who handles what? When? Plugins observe/modify
|
|
|
34
79
|
npm install vapor-chamber
|
|
35
80
|
```
|
|
36
81
|
|
|
82
|
+
**Requirements:** Node.js ≥20.19.0 | Vue ≥3.5.0 (optional peer dep) | Vite 7/8 compatible
|
|
83
|
+
|
|
37
84
|
## Quick Start
|
|
38
85
|
|
|
39
86
|
```typescript
|
|
@@ -44,17 +91,17 @@ const bus = createCommandBus();
|
|
|
44
91
|
// Add plugins
|
|
45
92
|
bus.use(logger());
|
|
46
93
|
bus.use(validator({
|
|
47
|
-
'
|
|
94
|
+
'cart_add': (cmd) => cmd.payload?.quantity > 0 ? null : 'Quantity required'
|
|
48
95
|
}));
|
|
49
96
|
|
|
50
97
|
// Register handler
|
|
51
|
-
bus.register('
|
|
98
|
+
bus.register('cart_add', (cmd) => {
|
|
52
99
|
cart.items.push({ ...cmd.target, quantity: cmd.payload.quantity });
|
|
53
100
|
return cart.items;
|
|
54
101
|
});
|
|
55
102
|
|
|
56
103
|
// Dispatch
|
|
57
|
-
const result = bus.dispatch('
|
|
104
|
+
const result = bus.dispatch('cart_add', product, { quantity: 2 });
|
|
58
105
|
if (result.ok) {
|
|
59
106
|
console.log('Added:', result.value);
|
|
60
107
|
} else {
|
|
@@ -62,6 +109,56 @@ if (result.ok) {
|
|
|
62
109
|
}
|
|
63
110
|
```
|
|
64
111
|
|
|
112
|
+
## Vue 3.6 Vapor Mode
|
|
113
|
+
|
|
114
|
+
Vapor Chamber v0.4.0 is aligned with Vue 3.6 beta. It works in three contexts:
|
|
115
|
+
|
|
116
|
+
### 1. Pure Vapor App (smallest bundle)
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { createVaporChamberApp, getCommandBus } from 'vapor-chamber';
|
|
120
|
+
import App from './App.vue';
|
|
121
|
+
|
|
122
|
+
// No VDOM runtime — ~10KB baseline
|
|
123
|
+
createVaporChamberApp(App).mount('#app');
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```vue
|
|
127
|
+
<script setup vapor>
|
|
128
|
+
import { useCommand } from 'vapor-chamber';
|
|
129
|
+
|
|
130
|
+
const { dispatch, loading } = useCommand();
|
|
131
|
+
</script>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 2. Mixed VDOM + Vapor (gradual migration)
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { createApp } from 'vue';
|
|
138
|
+
import { getVaporInteropPlugin } from 'vapor-chamber';
|
|
139
|
+
|
|
140
|
+
const app = createApp(App);
|
|
141
|
+
const interop = getVaporInteropPlugin();
|
|
142
|
+
if (interop) app.use(interop);
|
|
143
|
+
app.mount('#app');
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Now Vapor and VDOM components can nest inside each other. Useful for incremental migration.
|
|
147
|
+
|
|
148
|
+
### 3. Standard Vue 3 (no Vapor)
|
|
149
|
+
|
|
150
|
+
Everything works without Vapor. The signal shim auto-detects Vue's `ref()` for reactivity. In Vue 3.6+ this is alien-signals backed.
|
|
151
|
+
|
|
152
|
+
### Vapor Detection
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { isVaporAvailable } from 'vapor-chamber';
|
|
156
|
+
|
|
157
|
+
if (isVaporAvailable()) {
|
|
158
|
+
// Vue 3.6+ with createVaporApp available
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
65
162
|
## Core Concepts
|
|
66
163
|
|
|
67
164
|
### Commands
|
|
@@ -70,27 +167,48 @@ A command has three parts:
|
|
|
70
167
|
|
|
71
168
|
```typescript
|
|
72
169
|
bus.dispatch(
|
|
73
|
-
'
|
|
170
|
+
'cart_add', // action - what to do
|
|
74
171
|
product, // target - what to act on
|
|
75
172
|
{ quantity: 2 } // payload - additional data (optional)
|
|
76
173
|
);
|
|
77
174
|
```
|
|
78
175
|
|
|
176
|
+
### Naming Convention
|
|
177
|
+
|
|
178
|
+
Enforce consistent action names at register and dispatch time:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const bus = createCommandBus({
|
|
182
|
+
naming: {
|
|
183
|
+
pattern: /^[a-z][a-z0-9]*(_[a-z][a-z0-9]*)+$/, // snake_case
|
|
184
|
+
onViolation: 'throw' // or 'warn' or 'ignore'
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
bus.register('cart_add', handler); // ✓ passes
|
|
189
|
+
bus.register('cartAdd', handler); // ✗ throws
|
|
190
|
+
```
|
|
191
|
+
|
|
79
192
|
### Handlers
|
|
80
193
|
|
|
81
194
|
One handler per action. Returns a value or throws:
|
|
82
195
|
|
|
83
196
|
```typescript
|
|
84
|
-
bus.register('
|
|
85
|
-
// cmd.action = 'cart.add'
|
|
86
|
-
// cmd.target = product
|
|
87
|
-
// cmd.payload = { quantity: 2 }
|
|
88
|
-
|
|
197
|
+
bus.register('cart_add', (cmd) => {
|
|
89
198
|
cart.items.push(cmd.target);
|
|
90
199
|
return cart.items; // becomes result.value
|
|
91
200
|
});
|
|
92
201
|
```
|
|
93
202
|
|
|
203
|
+
Register with options for undo support and per-command throttling:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
bus.register('cart_add', addHandler, {
|
|
207
|
+
undo: (cmd) => { cart.items.pop(); },
|
|
208
|
+
throttle: 300, // max once per 300ms per target
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
94
212
|
### Results
|
|
95
213
|
|
|
96
214
|
Every dispatch returns a result:
|
|
@@ -118,7 +236,45 @@ const timingPlugin: Plugin = (cmd, next) => {
|
|
|
118
236
|
bus.use(timingPlugin);
|
|
119
237
|
```
|
|
120
238
|
|
|
121
|
-
Plugins execute
|
|
239
|
+
Plugins execute by priority (highest first), then registration order for equal priorities:
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
bus.use(validatorPlugin, { priority: 10 }); // runs first
|
|
243
|
+
bus.use(analyticsPlugin, { priority: 1 }); // runs after validation
|
|
244
|
+
bus.use(loggerPlugin); // priority 0 (default, runs last)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Wildcard Listeners
|
|
248
|
+
|
|
249
|
+
Subscribe to command patterns without being a handler:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// All commands
|
|
253
|
+
bus.on('*', (cmd, result) => analytics.track(cmd.action));
|
|
254
|
+
|
|
255
|
+
// Prefix matching
|
|
256
|
+
bus.on('shop_*', (cmd, result) => console.log('Shop event:', cmd.action));
|
|
257
|
+
|
|
258
|
+
// Exact match
|
|
259
|
+
bus.on('cart_add', (cmd, result) => updateBadge());
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Request / Response
|
|
263
|
+
|
|
264
|
+
Async request/response pattern with timeout:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// Register a responder
|
|
268
|
+
bus.respond('get_auth_token', async (cmd) => {
|
|
269
|
+
const response = await fetch('/api/token');
|
|
270
|
+
return response.json();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Request with timeout
|
|
274
|
+
const result = await bus.request('get_auth_token', { userId: 42 }, { timeout: 3000 });
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Falls back to normal `dispatch()` if no responder is registered.
|
|
122
278
|
|
|
123
279
|
## Built-in Plugins
|
|
124
280
|
|
|
@@ -129,18 +285,20 @@ Plugins execute in order: first added = outermost wrapper.
|
|
|
129
285
|
| `history(options?)` | Track command history for undo/redo |
|
|
130
286
|
| `debounce(actions, wait)` | Delay execution until activity stops |
|
|
131
287
|
| `throttle(actions, wait)` | Limit execution frequency |
|
|
288
|
+
| `authGuard(options)` | Block protected commands when unauthenticated |
|
|
289
|
+
| `optimistic(handlers)` | Apply optimistic updates, rollback on failure |
|
|
132
290
|
|
|
133
291
|
### logger
|
|
134
292
|
|
|
135
293
|
```typescript
|
|
136
|
-
bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('
|
|
294
|
+
bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('cart_') }));
|
|
137
295
|
```
|
|
138
296
|
|
|
139
297
|
### validator
|
|
140
298
|
|
|
141
299
|
```typescript
|
|
142
300
|
bus.use(validator({
|
|
143
|
-
'
|
|
301
|
+
'cart_add': (cmd) => {
|
|
144
302
|
if (!cmd.target?.id) return 'Product must have an ID';
|
|
145
303
|
return null; // null = valid
|
|
146
304
|
}
|
|
@@ -158,16 +316,78 @@ historyPlugin.redo();
|
|
|
158
316
|
historyPlugin.getState(); // { past, future, canUndo, canRedo }
|
|
159
317
|
```
|
|
160
318
|
|
|
319
|
+
With bus-backed undo (executes inverse handlers):
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
const historyPlugin = history({ maxSize: 100, bus });
|
|
323
|
+
bus.use(historyPlugin);
|
|
324
|
+
|
|
325
|
+
// If cart_add was registered with { undo: fn }, calling undo() executes it
|
|
326
|
+
historyPlugin.undo();
|
|
327
|
+
```
|
|
328
|
+
|
|
161
329
|
### debounce
|
|
162
330
|
|
|
163
331
|
```typescript
|
|
164
|
-
bus.use(debounce(['
|
|
332
|
+
bus.use(debounce(['search_query'], 300)); // wait 300ms after last call
|
|
165
333
|
```
|
|
166
334
|
|
|
167
335
|
### throttle
|
|
168
336
|
|
|
169
337
|
```typescript
|
|
170
|
-
bus.use(throttle(['
|
|
338
|
+
bus.use(throttle(['ui_scroll'], 100)); // max once per 100ms
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### authGuard
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
bus.use(authGuard({
|
|
345
|
+
isAuthenticated: () => !!user.value,
|
|
346
|
+
protected: ['shop_cart_', 'shop_wishlist_'],
|
|
347
|
+
onUnauthenticated: (cmd) => router.push('/login'),
|
|
348
|
+
}));
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### optimistic
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
bus.use(optimistic({
|
|
355
|
+
'cart_add': {
|
|
356
|
+
apply: (cmd) => {
|
|
357
|
+
cartCount.value++;
|
|
358
|
+
return () => { cartCount.value--; }; // rollback function
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}));
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Batch Dispatch
|
|
365
|
+
|
|
366
|
+
Dispatch multiple commands as a unit. Stops on the first failure:
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
const result = bus.dispatchBatch([
|
|
370
|
+
{ action: 'cart_add', target: cart, payload: item },
|
|
371
|
+
{ action: 'totals_update', target: cart },
|
|
372
|
+
{ action: 'analytics_track', target: session, payload: item },
|
|
373
|
+
]);
|
|
374
|
+
|
|
375
|
+
if (result.ok) {
|
|
376
|
+
console.log('All succeeded:', result.results);
|
|
377
|
+
} else {
|
|
378
|
+
console.error('Stopped at failure:', result.error);
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Dead Letter Handling
|
|
383
|
+
|
|
384
|
+
Configure what happens when a command has no registered handler:
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
createCommandBus() // default: returns { ok: false, error }
|
|
388
|
+
createCommandBus({ onMissing: 'throw' }) // throws the error
|
|
389
|
+
createCommandBus({ onMissing: 'ignore' }) // returns { ok: true, value: undefined }
|
|
390
|
+
createCommandBus({ onMissing: (cmd) => { ... } }) // custom fallback
|
|
171
391
|
```
|
|
172
392
|
|
|
173
393
|
## Async Command Bus
|
|
@@ -179,22 +399,22 @@ import { createAsyncCommandBus } from 'vapor-chamber';
|
|
|
179
399
|
|
|
180
400
|
const bus = createAsyncCommandBus();
|
|
181
401
|
|
|
182
|
-
bus.register('
|
|
402
|
+
bus.register('user_fetch', async (cmd) => {
|
|
183
403
|
const response = await fetch(`/api/users/${cmd.target.id}`);
|
|
184
404
|
return response.json();
|
|
185
405
|
});
|
|
186
406
|
|
|
187
|
-
const result = await bus.dispatch('
|
|
407
|
+
const result = await bus.dispatch('user_fetch', { id: 123 });
|
|
188
408
|
```
|
|
189
409
|
|
|
190
410
|
## Vapor Composables
|
|
191
411
|
|
|
192
|
-
For Vue Vapor components:
|
|
193
|
-
|
|
194
412
|
### useCommand
|
|
195
413
|
|
|
414
|
+
Dispatch commands with reactive loading/error state:
|
|
415
|
+
|
|
196
416
|
```vue
|
|
197
|
-
<script setup>
|
|
417
|
+
<script setup vapor>
|
|
198
418
|
import { useCommand } from 'vapor-chamber';
|
|
199
419
|
|
|
200
420
|
const { dispatch, loading, lastError } = useCommand();
|
|
@@ -206,16 +426,36 @@ const { dispatch, loading, lastError } = useCommand();
|
|
|
206
426
|
</template>
|
|
207
427
|
```
|
|
208
428
|
|
|
429
|
+
### defineVaporCommand
|
|
430
|
+
|
|
431
|
+
Zero-overhead dispatch for hot paths — no reactive `loading`/`lastError` signals created.
|
|
432
|
+
Ideal for GA4 tracking, scroll events, debounced search, fire-and-forget patterns:
|
|
433
|
+
|
|
434
|
+
```vue
|
|
435
|
+
<script setup vapor>
|
|
436
|
+
import { defineVaporCommand } from 'vapor-chamber';
|
|
437
|
+
|
|
438
|
+
const { dispatch } = defineVaporCommand('analytics_track', (cmd) => {
|
|
439
|
+
gtag('event', cmd.target.event, cmd.target.params);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Fire-and-forget — no reactive overhead in the alien-signals graph
|
|
443
|
+
dispatch({ event: 'page_view', params: { page: '/shop' } });
|
|
444
|
+
</script>
|
|
445
|
+
```
|
|
446
|
+
|
|
209
447
|
### useCommandState
|
|
210
448
|
|
|
449
|
+
State managed by commands:
|
|
450
|
+
|
|
211
451
|
```vue
|
|
212
|
-
<script setup>
|
|
452
|
+
<script setup vapor>
|
|
213
453
|
import { useCommandState } from 'vapor-chamber';
|
|
214
454
|
|
|
215
455
|
const { state: cart } = useCommandState(
|
|
216
456
|
{ items: [], total: 0 },
|
|
217
457
|
{
|
|
218
|
-
'
|
|
458
|
+
'cart_add': (state, cmd) => ({
|
|
219
459
|
items: [...state.items, cmd.target],
|
|
220
460
|
total: state.total + cmd.target.price
|
|
221
461
|
})
|
|
@@ -226,16 +466,84 @@ const { state: cart } = useCommandState(
|
|
|
226
466
|
|
|
227
467
|
### useCommandHistory
|
|
228
468
|
|
|
469
|
+
Reactive undo/redo:
|
|
470
|
+
|
|
229
471
|
```vue
|
|
230
|
-
<script setup>
|
|
472
|
+
<script setup vapor>
|
|
231
473
|
import { useCommandHistory } from 'vapor-chamber';
|
|
232
474
|
|
|
233
475
|
const { canUndo, canRedo, undo, redo } = useCommandHistory({
|
|
234
|
-
filter: (cmd) => cmd.action.startsWith('
|
|
476
|
+
filter: (cmd) => cmd.action.startsWith('editor_')
|
|
235
477
|
});
|
|
236
478
|
</script>
|
|
237
479
|
```
|
|
238
480
|
|
|
481
|
+
### useCommandBus
|
|
482
|
+
|
|
483
|
+
Lightweight access to the shared bus — tree-shakeable:
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
import { useCommandBus } from 'vapor-chamber';
|
|
487
|
+
|
|
488
|
+
const bus = useCommandBus();
|
|
489
|
+
bus.dispatch('cart_add', product, { quantity: 1 });
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Use `useCommand()` when you need reactive `loading`/`lastError` signals. Use `defineVaporCommand()` for zero-overhead hot paths. Use `useCommandBus()` when you just need to dispatch.
|
|
493
|
+
|
|
494
|
+
### configureSignal
|
|
495
|
+
|
|
496
|
+
Inject a custom signal factory. In Vue 3.6+, `ref()` is auto-detected and backed by alien-signals — calling `configureSignal` is only needed for custom signal implementations:
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
import { ref } from 'vue';
|
|
500
|
+
import { configureSignal } from 'vapor-chamber';
|
|
501
|
+
|
|
502
|
+
configureSignal(ref); // explicit — usually auto-detected
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Testing
|
|
506
|
+
|
|
507
|
+
`createTestBus()` records all dispatched commands without executing real handlers:
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
import { createTestBus, setCommandBus } from 'vapor-chamber';
|
|
511
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
512
|
+
import { resetCommandBus } from 'vapor-chamber';
|
|
513
|
+
|
|
514
|
+
describe('CartButton', () => {
|
|
515
|
+
let bus: TestBus;
|
|
516
|
+
|
|
517
|
+
beforeEach(() => {
|
|
518
|
+
bus = createTestBus();
|
|
519
|
+
setCommandBus(bus);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
afterEach(() => {
|
|
523
|
+
resetCommandBus();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('dispatches cart_add on click', () => {
|
|
527
|
+
// ... render component, click button ...
|
|
528
|
+
expect(bus.wasDispatched('cart_add')).toBe(true);
|
|
529
|
+
expect(bus.getDispatched('cart_add')[0].cmd.payload).toEqual({ quantity: 1 });
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### setupDevtools
|
|
535
|
+
|
|
536
|
+
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:
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
import { createApp } from 'vue';
|
|
540
|
+
import { getCommandBus, setupDevtools } from 'vapor-chamber';
|
|
541
|
+
|
|
542
|
+
const app = createApp(App);
|
|
543
|
+
setupDevtools(getCommandBus(), app);
|
|
544
|
+
app.mount('#app');
|
|
545
|
+
```
|
|
546
|
+
|
|
239
547
|
## Examples
|
|
240
548
|
|
|
241
549
|
See the [`examples/`](./examples) folder for complete, runnable examples:
|
|
@@ -249,52 +557,93 @@ See the [`examples/`](./examples) folder for complete, runnable examples:
|
|
|
249
557
|
| [`custom-plugins.ts`](./examples/custom-plugins.ts) | Analytics, auth guard, rate limiter plugins |
|
|
250
558
|
| [`vue-vapor-component.vue`](./examples/vue-vapor-component.vue) | Full Vue Vapor todo app |
|
|
251
559
|
|
|
252
|
-
Run TypeScript examples with:
|
|
253
|
-
```bash
|
|
254
|
-
npx ts-node examples/shopping-cart.ts
|
|
255
|
-
```
|
|
256
|
-
|
|
257
560
|
## API Reference
|
|
258
561
|
|
|
259
562
|
### Core
|
|
260
563
|
|
|
261
564
|
| Function | Description |
|
|
262
565
|
|----------|-------------|
|
|
263
|
-
| `createCommandBus()` | Create a synchronous command bus |
|
|
264
|
-
| `createAsyncCommandBus()` | Create an async command bus |
|
|
566
|
+
| `createCommandBus(options?)` | Create a synchronous command bus |
|
|
567
|
+
| `createAsyncCommandBus(options?)` | Create an async command bus |
|
|
568
|
+
| `createTestBus(options?)` | Create a test bus that records dispatches |
|
|
569
|
+
|
|
570
|
+
**`CommandBusOptions`**
|
|
571
|
+
|
|
572
|
+
| Option | Type | Default | Description |
|
|
573
|
+
|--------|------|---------|-------------|
|
|
574
|
+
| `onMissing` | `'error' \| 'throw' \| 'ignore' \| fn` | `'error'` | Behavior when no handler is registered |
|
|
575
|
+
| `naming` | `{ pattern: RegExp, onViolation?: string }` | — | Enforce naming convention on actions |
|
|
265
576
|
|
|
266
577
|
### Command Bus Methods
|
|
267
578
|
|
|
268
579
|
| Method | Description |
|
|
269
580
|
|--------|-------------|
|
|
270
581
|
| `dispatch(action, target, payload?)` | Execute a command |
|
|
271
|
-
| `
|
|
272
|
-
| `
|
|
582
|
+
| `dispatchBatch(commands[])` | Execute multiple commands; stops on first failure |
|
|
583
|
+
| `register(action, handler, options?)` | Register a handler. Options: `{ undo?, throttle?, debounce? }` |
|
|
584
|
+
| `use(plugin, options?)` | Add a plugin. `options.priority` controls order |
|
|
273
585
|
| `onAfter(hook)` | Run callback after every command |
|
|
586
|
+
| `on(pattern, listener)` | Subscribe to commands matching a pattern (`*`, `prefix_*`, exact) |
|
|
587
|
+
| `request(action, target, options?)` | Async request/response with timeout |
|
|
588
|
+
| `respond(action, handler)` | Register a responder for `request()` calls |
|
|
589
|
+
| `getUndoHandler(action)` | Get the undo handler for an action |
|
|
274
590
|
|
|
275
591
|
### Composables
|
|
276
592
|
|
|
277
593
|
| Composable | Description |
|
|
278
594
|
|------------|-------------|
|
|
279
595
|
| `useCommand()` | Dispatch with reactive loading/error state |
|
|
596
|
+
| `defineVaporCommand(action, handler, options?)` | Zero-overhead dispatch for hot paths |
|
|
280
597
|
| `useCommandState(initial, handlers)` | State managed by commands |
|
|
281
598
|
| `useCommandHistory(options?)` | Reactive undo/redo |
|
|
282
|
-
| `
|
|
599
|
+
| `useCommandBus()` | Get shared bus instance |
|
|
600
|
+
| `getCommandBus()` | Get shared bus instance (non-composable) |
|
|
283
601
|
| `setCommandBus(bus)` | Set shared bus instance |
|
|
602
|
+
| `resetCommandBus()` | Reset shared bus to null (useful in tests) |
|
|
603
|
+
| `configureSignal(fn)` | Inject a custom signal factory |
|
|
604
|
+
| `isVaporAvailable()` | Returns true if Vue 3.6+ Vapor mode is detected |
|
|
605
|
+
| `createVaporChamberApp(component, props?)` | Create a Vapor app instance (requires Vue 3.6+) |
|
|
606
|
+
| `getVaporInteropPlugin()` | Returns `vaporInteropPlugin` for mixed trees |
|
|
607
|
+
| `setupDevtools(bus, app)` | Connect bus to Vue DevTools |
|
|
608
|
+
|
|
609
|
+
## Roadmap
|
|
610
|
+
|
|
611
|
+
| Feature | Status |
|
|
612
|
+
|---------|--------|
|
|
613
|
+
| DevTools integration | ✅ Done |
|
|
614
|
+
| DevTools production strip (0KB in prod) | ✅ Done |
|
|
615
|
+
| Command batching (`dispatchBatch`) | ✅ Done |
|
|
616
|
+
| Middleware priority/ordering | ✅ Done |
|
|
617
|
+
| Dead letter handling (`onMissing`) | ✅ Done |
|
|
618
|
+
| Testing utilities (`createTestBus`) | ✅ Done |
|
|
619
|
+
| Naming convention enforcement | ✅ Done (v0.3.0) |
|
|
620
|
+
| Wildcard listeners (`on`) | ✅ Done (v0.3.0) |
|
|
621
|
+
| Request/response pattern | ✅ Done (v0.3.0) |
|
|
622
|
+
| Per-command throttle/undo at register | ✅ Done (v0.3.0) |
|
|
623
|
+
| Auth guard plugin | ✅ Done (v0.3.0) |
|
|
624
|
+
| Optimistic update plugin | ✅ Done (v0.3.0) |
|
|
625
|
+
| Vue 3.6 Vapor alignment | ✅ Done (v0.4.0) |
|
|
626
|
+
| `defineVaporCommand` zero-overhead composable | ✅ Done (v0.4.0) |
|
|
627
|
+
| `onScopeDispose` lifecycle alignment | ✅ Done (v0.4.0) |
|
|
628
|
+
| Persistence plugin (localStorage / IndexedDB) | Planned |
|
|
629
|
+
| SSR support | Planned (pending Vue Vapor stabilization) |
|
|
284
630
|
|
|
285
631
|
## Documentation
|
|
286
632
|
|
|
287
633
|
See the [`docs/`](./docs) folder for detailed documentation:
|
|
288
634
|
|
|
289
|
-
- [Whitepaper](./docs/whitepaper.md)
|
|
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
|
|
290
638
|
|
|
291
639
|
## Design Goals
|
|
292
640
|
|
|
293
|
-
1. **Minimal**
|
|
294
|
-
2. **Vapor-native**
|
|
295
|
-
3. **Composable**
|
|
296
|
-
4. **Type-safe**
|
|
297
|
-
5. **Predictable**
|
|
641
|
+
1. **Minimal** — ~1KB core, no dependencies
|
|
642
|
+
2. **Vapor-native** — Built for signals, not VDOM
|
|
643
|
+
3. **Composable** — Plugins for everything
|
|
644
|
+
4. **Type-safe** — Full TypeScript support
|
|
645
|
+
5. **Predictable** — Sync by default, explicit async
|
|
646
|
+
6. **Progressive** — Works in VDOM, Vapor, and mixed trees
|
|
298
647
|
|
|
299
648
|
## License
|
|
300
649
|
|