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 CHANGED
@@ -1,6 +1,10 @@
1
- # Vapor Chamber
1
+ <p align="center">
2
+ <img src="assets/vapor-chamber.png" alt="Vapor Chamber">
3
+ </p>
2
4
 
3
- A lightweight command bus designed for [Vue Vapor](https://github.com/vuejs/vue-vapor). ~1KB core.
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 in order: first added = outermost wrapper.
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
- | `useCommand()` | Dispatch with reactive loading/error state |
280
- | `useCommandState(initial, handlers)` | State managed by commands |
281
- | `useCommandHistory(options?)` | Reactive undo/redo |
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
@@ -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,KAAK,MAAM,CAAC,CAAC,IAAI;IAAE,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAkC9B,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;;;;;EAqB7E;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;;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"}
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
- // Detect environment and get signal function
9
- function getSignalFn() {
10
- // Try Vapor's signal first (when available)
11
- // @ts-ignore - Vapor not yet typed
12
- if (typeof window !== 'undefined' && window.__VUE_VAPOR__?.signal) {
13
- // @ts-ignore
14
- return window.__VUE_VAPOR__.signal;
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 = getSignalFn();
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: bus.register,
65
- use: bus.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
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * vapor-chamber - Command Bus for Vue Vapor
3
- * ~1KB - Commands + Plugins + Hooks
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) => any | Promise<any>;
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,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;AAChE,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,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,aAAa,CAAC;IACxE,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;IAC3D,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,IAAI,CAAC;IACpC,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,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,MAAM,IAAI,CAAC;IAChE,GAAG,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,IAAI,CAAC;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,MAAM,IAAI,CAAC;CAC1C;AAED,wBAAgB,gBAAgB,IAAI,UAAU,CAgE7C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,eAAe,CAgEvD"}
1
+ {"version":3,"file":"command-bus.d.ts","sourceRoot":"","sources":["../src/command-bus.ts"],"names":[],"mappings":"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"}
@@ -1,31 +1,57 @@
1
1
  /**
2
2
  * vapor-chamber - Command Bus for Vue Vapor
3
- * ~1KB - Commands + Plugins + Hooks
3
+ * ~2KB gzipped (core + plugins + composables) — DevTools loaded dynamically
4
4
  */
5
- export function createCommandBus() {
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 plugins = [];
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 { ok: false, error: new Error(`No handler: ${action}`) };
16
- }
45
+ if (!handler)
46
+ return handleMissing(cmd);
17
47
  try {
18
- const value = handler(cmd);
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
- // Apply plugins (right to left, so first plugin is outermost)
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
- plugins.push(plugin);
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 = plugins.indexOf(plugin);
47
- if (i !== -1)
48
- plugins.splice(i, 1);
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 plugins = [];
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 { ok: false, error: new Error(`No handler: ${action}`) };
75
- }
141
+ if (!handler)
142
+ return handleMissing(cmd);
76
143
  try {
77
- const value = await handler(cmd);
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
- // Apply plugins (right to left, so first plugin is outermost)
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
- plugins.push(plugin);
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 = plugins.indexOf(plugin);
106
- if (i !== -1)
107
- plugins.splice(i, 1);
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"}
@@ -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
- * A ~1KB command bus with plugins, hooks, and Vapor-native reactivity.
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
@@ -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,GACf,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,MAAM,EACN,SAAS,EACT,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,KAAK,YAAY,GAClB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,aAAa,EACb,aAAa,EACb,UAAU,EACV,eAAe,EACf,iBAAiB,GAClB,MAAM,WAAW,CAAC"}
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
- * A ~1KB command bus with plugins, hooks, and Vapor-native reactivity.
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';
@@ -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,CAwBR;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,MAAM,EAAE,EACjB,IAAI,EAAE,MAAM,GACX,MAAM,CAmBR"}
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"}
@@ -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.1.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": {