vapor-chamber 0.1.0 → 0.2.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 +202 -9
- package/dist/chamber.d.ts +27 -2
- package/dist/chamber.d.ts.map +1 -1
- package/dist/chamber.js +58 -22
- package/dist/command-bus.d.ts +35 -6
- package/dist/command-bus.d.ts.map +1 -1
- package/dist/command-bus.js +114 -37
- package/dist/devtools.d.ts +33 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +153 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +4 -0
- package/dist/testing.d.ts +44 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +126 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
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. Optional DevTools integration.
|
|
7
|
+
</p>
|
|
4
8
|
|
|
5
9
|
## What is Vue Vapor?
|
|
6
10
|
|
|
@@ -8,6 +12,45 @@ Vue Vapor is Vue's upcoming compilation strategy that eliminates the Virtual DOM
|
|
|
8
12
|
|
|
9
13
|
**Vapor Chamber** embraces this philosophy: minimal abstraction, direct updates, signal-native reactivity.
|
|
10
14
|
|
|
15
|
+
## Migrating from Vue 3 emitters
|
|
16
|
+
|
|
17
|
+
If you're already using Vue 3's `emit` / `eventBus` pattern, here's the before and after:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
// Before — Vue 3 emitter
|
|
21
|
+
// cart.vue
|
|
22
|
+
emit('cart:add', product);
|
|
23
|
+
|
|
24
|
+
// App.vue
|
|
25
|
+
bus.on('cart:add', (product) => {
|
|
26
|
+
cart.items.push(product);
|
|
27
|
+
analytics.track('add');
|
|
28
|
+
validate(product); // where does this live?
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ProductList.vue — also listens?
|
|
32
|
+
bus.on('cart:add', updateBadge); // now two handlers, hard to trace
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
// After — Vapor Chamber
|
|
37
|
+
// Anywhere in the app
|
|
38
|
+
bus.dispatch('cart.add', product, { quantity: 1 });
|
|
39
|
+
|
|
40
|
+
// One place, once:
|
|
41
|
+
bus.register('cart.add', (cmd) => {
|
|
42
|
+
cart.items.push(cmd.target);
|
|
43
|
+
return cart.items;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Cross-cutting concerns as plugins, not scattered listeners:
|
|
47
|
+
bus.use(logger());
|
|
48
|
+
bus.use(validator({ 'cart.add': (cmd) => cmd.target.id ? null : 'Missing ID' }));
|
|
49
|
+
bus.use(analyticsPlugin);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**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.
|
|
53
|
+
|
|
11
54
|
## Why a Command Bus?
|
|
12
55
|
|
|
13
56
|
Traditional event systems scatter logic across components. A command bus centralizes it:
|
|
@@ -118,7 +161,13 @@ const timingPlugin: Plugin = (cmd, next) => {
|
|
|
118
161
|
bus.use(timingPlugin);
|
|
119
162
|
```
|
|
120
163
|
|
|
121
|
-
Plugins execute
|
|
164
|
+
Plugins execute by priority (highest first), then registration order for equal priorities:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
bus.use(validatorPlugin, { priority: 10 }); // runs first
|
|
168
|
+
bus.use(analyticsPlugin, { priority: 1 }); // runs after validation
|
|
169
|
+
bus.use(loggerPlugin); // priority 0 (default, runs last)
|
|
170
|
+
```
|
|
122
171
|
|
|
123
172
|
## Built-in Plugins
|
|
124
173
|
|
|
@@ -170,6 +219,50 @@ bus.use(debounce(['search.query'], 300)); // wait 300ms after last call
|
|
|
170
219
|
bus.use(throttle(['ui.scroll'], 100)); // max once per 100ms
|
|
171
220
|
```
|
|
172
221
|
|
|
222
|
+
## Batch Dispatch
|
|
223
|
+
|
|
224
|
+
Dispatch multiple commands as a unit. Stops on the first failure:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const result = bus.dispatchBatch([
|
|
228
|
+
{ action: 'cart.add', target: cart, payload: item },
|
|
229
|
+
{ action: 'totals.update', target: cart },
|
|
230
|
+
{ action: 'analytics.track', target: session, payload: item },
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
if (result.ok) {
|
|
234
|
+
console.log('All succeeded:', result.results);
|
|
235
|
+
} else {
|
|
236
|
+
console.error('Stopped at failure:', result.error);
|
|
237
|
+
console.log('Partial results:', result.results);
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Works on both sync and async buses.
|
|
242
|
+
|
|
243
|
+
## Dead Letter Handling
|
|
244
|
+
|
|
245
|
+
Configure what happens when a command has no registered handler:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// Default: returns { ok: false, error }
|
|
249
|
+
createCommandBus()
|
|
250
|
+
|
|
251
|
+
// Throw instead of returning an error result
|
|
252
|
+
createCommandBus({ onMissing: 'throw' })
|
|
253
|
+
|
|
254
|
+
// Silently succeed (useful for optional commands)
|
|
255
|
+
createCommandBus({ onMissing: 'ignore' })
|
|
256
|
+
|
|
257
|
+
// Custom fallback
|
|
258
|
+
createCommandBus({
|
|
259
|
+
onMissing: (cmd) => {
|
|
260
|
+
console.warn(`Unhandled: ${cmd.action}`);
|
|
261
|
+
return { ok: true, value: null };
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
```
|
|
265
|
+
|
|
173
266
|
## Async Command Bus
|
|
174
267
|
|
|
175
268
|
For async handlers (API calls, IndexedDB, etc.):
|
|
@@ -236,6 +329,81 @@ const { canUndo, canRedo, undo, redo } = useCommandHistory({
|
|
|
236
329
|
</script>
|
|
237
330
|
```
|
|
238
331
|
|
|
332
|
+
### useCommandBus
|
|
333
|
+
|
|
334
|
+
Lightweight composable for the "toolbox" pattern — import only when needed, tree-shaken out of builds that don't use it. Returns the shared bus directly:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { useCommandBus } from 'vapor-chamber';
|
|
338
|
+
|
|
339
|
+
const bus = useCommandBus();
|
|
340
|
+
bus.dispatch('cart.add', product, { quantity: 1 });
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Use `useCommand()` when you need reactive `loading`/`lastError` signals. Use `useCommandBus()` when you just need to dispatch.
|
|
344
|
+
|
|
345
|
+
### configureSignal
|
|
346
|
+
|
|
347
|
+
Inject Vue Vapor's native signal factory once at app setup. Falls back to a built-in shim automatically in non-Vapor environments (standard Vue 3, tests, SSR):
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
import { signal } from 'vue-vapor';
|
|
351
|
+
import { configureSignal } from 'vapor-chamber';
|
|
352
|
+
|
|
353
|
+
configureSignal(signal);
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Testing
|
|
357
|
+
|
|
358
|
+
`createTestBus()` records all dispatched commands without executing real handlers. Use it to test components that call `dispatch` without wiring up the full application:
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import { createTestBus, setCommandBus } from 'vapor-chamber';
|
|
362
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
363
|
+
|
|
364
|
+
describe('CartButton', () => {
|
|
365
|
+
let bus: TestBus;
|
|
366
|
+
|
|
367
|
+
beforeEach(() => {
|
|
368
|
+
bus = createTestBus();
|
|
369
|
+
setCommandBus(bus);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('dispatches cart.add on click', () => {
|
|
373
|
+
// ... render component, click button ...
|
|
374
|
+
expect(bus.wasDispatched('cart.add')).toBe(true);
|
|
375
|
+
expect(bus.getDispatched('cart.add')[0].cmd.payload).toEqual({ quantity: 1 });
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Register real handlers for actions you want to test deeply:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
bus.register('cart.add', (cmd) => {
|
|
384
|
+
// real handler logic
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### setupDevtools
|
|
389
|
+
|
|
390
|
+
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:
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
import { createApp } from 'vue';
|
|
394
|
+
import { getCommandBus, setupDevtools } from 'vapor-chamber';
|
|
395
|
+
|
|
396
|
+
const app = createApp(App);
|
|
397
|
+
setupDevtools(getCommandBus(), app);
|
|
398
|
+
app.mount('#app');
|
|
399
|
+
```
|
|
400
|
+
|
|
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
|
+
|
|
239
407
|
## Examples
|
|
240
408
|
|
|
241
409
|
See the [`examples/`](./examples) folder for complete, runnable examples:
|
|
@@ -260,33 +428,58 @@ npx ts-node examples/shopping-cart.ts
|
|
|
260
428
|
|
|
261
429
|
| Function | Description |
|
|
262
430
|
|----------|-------------|
|
|
263
|
-
| `createCommandBus()` | Create a synchronous command bus |
|
|
264
|
-
| `createAsyncCommandBus()` | Create an async command bus |
|
|
431
|
+
| `createCommandBus(options?)` | Create a synchronous command bus |
|
|
432
|
+
| `createAsyncCommandBus(options?)` | Create an async command bus |
|
|
433
|
+
| `createTestBus(options?)` | Create a test bus that records dispatches (see [Testing](#testing)) |
|
|
434
|
+
|
|
435
|
+
**`CommandBusOptions`**
|
|
436
|
+
|
|
437
|
+
| Option | Type | Default | Description |
|
|
438
|
+
|--------|------|---------|-------------|
|
|
439
|
+
| `onMissing` | `'error' \| 'throw' \| 'ignore' \| fn` | `'error'` | Behavior when no handler is registered for an action |
|
|
265
440
|
|
|
266
441
|
### Command Bus Methods
|
|
267
442
|
|
|
268
443
|
| Method | Description |
|
|
269
444
|
|--------|-------------|
|
|
270
445
|
| `dispatch(action, target, payload?)` | Execute a command |
|
|
446
|
+
| `dispatchBatch(commands[])` | Execute multiple commands; stops on first failure |
|
|
271
447
|
| `register(action, handler)` | Register a handler (returns unregister fn) |
|
|
272
|
-
| `use(plugin)` | Add a plugin (returns unsubscribe fn) |
|
|
448
|
+
| `use(plugin, options?)` | Add a plugin (returns unsubscribe fn). `options.priority` controls order — higher runs first |
|
|
273
449
|
| `onAfter(hook)` | Run callback after every command |
|
|
274
450
|
|
|
275
451
|
### Composables
|
|
276
452
|
|
|
277
453
|
| Composable | Description |
|
|
278
454
|
|------------|-------------|
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
455
|
+
| `useCommandBus()` | Get the shared bus — lightweight, tree-shakeable |
|
|
456
|
+
| `useCommand()` | Dispatch with reactive loading/error state. Returns `dispose()` to clean up registered handlers/plugins |
|
|
457
|
+
| `useCommandState(initial, handlers)` | State managed by commands. Returns `dispose()` to unregister handlers |
|
|
458
|
+
| `useCommandHistory(options?)` | Reactive undo/redo. Returns `dispose()` to unsubscribe |
|
|
282
459
|
| `getCommandBus()` | Get shared bus instance |
|
|
283
460
|
| `setCommandBus(bus)` | Set shared bus instance |
|
|
461
|
+
| `configureSignal(fn)` | Inject a custom signal factory (e.g. Vue Vapor's native `signal`) |
|
|
462
|
+
| `setupDevtools(bus, app)` | Connect bus to Vue DevTools. No-ops automatically in production builds |
|
|
463
|
+
|
|
464
|
+
## Roadmap
|
|
465
|
+
|
|
466
|
+
| Feature | Status |
|
|
467
|
+
|---------|--------|
|
|
468
|
+
| DevTools integration | ✅ Done |
|
|
469
|
+
| DevTools production strip (0KB in prod) | ✅ Done |
|
|
470
|
+
| Command batching (`dispatchBatch`) | ✅ Done |
|
|
471
|
+
| Middleware priority/ordering | ✅ Done |
|
|
472
|
+
| Dead letter handling (`onMissing`) | ✅ Done |
|
|
473
|
+
| Testing utilities (`createTestBus`) | ✅ Done |
|
|
474
|
+
| Persistence plugin (localStorage / IndexedDB) | Planned |
|
|
475
|
+
| SSR support | Planned (pending Vue Vapor stabilization) |
|
|
284
476
|
|
|
285
477
|
## Documentation
|
|
286
478
|
|
|
287
479
|
See the [`docs/`](./docs) folder for detailed documentation:
|
|
288
480
|
|
|
289
481
|
- [Whitepaper](./docs/whitepaper.md) - Design philosophy and architecture
|
|
482
|
+
- [SSR Guide](./docs/ssr.md) - Server-side rendering and hydration
|
|
290
483
|
|
|
291
484
|
## Design Goals
|
|
292
485
|
|
package/dist/chamber.d.ts
CHANGED
|
@@ -5,9 +5,21 @@
|
|
|
5
5
|
* the expected signal-based API. Update imports when Vapor stabilizes.
|
|
6
6
|
*/
|
|
7
7
|
import { type CommandBus, type Command, type CommandResult, type Handler, type Plugin } from './command-bus';
|
|
8
|
-
type Signal<T> = {
|
|
8
|
+
export type Signal<T> = {
|
|
9
9
|
value: T;
|
|
10
10
|
};
|
|
11
|
+
export type CreateSignal = <T>(initial: T) => Signal<T>;
|
|
12
|
+
/**
|
|
13
|
+
* Configure the signal factory used by vapor-chamber composables.
|
|
14
|
+
* Call this once at app setup when Vue Vapor's signal API is available.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* import { signal } from 'vue-vapor';
|
|
18
|
+
* import { configureSignal } from 'vapor-chamber';
|
|
19
|
+
* configureSignal(signal);
|
|
20
|
+
*/
|
|
21
|
+
export declare function configureSignal(fn: CreateSignal): void;
|
|
22
|
+
export declare const signal: CreateSignal;
|
|
11
23
|
export declare function getCommandBus(): CommandBus;
|
|
12
24
|
export declare function setCommandBus(bus: CommandBus): void;
|
|
13
25
|
/**
|
|
@@ -19,6 +31,7 @@ export declare function useCommand(): {
|
|
|
19
31
|
lastError: Signal<Error | null>;
|
|
20
32
|
register: (action: string, handler: Handler) => () => void;
|
|
21
33
|
use: (plugin: Plugin) => () => void;
|
|
34
|
+
dispose: () => void;
|
|
22
35
|
};
|
|
23
36
|
/**
|
|
24
37
|
* useCommandState - create reactive state that updates via commands
|
|
@@ -29,6 +42,19 @@ export declare function useCommandState<T>(initial: T, handlers: {
|
|
|
29
42
|
state: Signal<T>;
|
|
30
43
|
dispose: () => void;
|
|
31
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* useCommandBus - lightweight composable wrapper around the shared bus.
|
|
47
|
+
*
|
|
48
|
+
* Designed for the "toolbox" pattern: import only when needed, tree-shaken
|
|
49
|
+
* out of builds that don't use it. Provides the full bus API plus reactive
|
|
50
|
+
* loading/error signals — without the automatic cleanup tracking of useCommand.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* import { useCommandBus } from 'vapor-chamber';
|
|
54
|
+
* const bus = useCommandBus();
|
|
55
|
+
* bus.dispatch('cart.add', product, { quantity: 1 });
|
|
56
|
+
*/
|
|
57
|
+
export declare function useCommandBus(): CommandBus;
|
|
32
58
|
/**
|
|
33
59
|
* useCommandHistory - undo/redo with reactive state
|
|
34
60
|
*/
|
|
@@ -45,5 +71,4 @@ export declare function useCommandHistory(options?: {
|
|
|
45
71
|
clear: () => void;
|
|
46
72
|
dispose: () => void;
|
|
47
73
|
};
|
|
48
|
-
export {};
|
|
49
74
|
//# sourceMappingURL=chamber.d.ts.map
|
package/dist/chamber.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chamber.d.ts","sourceRoot":"","sources":["../src/chamber.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAoB,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAI/H,
|
|
1
|
+
{"version":3,"file":"chamber.d.ts","sourceRoot":"","sources":["../src/chamber.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAoB,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAI/H,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI;IAAE,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AACrC,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC;AAkBxD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAEtD;AAED,eAAO,MAAM,MAAM,EAAE,YAAoD,CAAC;AAO1E,wBAAgB,aAAa,IAAI,UAAU,CAK1C;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAEnD;AAED;;GAEG;AACH,wBAAgB,UAAU;uBAKE,MAAM,UAAU,GAAG,YAAY,GAAG,KAAG,aAAa;;;uBAgBlD,MAAM,WAAW,OAAO,KAAG,MAAM,IAAI;kBAM1C,MAAM,KAAG,MAAM,IAAI;;EAmBzC;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,OAAO,EAAE,CAAC,EACV,QAAQ,EAAE;IACR,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,KAAK,CAAC,CAAC;CACjD;;;EAsBF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,eAE5B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;CAC/B;;;;;gBAqBa,OAAO,GAAG,SAAS;gBAYnB,OAAO,GAAG,SAAS;;;EA6BrC"}
|
package/dist/chamber.js
CHANGED
|
@@ -5,28 +5,33 @@
|
|
|
5
5
|
* the expected signal-based API. Update imports when Vapor stabilizes.
|
|
6
6
|
*/
|
|
7
7
|
import { createCommandBus } from './command-bus';
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
// Fallback: simple signal implementation for testing/non-Vapor
|
|
17
|
-
return (initial) => {
|
|
18
|
-
let _value = initial;
|
|
19
|
-
const listeners = [];
|
|
20
|
-
return {
|
|
21
|
-
get value() { return _value; },
|
|
22
|
-
set value(v) {
|
|
23
|
-
_value = v;
|
|
24
|
-
listeners.forEach(fn => fn(v));
|
|
25
|
-
}
|
|
26
|
-
};
|
|
8
|
+
// Fallback signal implementation used when Vapor is not available.
|
|
9
|
+
// Uses a plain getter/setter — Vapor's compiler tracks reads/writes itself,
|
|
10
|
+
// so no listener array is needed here.
|
|
11
|
+
const fallbackSignal = (initial) => {
|
|
12
|
+
let _value = initial;
|
|
13
|
+
return {
|
|
14
|
+
get value() { return _value; },
|
|
15
|
+
set value(v) { _value = v; }
|
|
27
16
|
};
|
|
17
|
+
};
|
|
18
|
+
// Allow explicit configuration of the signal factory — avoids probing
|
|
19
|
+
// private/internal globals (e.g. window.__VUE_VAPOR__) which can break
|
|
20
|
+
// proxy traps and is not a stable public API.
|
|
21
|
+
let _signalFn = fallbackSignal;
|
|
22
|
+
/**
|
|
23
|
+
* Configure the signal factory used by vapor-chamber composables.
|
|
24
|
+
* Call this once at app setup when Vue Vapor's signal API is available.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* import { signal } from 'vue-vapor';
|
|
28
|
+
* import { configureSignal } from 'vapor-chamber';
|
|
29
|
+
* configureSignal(signal);
|
|
30
|
+
*/
|
|
31
|
+
export function configureSignal(fn) {
|
|
32
|
+
_signalFn = fn;
|
|
28
33
|
}
|
|
29
|
-
const signal =
|
|
34
|
+
export const signal = (initial) => _signalFn(initial);
|
|
30
35
|
/**
|
|
31
36
|
* Shared command bus instance
|
|
32
37
|
*/
|
|
@@ -57,12 +62,28 @@ export function useCommand() {
|
|
|
57
62
|
}
|
|
58
63
|
return result;
|
|
59
64
|
}
|
|
65
|
+
const cleanups = [];
|
|
66
|
+
function register(action, handler) {
|
|
67
|
+
const unregister = bus.register(action, handler);
|
|
68
|
+
cleanups.push(unregister);
|
|
69
|
+
return unregister;
|
|
70
|
+
}
|
|
71
|
+
function use(plugin) {
|
|
72
|
+
const remove = bus.use(plugin);
|
|
73
|
+
cleanups.push(remove);
|
|
74
|
+
return remove;
|
|
75
|
+
}
|
|
76
|
+
function dispose() {
|
|
77
|
+
cleanups.forEach(fn => fn());
|
|
78
|
+
cleanups.length = 0;
|
|
79
|
+
}
|
|
60
80
|
return {
|
|
61
81
|
dispatch,
|
|
62
82
|
loading,
|
|
63
83
|
lastError,
|
|
64
|
-
register
|
|
65
|
-
use
|
|
84
|
+
register,
|
|
85
|
+
use,
|
|
86
|
+
dispose,
|
|
66
87
|
};
|
|
67
88
|
}
|
|
68
89
|
/**
|
|
@@ -86,6 +107,21 @@ export function useCommandState(initial, handlers) {
|
|
|
86
107
|
};
|
|
87
108
|
return { state, dispose };
|
|
88
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* useCommandBus - lightweight composable wrapper around the shared bus.
|
|
112
|
+
*
|
|
113
|
+
* Designed for the "toolbox" pattern: import only when needed, tree-shaken
|
|
114
|
+
* out of builds that don't use it. Provides the full bus API plus reactive
|
|
115
|
+
* loading/error signals — without the automatic cleanup tracking of useCommand.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* import { useCommandBus } from 'vapor-chamber';
|
|
119
|
+
* const bus = useCommandBus();
|
|
120
|
+
* bus.dispatch('cart.add', product, { quantity: 1 });
|
|
121
|
+
*/
|
|
122
|
+
export function useCommandBus() {
|
|
123
|
+
return getCommandBus();
|
|
124
|
+
}
|
|
89
125
|
/**
|
|
90
126
|
* useCommandHistory - undo/redo with reactive state
|
|
91
127
|
*/
|
package/dist/command-bus.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vapor-chamber - Command Bus for Vue Vapor
|
|
3
|
-
* ~
|
|
3
|
+
* ~2KB gzipped (core + plugins + composables) — DevTools loaded dynamically
|
|
4
4
|
*/
|
|
5
5
|
export type Command = {
|
|
6
6
|
action: string;
|
|
@@ -13,26 +13,55 @@ export type CommandResult = {
|
|
|
13
13
|
error?: Error;
|
|
14
14
|
};
|
|
15
15
|
export type Handler = (cmd: Command) => any;
|
|
16
|
-
export type AsyncHandler = (cmd: Command) =>
|
|
16
|
+
export type AsyncHandler = (cmd: Command) => Promise<any>;
|
|
17
17
|
export type Plugin = (cmd: Command, next: () => CommandResult) => CommandResult;
|
|
18
18
|
export type AsyncPlugin = (cmd: Command, next: () => CommandResult | Promise<CommandResult>) => CommandResult | Promise<CommandResult>;
|
|
19
19
|
export type Hook = (cmd: Command, result: CommandResult) => void;
|
|
20
20
|
export type AsyncHook = (cmd: Command, result: CommandResult) => void | Promise<void>;
|
|
21
|
+
/** Options for plugin registration. Higher priority runs first (outermost). Default: 0. */
|
|
22
|
+
export type PluginOptions = {
|
|
23
|
+
priority?: number;
|
|
24
|
+
};
|
|
25
|
+
/** Batch dispatch input */
|
|
26
|
+
export type BatchCommand = {
|
|
27
|
+
action: string;
|
|
28
|
+
target: any;
|
|
29
|
+
payload?: any;
|
|
30
|
+
};
|
|
31
|
+
/** Result of a batch dispatch */
|
|
32
|
+
export type BatchResult = {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
results: CommandResult[];
|
|
35
|
+
error?: Error;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Dead letter mode — what to do when a command has no registered handler.
|
|
39
|
+
* - `'error'` (default): returns `{ ok: false, error }`
|
|
40
|
+
* - `'throw'`: throws the error
|
|
41
|
+
* - `'ignore'`: returns `{ ok: true, value: undefined }`
|
|
42
|
+
* - function: called with the command, return value used as result
|
|
43
|
+
*/
|
|
44
|
+
export type DeadLetterMode = 'error' | 'throw' | 'ignore' | ((cmd: Command) => CommandResult);
|
|
45
|
+
export type CommandBusOptions = {
|
|
46
|
+
onMissing?: DeadLetterMode;
|
|
47
|
+
};
|
|
21
48
|
export interface CommandBus {
|
|
22
49
|
dispatch: (action: string, target: any, payload?: any) => CommandResult;
|
|
50
|
+
dispatchBatch: (commands: BatchCommand[]) => BatchResult;
|
|
23
51
|
register: (action: string, handler: Handler) => () => void;
|
|
24
|
-
use: (plugin: Plugin) => () => void;
|
|
52
|
+
use: (plugin: Plugin, options?: PluginOptions) => () => void;
|
|
25
53
|
onAfter: (hook: Hook) => () => void;
|
|
26
54
|
}
|
|
27
55
|
export interface AsyncCommandBus {
|
|
28
56
|
dispatch: (action: string, target: any, payload?: any) => Promise<CommandResult>;
|
|
57
|
+
dispatchBatch: (commands: BatchCommand[]) => Promise<BatchResult>;
|
|
29
58
|
register: (action: string, handler: AsyncHandler) => () => void;
|
|
30
|
-
use: (plugin: AsyncPlugin) => () => void;
|
|
59
|
+
use: (plugin: AsyncPlugin, options?: PluginOptions) => () => void;
|
|
31
60
|
onAfter: (hook: AsyncHook) => () => void;
|
|
32
61
|
}
|
|
33
|
-
export declare function createCommandBus(): CommandBus;
|
|
62
|
+
export declare function createCommandBus(options?: CommandBusOptions): CommandBus;
|
|
34
63
|
/**
|
|
35
64
|
* Async command bus - supports async handlers, plugins, and hooks
|
|
36
65
|
*/
|
|
37
|
-
export declare function createAsyncCommandBus(): AsyncCommandBus;
|
|
66
|
+
export declare function createAsyncCommandBus(options?: CommandBusOptions): AsyncCommandBus;
|
|
38
67
|
//# sourceMappingURL=command-bus.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-bus.d.ts","sourceRoot":"","sources":["../src/command-bus.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,OAAO,GAAG;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,GAAG,CAAC;IACZ,OAAO,CAAC,EAAE,GAAG,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,GAAG,CAAC;AAC5C,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"command-bus.d.ts","sourceRoot":"","sources":["../src/command-bus.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,OAAO,GAAG;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,GAAG,CAAC;IACZ,OAAO,CAAC,EAAE,GAAG,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,GAAG,CAAC;AAC5C,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAC1D,MAAM,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,KAAK,aAAa,CAAC;AAChF,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,KAAK,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;AACvI,MAAM,MAAM,IAAI,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;AACjE,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEtF,2FAA2F;AAC3F,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD,2BAA2B;AAC3B,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;IAAC,OAAO,CAAC,EAAE,GAAG,CAAA;CAAE,CAAC;AAE1E,iCAAiC;AACjC,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,aAAa,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAEnF;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,aAAa,CAAC,CAAC;AAE9F,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B,CAAC;AAEF,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,aAAa,CAAC;IACxE,aAAa,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,WAAW,CAAC;IACzD,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;IAC3D,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,KAAK,MAAM,IAAI,CAAC;IAC7D,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IACjF,aAAa,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAClE,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,MAAM,IAAI,CAAC;IAChE,GAAG,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,aAAa,KAAK,MAAM,IAAI,CAAC;IAClE,OAAO,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,MAAM,IAAI,CAAC;CAC1C;AAeD,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAwF5E;AAgBD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,iBAAsB,GAAG,eAAe,CAuFtF"}
|
package/dist/command-bus.js
CHANGED
|
@@ -1,31 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vapor-chamber - Command Bus for Vue Vapor
|
|
3
|
-
* ~
|
|
3
|
+
* ~2KB gzipped (core + plugins + composables) — DevTools loaded dynamically
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
// Build a runner once per plugin-list change. On each dispatch the runner
|
|
6
|
+
// receives cmd and execute as arguments — no per-dispatch closure allocation.
|
|
7
|
+
function buildRunner(plugins) {
|
|
8
|
+
return function run(cmd, execute) {
|
|
9
|
+
let i = 0;
|
|
10
|
+
function next() {
|
|
11
|
+
const plugin = plugins[i++];
|
|
12
|
+
return plugin ? plugin(cmd, next) : execute();
|
|
13
|
+
}
|
|
14
|
+
return next();
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function createCommandBus(options = {}) {
|
|
6
18
|
const handlers = new Map();
|
|
7
|
-
const
|
|
19
|
+
const pluginEntries = [];
|
|
8
20
|
const afterHooks = [];
|
|
21
|
+
// Cached runner — rebuilt only when plugins are added or removed
|
|
22
|
+
let runner = buildRunner([]);
|
|
23
|
+
function handleMissing(cmd) {
|
|
24
|
+
const mode = options.onMissing ?? 'error';
|
|
25
|
+
if (mode === 'ignore')
|
|
26
|
+
return { ok: true, value: undefined };
|
|
27
|
+
if (mode === 'throw')
|
|
28
|
+
throw new Error(`No handler: ${cmd.action}`);
|
|
29
|
+
if (typeof mode === 'function')
|
|
30
|
+
return mode(cmd);
|
|
31
|
+
// 'error' (default)
|
|
32
|
+
return { ok: false, error: new Error(`No handler: ${cmd.action}`) };
|
|
33
|
+
}
|
|
34
|
+
function rebuildRunner() {
|
|
35
|
+
const sorted = pluginEntries
|
|
36
|
+
.slice()
|
|
37
|
+
.sort((a, b) => b.priority - a.priority)
|
|
38
|
+
.map(e => e.plugin);
|
|
39
|
+
runner = buildRunner(sorted);
|
|
40
|
+
}
|
|
9
41
|
function dispatch(action, target, payload) {
|
|
10
42
|
const cmd = { action, target, payload };
|
|
11
43
|
const handler = handlers.get(action);
|
|
12
|
-
// Build execution chain: plugins wrap the handler
|
|
13
44
|
const execute = () => {
|
|
14
|
-
if (!handler)
|
|
15
|
-
return
|
|
16
|
-
}
|
|
45
|
+
if (!handler)
|
|
46
|
+
return handleMissing(cmd);
|
|
17
47
|
try {
|
|
18
|
-
|
|
19
|
-
return { ok: true, value };
|
|
48
|
+
return { ok: true, value: handler(cmd) };
|
|
20
49
|
}
|
|
21
50
|
catch (e) {
|
|
22
51
|
return { ok: false, error: e };
|
|
23
52
|
}
|
|
24
53
|
};
|
|
25
|
-
|
|
26
|
-
const chain = plugins.reduceRight((next, plugin) => () => plugin(cmd, next), execute);
|
|
27
|
-
const result = chain();
|
|
28
|
-
// Run after hooks
|
|
54
|
+
const result = runner(cmd, execute);
|
|
29
55
|
for (const hook of afterHooks) {
|
|
30
56
|
try {
|
|
31
57
|
hook(cmd, result);
|
|
@@ -36,16 +62,30 @@ export function createCommandBus() {
|
|
|
36
62
|
}
|
|
37
63
|
return result;
|
|
38
64
|
}
|
|
65
|
+
function dispatchBatch(commands) {
|
|
66
|
+
const results = [];
|
|
67
|
+
for (const { action, target, payload } of commands) {
|
|
68
|
+
const result = dispatch(action, target, payload);
|
|
69
|
+
results.push(result);
|
|
70
|
+
if (!result.ok)
|
|
71
|
+
return { ok: false, results, error: result.error };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, results };
|
|
74
|
+
}
|
|
39
75
|
function register(action, handler) {
|
|
40
76
|
handlers.set(action, handler);
|
|
41
77
|
return () => handlers.delete(action);
|
|
42
78
|
}
|
|
43
|
-
function use(plugin) {
|
|
44
|
-
|
|
79
|
+
function use(plugin, opts = {}) {
|
|
80
|
+
const entry = { plugin, priority: opts.priority ?? 0 };
|
|
81
|
+
pluginEntries.push(entry);
|
|
82
|
+
rebuildRunner();
|
|
45
83
|
return () => {
|
|
46
|
-
const i =
|
|
47
|
-
if (i !== -1)
|
|
48
|
-
|
|
84
|
+
const i = pluginEntries.indexOf(entry);
|
|
85
|
+
if (i !== -1) {
|
|
86
|
+
pluginEntries.splice(i, 1);
|
|
87
|
+
rebuildRunner();
|
|
88
|
+
}
|
|
49
89
|
};
|
|
50
90
|
}
|
|
51
91
|
function onAfter(hook) {
|
|
@@ -56,35 +96,58 @@ export function createCommandBus() {
|
|
|
56
96
|
afterHooks.splice(i, 1);
|
|
57
97
|
};
|
|
58
98
|
}
|
|
59
|
-
return { dispatch, register, use, onAfter };
|
|
99
|
+
return { dispatch, dispatchBatch, register, use, onAfter };
|
|
100
|
+
}
|
|
101
|
+
function buildAsyncRunner(plugins) {
|
|
102
|
+
return function run(cmd, execute) {
|
|
103
|
+
let i = 0;
|
|
104
|
+
function next() {
|
|
105
|
+
const plugin = plugins[i++];
|
|
106
|
+
return plugin ? plugin(cmd, next) : execute();
|
|
107
|
+
}
|
|
108
|
+
return Promise.resolve(next());
|
|
109
|
+
};
|
|
60
110
|
}
|
|
61
111
|
/**
|
|
62
112
|
* Async command bus - supports async handlers, plugins, and hooks
|
|
63
113
|
*/
|
|
64
|
-
export function createAsyncCommandBus() {
|
|
114
|
+
export function createAsyncCommandBus(options = {}) {
|
|
65
115
|
const handlers = new Map();
|
|
66
|
-
const
|
|
116
|
+
const pluginEntries = [];
|
|
67
117
|
const afterHooks = [];
|
|
118
|
+
// Cached runner — rebuilt only when plugins are added or removed
|
|
119
|
+
let runner = buildAsyncRunner([]);
|
|
120
|
+
function handleMissing(cmd) {
|
|
121
|
+
const mode = options.onMissing ?? 'error';
|
|
122
|
+
if (mode === 'ignore')
|
|
123
|
+
return { ok: true, value: undefined };
|
|
124
|
+
if (mode === 'throw')
|
|
125
|
+
throw new Error(`No handler: ${cmd.action}`);
|
|
126
|
+
if (typeof mode === 'function')
|
|
127
|
+
return mode(cmd);
|
|
128
|
+
return { ok: false, error: new Error(`No handler: ${cmd.action}`) };
|
|
129
|
+
}
|
|
130
|
+
function rebuildRunner() {
|
|
131
|
+
const sorted = pluginEntries
|
|
132
|
+
.slice()
|
|
133
|
+
.sort((a, b) => b.priority - a.priority)
|
|
134
|
+
.map(e => e.plugin);
|
|
135
|
+
runner = buildAsyncRunner(sorted);
|
|
136
|
+
}
|
|
68
137
|
async function dispatch(action, target, payload) {
|
|
69
138
|
const cmd = { action, target, payload };
|
|
70
139
|
const handler = handlers.get(action);
|
|
71
|
-
// Build execution chain: plugins wrap the handler
|
|
72
140
|
const execute = async () => {
|
|
73
|
-
if (!handler)
|
|
74
|
-
return
|
|
75
|
-
}
|
|
141
|
+
if (!handler)
|
|
142
|
+
return handleMissing(cmd);
|
|
76
143
|
try {
|
|
77
|
-
|
|
78
|
-
return { ok: true, value };
|
|
144
|
+
return { ok: true, value: await handler(cmd) };
|
|
79
145
|
}
|
|
80
146
|
catch (e) {
|
|
81
147
|
return { ok: false, error: e };
|
|
82
148
|
}
|
|
83
149
|
};
|
|
84
|
-
|
|
85
|
-
const chain = plugins.reduceRight((next, plugin) => () => plugin(cmd, next), execute);
|
|
86
|
-
const result = await chain();
|
|
87
|
-
// Run after hooks
|
|
150
|
+
const result = await runner(cmd, execute);
|
|
88
151
|
for (const hook of afterHooks) {
|
|
89
152
|
try {
|
|
90
153
|
await hook(cmd, result);
|
|
@@ -95,16 +158,30 @@ export function createAsyncCommandBus() {
|
|
|
95
158
|
}
|
|
96
159
|
return result;
|
|
97
160
|
}
|
|
161
|
+
async function dispatchBatch(commands) {
|
|
162
|
+
const results = [];
|
|
163
|
+
for (const { action, target, payload } of commands) {
|
|
164
|
+
const result = await dispatch(action, target, payload);
|
|
165
|
+
results.push(result);
|
|
166
|
+
if (!result.ok)
|
|
167
|
+
return { ok: false, results, error: result.error };
|
|
168
|
+
}
|
|
169
|
+
return { ok: true, results };
|
|
170
|
+
}
|
|
98
171
|
function register(action, handler) {
|
|
99
172
|
handlers.set(action, handler);
|
|
100
173
|
return () => handlers.delete(action);
|
|
101
174
|
}
|
|
102
|
-
function use(plugin) {
|
|
103
|
-
|
|
175
|
+
function use(plugin, opts = {}) {
|
|
176
|
+
const entry = { plugin, priority: opts.priority ?? 0 };
|
|
177
|
+
pluginEntries.push(entry);
|
|
178
|
+
rebuildRunner();
|
|
104
179
|
return () => {
|
|
105
|
-
const i =
|
|
106
|
-
if (i !== -1)
|
|
107
|
-
|
|
180
|
+
const i = pluginEntries.indexOf(entry);
|
|
181
|
+
if (i !== -1) {
|
|
182
|
+
pluginEntries.splice(i, 1);
|
|
183
|
+
rebuildRunner();
|
|
184
|
+
}
|
|
108
185
|
};
|
|
109
186
|
}
|
|
110
187
|
function onAfter(hook) {
|
|
@@ -115,5 +192,5 @@ export function createAsyncCommandBus() {
|
|
|
115
192
|
afterHooks.splice(i, 1);
|
|
116
193
|
};
|
|
117
194
|
}
|
|
118
|
-
return { dispatch, register, use, onAfter };
|
|
195
|
+
return { dispatch, dispatchBatch, register, use, onAfter };
|
|
119
196
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vapor-chamber - Vue DevTools integration
|
|
3
|
+
*
|
|
4
|
+
* Optional. Call setupDevtools(bus, app) once at app setup.
|
|
5
|
+
* Requires @vue/devtools-api to be installed — silently no-ops if not present.
|
|
6
|
+
*/
|
|
7
|
+
import type { Hook } from './command-bus';
|
|
8
|
+
interface Observable {
|
|
9
|
+
onAfter: (hook: Hook) => () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Connect a command bus to Vue DevTools.
|
|
13
|
+
*
|
|
14
|
+
* - Adds a **Commands** timeline layer: every dispatch appears as an event,
|
|
15
|
+
* green for success, red for error.
|
|
16
|
+
* - Adds a **Vapor Chamber** inspector panel: browse recent commands,
|
|
17
|
+
* inspect target/payload/result of each one.
|
|
18
|
+
*
|
|
19
|
+
* @param bus A CommandBus or AsyncCommandBus instance to observe.
|
|
20
|
+
* @param app The Vue app instance (passed to setupDevtoolsPlugin).
|
|
21
|
+
* @returns Unsubscribe function — call it to detach from the bus.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* import { createApp } from 'vue';
|
|
25
|
+
* import { getCommandBus, setupDevtools } from 'vapor-chamber';
|
|
26
|
+
*
|
|
27
|
+
* const app = createApp(App);
|
|
28
|
+
* setupDevtools(getCommandBus(), app);
|
|
29
|
+
* app.mount('#app');
|
|
30
|
+
*/
|
|
31
|
+
export declare function setupDevtools(bus: Observable, app: unknown): () => void;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=devtools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../src/devtools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAA0B,IAAI,EAAE,MAAM,eAAe,CAAC;AAOlE,UAAU,UAAU;IAClB,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,IAAI,CAAC;CACrC;AASD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,IAAI,CAyIvE"}
|
package/dist/devtools.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vapor-chamber - Vue DevTools integration
|
|
3
|
+
*
|
|
4
|
+
* Optional. Call setupDevtools(bus, app) once at app setup.
|
|
5
|
+
* Requires @vue/devtools-api to be installed — silently no-ops if not present.
|
|
6
|
+
*/
|
|
7
|
+
const INSPECTOR_ID = 'vapor-chamber';
|
|
8
|
+
const LAYER_ID = 'vapor-chamber';
|
|
9
|
+
/**
|
|
10
|
+
* Connect a command bus to Vue DevTools.
|
|
11
|
+
*
|
|
12
|
+
* - Adds a **Commands** timeline layer: every dispatch appears as an event,
|
|
13
|
+
* green for success, red for error.
|
|
14
|
+
* - Adds a **Vapor Chamber** inspector panel: browse recent commands,
|
|
15
|
+
* inspect target/payload/result of each one.
|
|
16
|
+
*
|
|
17
|
+
* @param bus A CommandBus or AsyncCommandBus instance to observe.
|
|
18
|
+
* @param app The Vue app instance (passed to setupDevtoolsPlugin).
|
|
19
|
+
* @returns Unsubscribe function — call it to detach from the bus.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* import { createApp } from 'vue';
|
|
23
|
+
* import { getCommandBus, setupDevtools } from 'vapor-chamber';
|
|
24
|
+
*
|
|
25
|
+
* const app = createApp(App);
|
|
26
|
+
* setupDevtools(getCommandBus(), app);
|
|
27
|
+
* app.mount('#app');
|
|
28
|
+
*/
|
|
29
|
+
export function setupDevtools(bus, app) {
|
|
30
|
+
// Guard: no-op in production. Bundlers (Vite, webpack, Rollup) replace
|
|
31
|
+
// process.env.NODE_ENV with 'production' in prod builds, making this entire
|
|
32
|
+
// function body dead code that tree-shakers eliminate for a true 0KB footprint.
|
|
33
|
+
// globalThis cast avoids requiring @types/node while preserving the replacement target.
|
|
34
|
+
const env = globalThis.process?.env?.NODE_ENV;
|
|
35
|
+
if (env === 'production') {
|
|
36
|
+
return () => { };
|
|
37
|
+
}
|
|
38
|
+
const entries = [];
|
|
39
|
+
let counter = 0;
|
|
40
|
+
let devApi = null;
|
|
41
|
+
// Hook into the bus — this runs even before devtools loads
|
|
42
|
+
const unsubscribe = bus.onAfter((cmd, result) => {
|
|
43
|
+
const entry = {
|
|
44
|
+
id: counter++,
|
|
45
|
+
cmd,
|
|
46
|
+
result,
|
|
47
|
+
time: Date.now(),
|
|
48
|
+
};
|
|
49
|
+
entries.unshift(entry);
|
|
50
|
+
if (entries.length > 100)
|
|
51
|
+
entries.pop(); // keep last 100 commands
|
|
52
|
+
if (devApi) {
|
|
53
|
+
devApi.addTimelineEvent({
|
|
54
|
+
layerId: LAYER_ID,
|
|
55
|
+
event: {
|
|
56
|
+
time: Date.now(),
|
|
57
|
+
title: cmd.action,
|
|
58
|
+
subtitle: result.ok ? '✓' : '✗ error',
|
|
59
|
+
data: {
|
|
60
|
+
action: cmd.action,
|
|
61
|
+
target: cmd.target,
|
|
62
|
+
...(cmd.payload !== undefined ? { payload: cmd.payload } : {}),
|
|
63
|
+
ok: result.ok,
|
|
64
|
+
...(result.ok
|
|
65
|
+
? { value: result.value }
|
|
66
|
+
: { error: result.error?.message }),
|
|
67
|
+
},
|
|
68
|
+
logType: result.ok ? 'default' : 'error',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
devApi.sendInspectorTree(INSPECTOR_ID);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Dynamic import — zero cost if @vue/devtools-api is not installed.
|
|
75
|
+
// Using a variable prevents TypeScript from attempting module resolution
|
|
76
|
+
// on an optional peer dependency that may not be installed.
|
|
77
|
+
const devtoolsModule = '@vue/devtools-api';
|
|
78
|
+
import(devtoolsModule)
|
|
79
|
+
.then(({ setupDevtoolsPlugin }) => {
|
|
80
|
+
setupDevtoolsPlugin({
|
|
81
|
+
id: 'vapor-chamber',
|
|
82
|
+
label: 'Vapor Chamber',
|
|
83
|
+
packageName: 'vapor-chamber',
|
|
84
|
+
homepage: 'https://github.com/lucianofedericopereira/vapor-chamber',
|
|
85
|
+
app,
|
|
86
|
+
}, (api) => {
|
|
87
|
+
devApi = api;
|
|
88
|
+
// Timeline layer: one event per dispatched command
|
|
89
|
+
api.addTimelineLayer({
|
|
90
|
+
id: LAYER_ID,
|
|
91
|
+
color: 0x41b883, // Vue green
|
|
92
|
+
label: 'Commands',
|
|
93
|
+
});
|
|
94
|
+
// Inspector panel: browse and inspect recent commands
|
|
95
|
+
api.addInspector({
|
|
96
|
+
id: INSPECTOR_ID,
|
|
97
|
+
label: 'Vapor Chamber',
|
|
98
|
+
icon: 'mediation',
|
|
99
|
+
treeFilterPlaceholder: 'Filter by action',
|
|
100
|
+
});
|
|
101
|
+
// Build the inspector tree from buffered entries
|
|
102
|
+
api.on.getInspectorTree((payload) => {
|
|
103
|
+
if (payload.inspectorId !== INSPECTOR_ID)
|
|
104
|
+
return;
|
|
105
|
+
const filter = (payload.filter ?? '').toLowerCase();
|
|
106
|
+
payload.rootNodes = entries
|
|
107
|
+
.filter(e => !filter || e.cmd.action.toLowerCase().includes(filter))
|
|
108
|
+
.map(e => ({
|
|
109
|
+
id: String(e.id),
|
|
110
|
+
label: e.cmd.action,
|
|
111
|
+
tags: [
|
|
112
|
+
{
|
|
113
|
+
label: e.result.ok ? 'ok' : 'error',
|
|
114
|
+
textColor: 0xffffff,
|
|
115
|
+
backgroundColor: e.result.ok ? 0x41b883 : 0xff4444,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
// Show full detail when a node is selected in the inspector
|
|
121
|
+
api.on.getInspectorState((payload) => {
|
|
122
|
+
if (payload.inspectorId !== INSPECTOR_ID)
|
|
123
|
+
return;
|
|
124
|
+
const entry = entries.find(e => String(e.id) === payload.nodeId);
|
|
125
|
+
if (!entry)
|
|
126
|
+
return;
|
|
127
|
+
payload.state = {
|
|
128
|
+
command: [
|
|
129
|
+
{ key: 'action', value: entry.cmd.action },
|
|
130
|
+
{ key: 'target', value: entry.cmd.target },
|
|
131
|
+
...(entry.cmd.payload !== undefined
|
|
132
|
+
? [{ key: 'payload', value: entry.cmd.payload }]
|
|
133
|
+
: []),
|
|
134
|
+
],
|
|
135
|
+
result: [
|
|
136
|
+
{ key: 'ok', value: entry.result.ok },
|
|
137
|
+
...(entry.result.ok
|
|
138
|
+
? [{ key: 'value', value: entry.result.value }]
|
|
139
|
+
: [{ key: 'error', value: entry.result.error?.message }]),
|
|
140
|
+
],
|
|
141
|
+
meta: [
|
|
142
|
+
{ key: 'time', value: new Date(entry.time).toISOString() },
|
|
143
|
+
{ key: 'index', value: entry.id },
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
})
|
|
149
|
+
.catch(() => {
|
|
150
|
+
// @vue/devtools-api not installed — silently no-op in production
|
|
151
|
+
});
|
|
152
|
+
return unsubscribe;
|
|
153
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vapor-chamber - Lightweight command bus for Vue Vapor
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* ~2KB gzipped (core + plugins + composables). DevTools loaded dynamically.
|
|
5
5
|
*/
|
|
6
|
-
export { createCommandBus, createAsyncCommandBus, type Command, type CommandResult, type CommandBus, type AsyncCommandBus, type Handler, type AsyncHandler, type Plugin, type AsyncPlugin, type Hook, type AsyncHook, } from './command-bus';
|
|
6
|
+
export { createCommandBus, createAsyncCommandBus, type Command, type CommandResult, type CommandBus, type AsyncCommandBus, type Handler, type AsyncHandler, type Plugin, type AsyncPlugin, type Hook, type AsyncHook, type PluginOptions, type BatchCommand, type BatchResult, type DeadLetterMode, type CommandBusOptions, } from './command-bus';
|
|
7
|
+
export { createTestBus, type TestBus, type RecordedDispatch } from './testing';
|
|
7
8
|
export { logger, validator, history, debounce, throttle, type HistoryState, } from './plugins';
|
|
8
|
-
export { getCommandBus, setCommandBus, useCommand, useCommandState, useCommandHistory, } from './chamber';
|
|
9
|
+
export { signal, configureSignal, type Signal, type CreateSignal, getCommandBus, setCommandBus, useCommandBus, useCommand, useCommandState, useCommandHistory, } from './chamber';
|
|
10
|
+
export { setupDevtools } from './devtools';
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,WAAW,EAChB,KAAK,IAAI,EACT,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,WAAW,EAChB,KAAK,IAAI,EACT,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,aAAa,EAAE,KAAK,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAG/E,OAAO,EACL,MAAM,EACN,SAAS,EACT,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,KAAK,YAAY,GAClB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,MAAM,EACN,eAAe,EACf,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,aAAa,EACb,aAAa,EACb,aAAa,EACb,UAAU,EACV,eAAe,EACf,iBAAiB,GAClB,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vapor-chamber - Lightweight command bus for Vue Vapor
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* ~2KB gzipped (core + plugins + composables). DevTools loaded dynamically.
|
|
5
5
|
*/
|
|
6
6
|
// Core
|
|
7
7
|
export { createCommandBus, createAsyncCommandBus, } from './command-bus';
|
|
8
|
+
// Testing utilities
|
|
9
|
+
export { createTestBus } from './testing';
|
|
8
10
|
// Plugins
|
|
9
11
|
export { logger, validator, history, debounce, throttle, } from './plugins';
|
|
10
12
|
// Vapor integration
|
|
11
|
-
export { getCommandBus, setCommandBus, useCommand, useCommandState, useCommandHistory, } from './chamber';
|
|
13
|
+
export { signal, configureSignal, getCommandBus, setCommandBus, useCommandBus, useCommand, useCommandState, useCommandHistory, } from './chamber';
|
|
14
|
+
// DevTools integration (optional — requires @vue/devtools-api)
|
|
15
|
+
export { setupDevtools } from './devtools';
|
package/dist/plugins.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAiB,MAAM,EAAE,MAAM,eAAe,CAAC;AAEpE;;GAEG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;CAC/B,GAAG,MAAM,CAwBd;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE;IAC/B,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;CACnD,GAAG,MAAM,CAWT;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,OAAO,CAAC,OAAO,GAAE;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;CAC/B,GAAG,MAAM,GAAG;IAChB,QAAQ,EAAE,MAAM,YAAY,CAAC;IAC7B,IAAI,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC;IAChC,IAAI,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC;IAChC,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAwCA;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,MAAM,EAAE,EACjB,IAAI,EAAE,MAAM,GACX,MAAM,
|
|
1
|
+
{"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAiB,MAAM,EAAE,MAAM,eAAe,CAAC;AAEpE;;GAEG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;CAC/B,GAAG,MAAM,CAwBd;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE;IAC/B,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;CACnD,GAAG,MAAM,CAWT;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,OAAO,CAAC,OAAO,GAAE;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;CAC/B,GAAG,MAAM,GAAG;IAChB,QAAQ,EAAE,MAAM,YAAY,CAAC;IAC7B,IAAI,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC;IAChC,IAAI,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC;IAChC,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAwCA;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,MAAM,EAAE,EACjB,IAAI,EAAE,MAAM,GACX,MAAM,CA0BR;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,MAAM,EAAE,EACjB,IAAI,EAAE,MAAM,GACX,MAAM,CAqBR"}
|
package/dist/plugins.js
CHANGED
|
@@ -102,6 +102,8 @@ export function debounce(actions, wait) {
|
|
|
102
102
|
const result = next();
|
|
103
103
|
results.set(key, result);
|
|
104
104
|
timers.delete(key);
|
|
105
|
+
// Clean up result after one render cycle to avoid unbounded map growth
|
|
106
|
+
setTimeout(() => results.delete(key), 0);
|
|
105
107
|
}, wait));
|
|
106
108
|
// Return pending status synchronously (check results map for actual result)
|
|
107
109
|
return results.get(key) ?? { ok: true, value: { pending: true, key } };
|
|
@@ -122,6 +124,8 @@ export function throttle(actions, wait) {
|
|
|
122
124
|
const last = lastRun.get(key) ?? 0;
|
|
123
125
|
if (now - last >= wait) {
|
|
124
126
|
lastRun.set(key, now);
|
|
127
|
+
// Schedule removal after the window so the map doesn't grow unbounded
|
|
128
|
+
setTimeout(() => lastRun.delete(key), wait);
|
|
125
129
|
return next();
|
|
126
130
|
}
|
|
127
131
|
// Throttled - return skipped status
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vapor-chamber - Testing utilities
|
|
3
|
+
*
|
|
4
|
+
* createTestBus() creates a command bus that records all dispatched commands
|
|
5
|
+
* without executing real handlers. Useful for unit-testing components that
|
|
6
|
+
* use useCommand() without wiring up the full application logic.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const bus = createTestBus();
|
|
10
|
+
* setCommandBus(bus);
|
|
11
|
+
*
|
|
12
|
+
* // Dispatch something under test
|
|
13
|
+
* bus.dispatch('cart:add', cart, { id: 1 });
|
|
14
|
+
*
|
|
15
|
+
* // Assert
|
|
16
|
+
* expect(bus.wasDispatched('cart:add')).toBe(true);
|
|
17
|
+
* expect(bus.getDispatched('cart:add')[0].cmd.payload).toEqual({ id: 1 });
|
|
18
|
+
*/
|
|
19
|
+
import type { Command, CommandResult, CommandBus } from './command-bus';
|
|
20
|
+
export interface RecordedDispatch {
|
|
21
|
+
cmd: Command;
|
|
22
|
+
result: CommandResult;
|
|
23
|
+
}
|
|
24
|
+
export interface TestBus extends CommandBus {
|
|
25
|
+
/** All dispatched commands in order */
|
|
26
|
+
readonly recorded: RecordedDispatch[];
|
|
27
|
+
/** True if any command with this action was dispatched */
|
|
28
|
+
wasDispatched(action: string): boolean;
|
|
29
|
+
/** All recorded dispatches for a given action */
|
|
30
|
+
getDispatched(action: string): RecordedDispatch[];
|
|
31
|
+
/** Clear the recorded list */
|
|
32
|
+
clear(): void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Creates a test bus that stubs all handlers (returning `{ ok: true }`) unless
|
|
36
|
+
* you register your own via `bus.register()`. All dispatches are recorded.
|
|
37
|
+
*
|
|
38
|
+
* Pass `{ passthroughHandlers: true }` to execute real handlers while still
|
|
39
|
+
* recording every dispatch.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createTestBus(opts?: {
|
|
42
|
+
passthroughHandlers?: boolean;
|
|
43
|
+
}): TestBus;
|
|
44
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAmE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEzI,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,OAAQ,SAAQ,UAAU;IACzC,uCAAuC;IACvC,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IACtC,0DAA0D;IAC1D,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,iDAAiD;IACjD,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAAC;IAClD,8BAA8B;IAC9B,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,GAAE;IAAE,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAuGnF"}
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vapor-chamber - Testing utilities
|
|
3
|
+
*
|
|
4
|
+
* createTestBus() creates a command bus that records all dispatched commands
|
|
5
|
+
* without executing real handlers. Useful for unit-testing components that
|
|
6
|
+
* use useCommand() without wiring up the full application logic.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const bus = createTestBus();
|
|
10
|
+
* setCommandBus(bus);
|
|
11
|
+
*
|
|
12
|
+
* // Dispatch something under test
|
|
13
|
+
* bus.dispatch('cart:add', cart, { id: 1 });
|
|
14
|
+
*
|
|
15
|
+
* // Assert
|
|
16
|
+
* expect(bus.wasDispatched('cart:add')).toBe(true);
|
|
17
|
+
* expect(bus.getDispatched('cart:add')[0].cmd.payload).toEqual({ id: 1 });
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Creates a test bus that stubs all handlers (returning `{ ok: true }`) unless
|
|
21
|
+
* you register your own via `bus.register()`. All dispatches are recorded.
|
|
22
|
+
*
|
|
23
|
+
* Pass `{ passthroughHandlers: true }` to execute real handlers while still
|
|
24
|
+
* recording every dispatch.
|
|
25
|
+
*/
|
|
26
|
+
export function createTestBus(opts = {}) {
|
|
27
|
+
const handlers = new Map();
|
|
28
|
+
const plugins = [];
|
|
29
|
+
const afterHooks = [];
|
|
30
|
+
const recorded = [];
|
|
31
|
+
function buildRunner(sortedPlugins) {
|
|
32
|
+
return function run(cmd, execute) {
|
|
33
|
+
let i = 0;
|
|
34
|
+
function next() {
|
|
35
|
+
const plugin = sortedPlugins[i++];
|
|
36
|
+
return plugin ? plugin(cmd, next) : execute();
|
|
37
|
+
}
|
|
38
|
+
return next();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
let runner = buildRunner([]);
|
|
42
|
+
function rebuildRunner() {
|
|
43
|
+
const sorted = plugins.slice().sort((a, b) => b.priority - a.priority).map(e => e.plugin);
|
|
44
|
+
runner = buildRunner(sorted);
|
|
45
|
+
}
|
|
46
|
+
function dispatch(action, target, payload) {
|
|
47
|
+
const cmd = { action, target, payload };
|
|
48
|
+
const handler = handlers.get(action);
|
|
49
|
+
const execute = () => {
|
|
50
|
+
if (opts.passthroughHandlers && handler) {
|
|
51
|
+
try {
|
|
52
|
+
return { ok: true, value: handler(cmd) };
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
return { ok: false, error: e };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Stub: return ok:true if no real handler registered, run it if one is
|
|
59
|
+
if (handler) {
|
|
60
|
+
try {
|
|
61
|
+
return { ok: true, value: handler(cmd) };
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
return { ok: false, error: e };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { ok: true, value: undefined };
|
|
68
|
+
};
|
|
69
|
+
const result = runner(cmd, execute);
|
|
70
|
+
recorded.push({ cmd, result });
|
|
71
|
+
for (const hook of afterHooks) {
|
|
72
|
+
try {
|
|
73
|
+
hook(cmd, result);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
console.error('[vapor-chamber/test] Hook error:', e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
function dispatchBatch(commands) {
|
|
82
|
+
const results = [];
|
|
83
|
+
for (const { action, target, payload } of commands) {
|
|
84
|
+
const result = dispatch(action, target, payload);
|
|
85
|
+
results.push(result);
|
|
86
|
+
if (!result.ok)
|
|
87
|
+
return { ok: false, results, error: result.error };
|
|
88
|
+
}
|
|
89
|
+
return { ok: true, results };
|
|
90
|
+
}
|
|
91
|
+
function register(action, handler) {
|
|
92
|
+
handlers.set(action, handler);
|
|
93
|
+
return () => handlers.delete(action);
|
|
94
|
+
}
|
|
95
|
+
function use(plugin, options = {}) {
|
|
96
|
+
const entry = { plugin, priority: options.priority ?? 0 };
|
|
97
|
+
plugins.push(entry);
|
|
98
|
+
rebuildRunner();
|
|
99
|
+
return () => {
|
|
100
|
+
const i = plugins.indexOf(entry);
|
|
101
|
+
if (i !== -1) {
|
|
102
|
+
plugins.splice(i, 1);
|
|
103
|
+
rebuildRunner();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function onAfter(hook) {
|
|
108
|
+
afterHooks.push(hook);
|
|
109
|
+
return () => {
|
|
110
|
+
const i = afterHooks.indexOf(hook);
|
|
111
|
+
if (i !== -1)
|
|
112
|
+
afterHooks.splice(i, 1);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
dispatch,
|
|
117
|
+
dispatchBatch,
|
|
118
|
+
register,
|
|
119
|
+
use,
|
|
120
|
+
onAfter,
|
|
121
|
+
recorded,
|
|
122
|
+
wasDispatched: (action) => recorded.some(r => r.cmd.action === action),
|
|
123
|
+
getDispatched: (action) => recorded.filter(r => r.cmd.action === action),
|
|
124
|
+
clear: () => recorded.splice(0),
|
|
125
|
+
};
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vapor-chamber",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Lightweight command bus for Vue Vapor - plugins, hooks, undo/redo",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
|
+
"sideEffects": false,
|
|
18
19
|
"scripts": {
|
|
19
20
|
"build": "tsc",
|
|
20
21
|
"dev": "tsc --watch",
|
|
@@ -47,11 +48,15 @@
|
|
|
47
48
|
},
|
|
48
49
|
"homepage": "https://github.com/lucianofedericopereira/vapor-chamber#readme",
|
|
49
50
|
"peerDependencies": {
|
|
50
|
-
"vue": ">=3.5.0"
|
|
51
|
+
"vue": ">=3.5.0",
|
|
52
|
+
"@vue/devtools-api": ">=6.0.0"
|
|
51
53
|
},
|
|
52
54
|
"peerDependenciesMeta": {
|
|
53
55
|
"vue": {
|
|
54
56
|
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@vue/devtools-api": {
|
|
59
|
+
"optional": true
|
|
55
60
|
}
|
|
56
61
|
},
|
|
57
62
|
"devDependencies": {
|