vapor-chamber 0.2.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 +258 -102
- package/dist/chamber.d.ts +71 -14
- package/dist/chamber.d.ts.map +1 -1
- package/dist/chamber.js +229 -24
- package/dist/command-bus.d.ts +46 -13
- package/dist/command-bus.d.ts.map +1 -1
- package/dist/command-bus.js +165 -13
- package/dist/devtools.d.ts.map +1 -1
- package/dist/devtools.js +3 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/plugins.d.ts +37 -3
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +95 -15
- package/dist/testing.d.ts +5 -6
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +31 -9
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
A lightweight command bus designed for <a href="https://github.com/vuejs/vue-vapor">Vue Vapor</a>. ~2KB gzipped. Optional DevTools integration.
|
|
6
|
+
A lightweight command bus designed for <a href="https://github.com/vuejs/vue-vapor">Vue Vapor</a>. ~2KB gzipped. Vue 3.6 Vapor aligned. Optional DevTools integration.
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
## What is Vue Vapor?
|
|
10
10
|
|
|
11
|
-
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.
|
|
12
14
|
|
|
13
15
|
**Vapor Chamber** embraces this philosophy: minimal abstraction, direct updates, signal-native reactivity.
|
|
14
16
|
|
|
@@ -35,17 +37,17 @@ bus.on('cart:add', updateBadge); // now two handlers, hard to trace
|
|
|
35
37
|
```
|
|
36
38
|
// After — Vapor Chamber
|
|
37
39
|
// Anywhere in the app
|
|
38
|
-
bus.dispatch('
|
|
40
|
+
bus.dispatch('cart_add', product, { quantity: 1 });
|
|
39
41
|
|
|
40
42
|
// One place, once:
|
|
41
|
-
bus.register('
|
|
43
|
+
bus.register('cart_add', (cmd) => {
|
|
42
44
|
cart.items.push(cmd.target);
|
|
43
45
|
return cart.items;
|
|
44
46
|
});
|
|
45
47
|
|
|
46
48
|
// Cross-cutting concerns as plugins, not scattered listeners:
|
|
47
49
|
bus.use(logger());
|
|
48
|
-
bus.use(validator({ '
|
|
50
|
+
bus.use(validator({ 'cart_add': (cmd) => cmd.target.id ? null : 'Missing ID' }));
|
|
49
51
|
bus.use(analyticsPlugin);
|
|
50
52
|
```
|
|
51
53
|
|
|
@@ -58,7 +60,7 @@ Traditional event systems scatter logic across components. A command bus central
|
|
|
58
60
|
```
|
|
59
61
|
Event-driven (scattered) Command bus (centralized)
|
|
60
62
|
───────────────────────── ─────────────────────────
|
|
61
|
-
Component A emits 'add' → dispatch('
|
|
63
|
+
Component A emits 'add' → dispatch('cart_add', product)
|
|
62
64
|
Component B listens... ↓
|
|
63
65
|
Component C also listens... Handler executes once
|
|
64
66
|
Who handles what? When? Plugins observe/modify
|
|
@@ -66,10 +68,10 @@ Who handles what? When? Plugins observe/modify
|
|
|
66
68
|
```
|
|
67
69
|
|
|
68
70
|
**Benefits:**
|
|
69
|
-
- **Semantic actions**
|
|
70
|
-
- **Single handler**
|
|
71
|
-
- **Plugin pipeline**
|
|
72
|
-
- **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
|
|
73
75
|
|
|
74
76
|
## Install
|
|
75
77
|
|
|
@@ -77,6 +79,8 @@ Who handles what? When? Plugins observe/modify
|
|
|
77
79
|
npm install vapor-chamber
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
**Requirements:** Node.js ≥20.19.0 | Vue ≥3.5.0 (optional peer dep) | Vite 7/8 compatible
|
|
83
|
+
|
|
80
84
|
## Quick Start
|
|
81
85
|
|
|
82
86
|
```typescript
|
|
@@ -87,17 +91,17 @@ const bus = createCommandBus();
|
|
|
87
91
|
// Add plugins
|
|
88
92
|
bus.use(logger());
|
|
89
93
|
bus.use(validator({
|
|
90
|
-
'
|
|
94
|
+
'cart_add': (cmd) => cmd.payload?.quantity > 0 ? null : 'Quantity required'
|
|
91
95
|
}));
|
|
92
96
|
|
|
93
97
|
// Register handler
|
|
94
|
-
bus.register('
|
|
98
|
+
bus.register('cart_add', (cmd) => {
|
|
95
99
|
cart.items.push({ ...cmd.target, quantity: cmd.payload.quantity });
|
|
96
100
|
return cart.items;
|
|
97
101
|
});
|
|
98
102
|
|
|
99
103
|
// Dispatch
|
|
100
|
-
const result = bus.dispatch('
|
|
104
|
+
const result = bus.dispatch('cart_add', product, { quantity: 2 });
|
|
101
105
|
if (result.ok) {
|
|
102
106
|
console.log('Added:', result.value);
|
|
103
107
|
} else {
|
|
@@ -105,6 +109,56 @@ if (result.ok) {
|
|
|
105
109
|
}
|
|
106
110
|
```
|
|
107
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
|
+
|
|
108
162
|
## Core Concepts
|
|
109
163
|
|
|
110
164
|
### Commands
|
|
@@ -113,27 +167,48 @@ A command has three parts:
|
|
|
113
167
|
|
|
114
168
|
```typescript
|
|
115
169
|
bus.dispatch(
|
|
116
|
-
'
|
|
170
|
+
'cart_add', // action - what to do
|
|
117
171
|
product, // target - what to act on
|
|
118
172
|
{ quantity: 2 } // payload - additional data (optional)
|
|
119
173
|
);
|
|
120
174
|
```
|
|
121
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
|
+
|
|
122
192
|
### Handlers
|
|
123
193
|
|
|
124
194
|
One handler per action. Returns a value or throws:
|
|
125
195
|
|
|
126
196
|
```typescript
|
|
127
|
-
bus.register('
|
|
128
|
-
// cmd.action = 'cart.add'
|
|
129
|
-
// cmd.target = product
|
|
130
|
-
// cmd.payload = { quantity: 2 }
|
|
131
|
-
|
|
197
|
+
bus.register('cart_add', (cmd) => {
|
|
132
198
|
cart.items.push(cmd.target);
|
|
133
199
|
return cart.items; // becomes result.value
|
|
134
200
|
});
|
|
135
201
|
```
|
|
136
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
|
+
|
|
137
212
|
### Results
|
|
138
213
|
|
|
139
214
|
Every dispatch returns a result:
|
|
@@ -169,6 +244,38 @@ bus.use(analyticsPlugin, { priority: 1 }); // runs after validation
|
|
|
169
244
|
bus.use(loggerPlugin); // priority 0 (default, runs last)
|
|
170
245
|
```
|
|
171
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.
|
|
278
|
+
|
|
172
279
|
## Built-in Plugins
|
|
173
280
|
|
|
174
281
|
| Plugin | Description |
|
|
@@ -178,18 +285,20 @@ bus.use(loggerPlugin); // priority 0 (default, runs last)
|
|
|
178
285
|
| `history(options?)` | Track command history for undo/redo |
|
|
179
286
|
| `debounce(actions, wait)` | Delay execution until activity stops |
|
|
180
287
|
| `throttle(actions, wait)` | Limit execution frequency |
|
|
288
|
+
| `authGuard(options)` | Block protected commands when unauthenticated |
|
|
289
|
+
| `optimistic(handlers)` | Apply optimistic updates, rollback on failure |
|
|
181
290
|
|
|
182
291
|
### logger
|
|
183
292
|
|
|
184
293
|
```typescript
|
|
185
|
-
bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('
|
|
294
|
+
bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('cart_') }));
|
|
186
295
|
```
|
|
187
296
|
|
|
188
297
|
### validator
|
|
189
298
|
|
|
190
299
|
```typescript
|
|
191
300
|
bus.use(validator({
|
|
192
|
-
'
|
|
301
|
+
'cart_add': (cmd) => {
|
|
193
302
|
if (!cmd.target?.id) return 'Product must have an ID';
|
|
194
303
|
return null; // null = valid
|
|
195
304
|
}
|
|
@@ -207,16 +316,49 @@ historyPlugin.redo();
|
|
|
207
316
|
historyPlugin.getState(); // { past, future, canUndo, canRedo }
|
|
208
317
|
```
|
|
209
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
|
+
|
|
210
329
|
### debounce
|
|
211
330
|
|
|
212
331
|
```typescript
|
|
213
|
-
bus.use(debounce(['
|
|
332
|
+
bus.use(debounce(['search_query'], 300)); // wait 300ms after last call
|
|
214
333
|
```
|
|
215
334
|
|
|
216
335
|
### throttle
|
|
217
336
|
|
|
218
337
|
```typescript
|
|
219
|
-
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
|
+
}));
|
|
220
362
|
```
|
|
221
363
|
|
|
222
364
|
## Batch Dispatch
|
|
@@ -225,42 +367,27 @@ Dispatch multiple commands as a unit. Stops on the first failure:
|
|
|
225
367
|
|
|
226
368
|
```typescript
|
|
227
369
|
const result = bus.dispatchBatch([
|
|
228
|
-
{ action: '
|
|
229
|
-
{ action: '
|
|
230
|
-
{ action: '
|
|
370
|
+
{ action: 'cart_add', target: cart, payload: item },
|
|
371
|
+
{ action: 'totals_update', target: cart },
|
|
372
|
+
{ action: 'analytics_track', target: session, payload: item },
|
|
231
373
|
]);
|
|
232
374
|
|
|
233
375
|
if (result.ok) {
|
|
234
376
|
console.log('All succeeded:', result.results);
|
|
235
377
|
} else {
|
|
236
378
|
console.error('Stopped at failure:', result.error);
|
|
237
|
-
console.log('Partial results:', result.results);
|
|
238
379
|
}
|
|
239
380
|
```
|
|
240
381
|
|
|
241
|
-
Works on both sync and async buses.
|
|
242
|
-
|
|
243
382
|
## Dead Letter Handling
|
|
244
383
|
|
|
245
384
|
Configure what happens when a command has no registered handler:
|
|
246
385
|
|
|
247
386
|
```typescript
|
|
248
|
-
//
|
|
249
|
-
createCommandBus()
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
createCommandBus({ onMissing: 'throw' })
|
|
253
|
-
|
|
254
|
-
// Silently succeed (useful for optional commands)
|
|
255
|
-
createCommandBus({ onMissing: 'ignore' })
|
|
256
|
-
|
|
257
|
-
// Custom fallback
|
|
258
|
-
createCommandBus({
|
|
259
|
-
onMissing: (cmd) => {
|
|
260
|
-
console.warn(`Unhandled: ${cmd.action}`);
|
|
261
|
-
return { ok: true, value: null };
|
|
262
|
-
}
|
|
263
|
-
})
|
|
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
|
|
264
391
|
```
|
|
265
392
|
|
|
266
393
|
## Async Command Bus
|
|
@@ -272,22 +399,22 @@ import { createAsyncCommandBus } from 'vapor-chamber';
|
|
|
272
399
|
|
|
273
400
|
const bus = createAsyncCommandBus();
|
|
274
401
|
|
|
275
|
-
bus.register('
|
|
402
|
+
bus.register('user_fetch', async (cmd) => {
|
|
276
403
|
const response = await fetch(`/api/users/${cmd.target.id}`);
|
|
277
404
|
return response.json();
|
|
278
405
|
});
|
|
279
406
|
|
|
280
|
-
const result = await bus.dispatch('
|
|
407
|
+
const result = await bus.dispatch('user_fetch', { id: 123 });
|
|
281
408
|
```
|
|
282
409
|
|
|
283
410
|
## Vapor Composables
|
|
284
411
|
|
|
285
|
-
For Vue Vapor components:
|
|
286
|
-
|
|
287
412
|
### useCommand
|
|
288
413
|
|
|
414
|
+
Dispatch commands with reactive loading/error state:
|
|
415
|
+
|
|
289
416
|
```vue
|
|
290
|
-
<script setup>
|
|
417
|
+
<script setup vapor>
|
|
291
418
|
import { useCommand } from 'vapor-chamber';
|
|
292
419
|
|
|
293
420
|
const { dispatch, loading, lastError } = useCommand();
|
|
@@ -299,16 +426,36 @@ const { dispatch, loading, lastError } = useCommand();
|
|
|
299
426
|
</template>
|
|
300
427
|
```
|
|
301
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
|
+
|
|
302
447
|
### useCommandState
|
|
303
448
|
|
|
449
|
+
State managed by commands:
|
|
450
|
+
|
|
304
451
|
```vue
|
|
305
|
-
<script setup>
|
|
452
|
+
<script setup vapor>
|
|
306
453
|
import { useCommandState } from 'vapor-chamber';
|
|
307
454
|
|
|
308
455
|
const { state: cart } = useCommandState(
|
|
309
456
|
{ items: [], total: 0 },
|
|
310
457
|
{
|
|
311
|
-
'
|
|
458
|
+
'cart_add': (state, cmd) => ({
|
|
312
459
|
items: [...state.items, cmd.target],
|
|
313
460
|
total: state.total + cmd.target.price
|
|
314
461
|
})
|
|
@@ -319,47 +466,50 @@ const { state: cart } = useCommandState(
|
|
|
319
466
|
|
|
320
467
|
### useCommandHistory
|
|
321
468
|
|
|
469
|
+
Reactive undo/redo:
|
|
470
|
+
|
|
322
471
|
```vue
|
|
323
|
-
<script setup>
|
|
472
|
+
<script setup vapor>
|
|
324
473
|
import { useCommandHistory } from 'vapor-chamber';
|
|
325
474
|
|
|
326
475
|
const { canUndo, canRedo, undo, redo } = useCommandHistory({
|
|
327
|
-
filter: (cmd) => cmd.action.startsWith('
|
|
476
|
+
filter: (cmd) => cmd.action.startsWith('editor_')
|
|
328
477
|
});
|
|
329
478
|
</script>
|
|
330
479
|
```
|
|
331
480
|
|
|
332
481
|
### useCommandBus
|
|
333
482
|
|
|
334
|
-
Lightweight
|
|
483
|
+
Lightweight access to the shared bus — tree-shakeable:
|
|
335
484
|
|
|
336
485
|
```typescript
|
|
337
486
|
import { useCommandBus } from 'vapor-chamber';
|
|
338
487
|
|
|
339
488
|
const bus = useCommandBus();
|
|
340
|
-
bus.dispatch('
|
|
489
|
+
bus.dispatch('cart_add', product, { quantity: 1 });
|
|
341
490
|
```
|
|
342
491
|
|
|
343
|
-
Use `useCommand()` when you need reactive `loading`/`lastError` signals. Use `useCommandBus()` when you just need to dispatch.
|
|
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.
|
|
344
493
|
|
|
345
494
|
### configureSignal
|
|
346
495
|
|
|
347
|
-
Inject
|
|
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:
|
|
348
497
|
|
|
349
498
|
```typescript
|
|
350
|
-
import {
|
|
499
|
+
import { ref } from 'vue';
|
|
351
500
|
import { configureSignal } from 'vapor-chamber';
|
|
352
501
|
|
|
353
|
-
configureSignal(
|
|
502
|
+
configureSignal(ref); // explicit — usually auto-detected
|
|
354
503
|
```
|
|
355
504
|
|
|
356
505
|
### Testing
|
|
357
506
|
|
|
358
|
-
`createTestBus()` records all dispatched commands without executing real handlers
|
|
507
|
+
`createTestBus()` records all dispatched commands without executing real handlers:
|
|
359
508
|
|
|
360
509
|
```typescript
|
|
361
510
|
import { createTestBus, setCommandBus } from 'vapor-chamber';
|
|
362
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
511
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
512
|
+
import { resetCommandBus } from 'vapor-chamber';
|
|
363
513
|
|
|
364
514
|
describe('CartButton', () => {
|
|
365
515
|
let bus: TestBus;
|
|
@@ -369,19 +519,15 @@ describe('CartButton', () => {
|
|
|
369
519
|
setCommandBus(bus);
|
|
370
520
|
});
|
|
371
521
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
expect(bus.wasDispatched('cart.add')).toBe(true);
|
|
375
|
-
expect(bus.getDispatched('cart.add')[0].cmd.payload).toEqual({ quantity: 1 });
|
|
522
|
+
afterEach(() => {
|
|
523
|
+
resetCommandBus();
|
|
376
524
|
});
|
|
377
|
-
});
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
Register real handlers for actions you want to test deeply:
|
|
381
525
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
+
});
|
|
385
531
|
});
|
|
386
532
|
```
|
|
387
533
|
|
|
@@ -398,12 +544,6 @@ setupDevtools(getCommandBus(), app);
|
|
|
398
544
|
app.mount('#app');
|
|
399
545
|
```
|
|
400
546
|
|
|
401
|
-
The inspector shows:
|
|
402
|
-
- Every dispatched command with its action, target, payload, and result
|
|
403
|
-
- Green `ok` / red `error` tags at a glance
|
|
404
|
-
- Full detail (value or error message) when a command is selected
|
|
405
|
-
- Filterable tree by action name
|
|
406
|
-
|
|
407
547
|
## Examples
|
|
408
548
|
|
|
409
549
|
See the [`examples/`](./examples) folder for complete, runnable examples:
|
|
@@ -417,11 +557,6 @@ See the [`examples/`](./examples) folder for complete, runnable examples:
|
|
|
417
557
|
| [`custom-plugins.ts`](./examples/custom-plugins.ts) | Analytics, auth guard, rate limiter plugins |
|
|
418
558
|
| [`vue-vapor-component.vue`](./examples/vue-vapor-component.vue) | Full Vue Vapor todo app |
|
|
419
559
|
|
|
420
|
-
Run TypeScript examples with:
|
|
421
|
-
```bash
|
|
422
|
-
npx ts-node examples/shopping-cart.ts
|
|
423
|
-
```
|
|
424
|
-
|
|
425
560
|
## API Reference
|
|
426
561
|
|
|
427
562
|
### Core
|
|
@@ -430,13 +565,14 @@ npx ts-node examples/shopping-cart.ts
|
|
|
430
565
|
|----------|-------------|
|
|
431
566
|
| `createCommandBus(options?)` | Create a synchronous command bus |
|
|
432
567
|
| `createAsyncCommandBus(options?)` | Create an async command bus |
|
|
433
|
-
| `createTestBus(options?)` | Create a test bus that records dispatches
|
|
568
|
+
| `createTestBus(options?)` | Create a test bus that records dispatches |
|
|
434
569
|
|
|
435
570
|
**`CommandBusOptions`**
|
|
436
571
|
|
|
437
572
|
| Option | Type | Default | Description |
|
|
438
573
|
|--------|------|---------|-------------|
|
|
439
|
-
| `onMissing` | `'error' \| 'throw' \| 'ignore' \| fn` | `'error'` | Behavior when no handler is registered
|
|
574
|
+
| `onMissing` | `'error' \| 'throw' \| 'ignore' \| fn` | `'error'` | Behavior when no handler is registered |
|
|
575
|
+
| `naming` | `{ pattern: RegExp, onViolation?: string }` | — | Enforce naming convention on actions |
|
|
440
576
|
|
|
441
577
|
### Command Bus Methods
|
|
442
578
|
|
|
@@ -444,22 +580,31 @@ npx ts-node examples/shopping-cart.ts
|
|
|
444
580
|
|--------|-------------|
|
|
445
581
|
| `dispatch(action, target, payload?)` | Execute a command |
|
|
446
582
|
| `dispatchBatch(commands[])` | Execute multiple commands; stops on first failure |
|
|
447
|
-
| `register(action, handler)` | Register a handler
|
|
448
|
-
| `use(plugin, options?)` | Add a plugin
|
|
583
|
+
| `register(action, handler, options?)` | Register a handler. Options: `{ undo?, throttle?, debounce? }` |
|
|
584
|
+
| `use(plugin, options?)` | Add a plugin. `options.priority` controls order |
|
|
449
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 |
|
|
450
590
|
|
|
451
591
|
### Composables
|
|
452
592
|
|
|
453
593
|
| Composable | Description |
|
|
454
594
|
|------------|-------------|
|
|
455
|
-
| `
|
|
456
|
-
| `
|
|
457
|
-
| `useCommandState(initial, handlers)` | State managed by commands
|
|
458
|
-
| `useCommandHistory(options?)` | Reactive undo/redo
|
|
459
|
-
| `
|
|
595
|
+
| `useCommand()` | Dispatch with reactive loading/error state |
|
|
596
|
+
| `defineVaporCommand(action, handler, options?)` | Zero-overhead dispatch for hot paths |
|
|
597
|
+
| `useCommandState(initial, handlers)` | State managed by commands |
|
|
598
|
+
| `useCommandHistory(options?)` | Reactive undo/redo |
|
|
599
|
+
| `useCommandBus()` | Get shared bus instance |
|
|
600
|
+
| `getCommandBus()` | Get shared bus instance (non-composable) |
|
|
460
601
|
| `setCommandBus(bus)` | Set shared bus instance |
|
|
461
|
-
| `
|
|
462
|
-
| `
|
|
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 |
|
|
463
608
|
|
|
464
609
|
## Roadmap
|
|
465
610
|
|
|
@@ -471,6 +616,15 @@ npx ts-node examples/shopping-cart.ts
|
|
|
471
616
|
| Middleware priority/ordering | ✅ Done |
|
|
472
617
|
| Dead letter handling (`onMissing`) | ✅ Done |
|
|
473
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) |
|
|
474
628
|
| Persistence plugin (localStorage / IndexedDB) | Planned |
|
|
475
629
|
| SSR support | Planned (pending Vue Vapor stabilization) |
|
|
476
630
|
|
|
@@ -478,16 +632,18 @@ npx ts-node examples/shopping-cart.ts
|
|
|
478
632
|
|
|
479
633
|
See the [`docs/`](./docs) folder for detailed documentation:
|
|
480
634
|
|
|
481
|
-
- [Whitepaper](./docs/whitepaper.md)
|
|
482
|
-
- [
|
|
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
|
|
483
638
|
|
|
484
639
|
## Design Goals
|
|
485
640
|
|
|
486
|
-
1. **Minimal**
|
|
487
|
-
2. **Vapor-native**
|
|
488
|
-
3. **Composable**
|
|
489
|
-
4. **Type-safe**
|
|
490
|
-
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
|
|
491
647
|
|
|
492
648
|
## License
|
|
493
649
|
|