vapor-chamber 0.1.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/LICENSE +504 -0
- package/README.md +301 -0
- package/dist/chamber.d.ts +49 -0
- package/dist/chamber.d.ts.map +1 -0
- package/dist/chamber.js +149 -0
- package/dist/command-bus.d.ts +38 -0
- package/dist/command-bus.d.ts.map +1 -0
- package/dist/command-bus.js +119 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/plugins.d.ts +46 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +130 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Vapor Chamber
|
|
2
|
+
|
|
3
|
+
A lightweight command bus designed for [Vue Vapor](https://github.com/vuejs/vue-vapor). ~1KB core.
|
|
4
|
+
|
|
5
|
+
## What is Vue Vapor?
|
|
6
|
+
|
|
7
|
+
Vue Vapor is Vue's upcoming compilation strategy that eliminates the Virtual DOM. Instead of diffing virtual trees, Vapor compiles templates to direct DOM operations using **signals** - reactive primitives that update only what changed.
|
|
8
|
+
|
|
9
|
+
**Vapor Chamber** embraces this philosophy: minimal abstraction, direct updates, signal-native reactivity.
|
|
10
|
+
|
|
11
|
+
## Why a Command Bus?
|
|
12
|
+
|
|
13
|
+
Traditional event systems scatter logic across components. A command bus centralizes it:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Event-driven (scattered) Command bus (centralized)
|
|
17
|
+
───────────────────────── ─────────────────────────
|
|
18
|
+
Component A emits 'add' → dispatch('cart.add', product)
|
|
19
|
+
Component B listens... ↓
|
|
20
|
+
Component C also listens... Handler executes once
|
|
21
|
+
Who handles what? When? Plugins observe/modify
|
|
22
|
+
Result returned
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Benefits:**
|
|
26
|
+
- **Semantic actions** - `cart.add` is clearer than `emit('add')`
|
|
27
|
+
- **Single handler** - One place to look, debug, test
|
|
28
|
+
- **Plugin pipeline** - Cross-cutting concerns (logging, validation, analytics) without cluttering handlers
|
|
29
|
+
- **Undo/redo** - Command history is natural when actions are explicit
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install vapor-chamber
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { createCommandBus, logger, validator } from 'vapor-chamber';
|
|
41
|
+
|
|
42
|
+
const bus = createCommandBus();
|
|
43
|
+
|
|
44
|
+
// Add plugins
|
|
45
|
+
bus.use(logger());
|
|
46
|
+
bus.use(validator({
|
|
47
|
+
'cart.add': (cmd) => cmd.payload?.quantity > 0 ? null : 'Quantity required'
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
// Register handler
|
|
51
|
+
bus.register('cart.add', (cmd) => {
|
|
52
|
+
cart.items.push({ ...cmd.target, quantity: cmd.payload.quantity });
|
|
53
|
+
return cart.items;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Dispatch
|
|
57
|
+
const result = bus.dispatch('cart.add', product, { quantity: 2 });
|
|
58
|
+
if (result.ok) {
|
|
59
|
+
console.log('Added:', result.value);
|
|
60
|
+
} else {
|
|
61
|
+
console.error('Failed:', result.error);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Core Concepts
|
|
66
|
+
|
|
67
|
+
### Commands
|
|
68
|
+
|
|
69
|
+
A command has three parts:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
bus.dispatch(
|
|
73
|
+
'cart.add', // action - what to do
|
|
74
|
+
product, // target - what to act on
|
|
75
|
+
{ quantity: 2 } // payload - additional data (optional)
|
|
76
|
+
);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Handlers
|
|
80
|
+
|
|
81
|
+
One handler per action. Returns a value or throws:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
bus.register('cart.add', (cmd) => {
|
|
85
|
+
// cmd.action = 'cart.add'
|
|
86
|
+
// cmd.target = product
|
|
87
|
+
// cmd.payload = { quantity: 2 }
|
|
88
|
+
|
|
89
|
+
cart.items.push(cmd.target);
|
|
90
|
+
return cart.items; // becomes result.value
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Results
|
|
95
|
+
|
|
96
|
+
Every dispatch returns a result:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
type CommandResult = {
|
|
100
|
+
ok: boolean; // success or failure
|
|
101
|
+
value?: any; // handler return value (if ok)
|
|
102
|
+
error?: Error; // error thrown (if not ok)
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Plugins
|
|
107
|
+
|
|
108
|
+
Plugins wrap handlers. They can modify commands, short-circuit execution, observe results, or transform output:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const timingPlugin: Plugin = (cmd, next) => {
|
|
112
|
+
const start = Date.now();
|
|
113
|
+
const result = next(); // call next plugin or handler
|
|
114
|
+
console.log(`${cmd.action} took ${Date.now() - start}ms`);
|
|
115
|
+
return result;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
bus.use(timingPlugin);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Plugins execute in order: first added = outermost wrapper.
|
|
122
|
+
|
|
123
|
+
## Built-in Plugins
|
|
124
|
+
|
|
125
|
+
| Plugin | Description |
|
|
126
|
+
|--------|-------------|
|
|
127
|
+
| `logger(options?)` | Log commands to console |
|
|
128
|
+
| `validator(rules)` | Validate commands before execution |
|
|
129
|
+
| `history(options?)` | Track command history for undo/redo |
|
|
130
|
+
| `debounce(actions, wait)` | Delay execution until activity stops |
|
|
131
|
+
| `throttle(actions, wait)` | Limit execution frequency |
|
|
132
|
+
|
|
133
|
+
### logger
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
bus.use(logger({ collapsed: true, filter: (cmd) => cmd.action.startsWith('cart.') }));
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### validator
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
bus.use(validator({
|
|
143
|
+
'cart.add': (cmd) => {
|
|
144
|
+
if (!cmd.target?.id) return 'Product must have an ID';
|
|
145
|
+
return null; // null = valid
|
|
146
|
+
}
|
|
147
|
+
}));
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### history
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const historyPlugin = history({ maxSize: 100 });
|
|
154
|
+
bus.use(historyPlugin);
|
|
155
|
+
|
|
156
|
+
historyPlugin.undo();
|
|
157
|
+
historyPlugin.redo();
|
|
158
|
+
historyPlugin.getState(); // { past, future, canUndo, canRedo }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### debounce
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
bus.use(debounce(['search.query'], 300)); // wait 300ms after last call
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### throttle
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
bus.use(throttle(['ui.scroll'], 100)); // max once per 100ms
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Async Command Bus
|
|
174
|
+
|
|
175
|
+
For async handlers (API calls, IndexedDB, etc.):
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { createAsyncCommandBus } from 'vapor-chamber';
|
|
179
|
+
|
|
180
|
+
const bus = createAsyncCommandBus();
|
|
181
|
+
|
|
182
|
+
bus.register('user.fetch', async (cmd) => {
|
|
183
|
+
const response = await fetch(`/api/users/${cmd.target.id}`);
|
|
184
|
+
return response.json();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const result = await bus.dispatch('user.fetch', { id: 123 });
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Vapor Composables
|
|
191
|
+
|
|
192
|
+
For Vue Vapor components:
|
|
193
|
+
|
|
194
|
+
### useCommand
|
|
195
|
+
|
|
196
|
+
```vue
|
|
197
|
+
<script setup>
|
|
198
|
+
import { useCommand } from 'vapor-chamber';
|
|
199
|
+
|
|
200
|
+
const { dispatch, loading, lastError } = useCommand();
|
|
201
|
+
</script>
|
|
202
|
+
|
|
203
|
+
<template>
|
|
204
|
+
<button @click="dispatch('save', doc)" :disabled="loading.value">Save</button>
|
|
205
|
+
<p v-if="lastError.value">{{ lastError.value.message }}</p>
|
|
206
|
+
</template>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### useCommandState
|
|
210
|
+
|
|
211
|
+
```vue
|
|
212
|
+
<script setup>
|
|
213
|
+
import { useCommandState } from 'vapor-chamber';
|
|
214
|
+
|
|
215
|
+
const { state: cart } = useCommandState(
|
|
216
|
+
{ items: [], total: 0 },
|
|
217
|
+
{
|
|
218
|
+
'cart.add': (state, cmd) => ({
|
|
219
|
+
items: [...state.items, cmd.target],
|
|
220
|
+
total: state.total + cmd.target.price
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
</script>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### useCommandHistory
|
|
228
|
+
|
|
229
|
+
```vue
|
|
230
|
+
<script setup>
|
|
231
|
+
import { useCommandHistory } from 'vapor-chamber';
|
|
232
|
+
|
|
233
|
+
const { canUndo, canRedo, undo, redo } = useCommandHistory({
|
|
234
|
+
filter: (cmd) => cmd.action.startsWith('editor.')
|
|
235
|
+
});
|
|
236
|
+
</script>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Examples
|
|
240
|
+
|
|
241
|
+
See the [`examples/`](./examples) folder for complete, runnable examples:
|
|
242
|
+
|
|
243
|
+
| Example | Description |
|
|
244
|
+
|---------|-------------|
|
|
245
|
+
| [`shopping-cart.ts`](./examples/shopping-cart.ts) | Cart with validation, history, and undo/redo |
|
|
246
|
+
| [`form-validation.ts`](./examples/form-validation.ts) | Form validation with error handling |
|
|
247
|
+
| [`async-api.ts`](./examples/async-api.ts) | Async handlers with retry plugin |
|
|
248
|
+
| [`realtime-search.ts`](./examples/realtime-search.ts) | Debounced search queries |
|
|
249
|
+
| [`custom-plugins.ts`](./examples/custom-plugins.ts) | Analytics, auth guard, rate limiter plugins |
|
|
250
|
+
| [`vue-vapor-component.vue`](./examples/vue-vapor-component.vue) | Full Vue Vapor todo app |
|
|
251
|
+
|
|
252
|
+
Run TypeScript examples with:
|
|
253
|
+
```bash
|
|
254
|
+
npx ts-node examples/shopping-cart.ts
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## API Reference
|
|
258
|
+
|
|
259
|
+
### Core
|
|
260
|
+
|
|
261
|
+
| Function | Description |
|
|
262
|
+
|----------|-------------|
|
|
263
|
+
| `createCommandBus()` | Create a synchronous command bus |
|
|
264
|
+
| `createAsyncCommandBus()` | Create an async command bus |
|
|
265
|
+
|
|
266
|
+
### Command Bus Methods
|
|
267
|
+
|
|
268
|
+
| Method | Description |
|
|
269
|
+
|--------|-------------|
|
|
270
|
+
| `dispatch(action, target, payload?)` | Execute a command |
|
|
271
|
+
| `register(action, handler)` | Register a handler (returns unregister fn) |
|
|
272
|
+
| `use(plugin)` | Add a plugin (returns unsubscribe fn) |
|
|
273
|
+
| `onAfter(hook)` | Run callback after every command |
|
|
274
|
+
|
|
275
|
+
### Composables
|
|
276
|
+
|
|
277
|
+
| Composable | Description |
|
|
278
|
+
|------------|-------------|
|
|
279
|
+
| `useCommand()` | Dispatch with reactive loading/error state |
|
|
280
|
+
| `useCommandState(initial, handlers)` | State managed by commands |
|
|
281
|
+
| `useCommandHistory(options?)` | Reactive undo/redo |
|
|
282
|
+
| `getCommandBus()` | Get shared bus instance |
|
|
283
|
+
| `setCommandBus(bus)` | Set shared bus instance |
|
|
284
|
+
|
|
285
|
+
## Documentation
|
|
286
|
+
|
|
287
|
+
See the [`docs/`](./docs) folder for detailed documentation:
|
|
288
|
+
|
|
289
|
+
- [Whitepaper](./docs/whitepaper.md) - Design philosophy and architecture
|
|
290
|
+
|
|
291
|
+
## Design Goals
|
|
292
|
+
|
|
293
|
+
1. **Minimal** - ~1KB core, no dependencies
|
|
294
|
+
2. **Vapor-native** - Built for signals, not VDOM
|
|
295
|
+
3. **Composable** - Plugins for everything
|
|
296
|
+
4. **Type-safe** - Full TypeScript support
|
|
297
|
+
5. **Predictable** - Sync by default, explicit async
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
[GNU Lesser General Public License v2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vapor-chamber - Vue Vapor integration
|
|
3
|
+
*
|
|
4
|
+
* Note: Vue Vapor is still in development. This integration uses
|
|
5
|
+
* the expected signal-based API. Update imports when Vapor stabilizes.
|
|
6
|
+
*/
|
|
7
|
+
import { type CommandBus, type Command, type CommandResult, type Handler, type Plugin } from './command-bus';
|
|
8
|
+
type Signal<T> = {
|
|
9
|
+
value: T;
|
|
10
|
+
};
|
|
11
|
+
export declare function getCommandBus(): CommandBus;
|
|
12
|
+
export declare function setCommandBus(bus: CommandBus): void;
|
|
13
|
+
/**
|
|
14
|
+
* useCommand - dispatch commands with reactive loading/error state
|
|
15
|
+
*/
|
|
16
|
+
export declare function useCommand(): {
|
|
17
|
+
dispatch: (action: string, target: any, payload?: any) => CommandResult;
|
|
18
|
+
loading: Signal<boolean>;
|
|
19
|
+
lastError: Signal<Error | null>;
|
|
20
|
+
register: (action: string, handler: Handler) => () => void;
|
|
21
|
+
use: (plugin: Plugin) => () => void;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* useCommandState - create reactive state that updates via commands
|
|
25
|
+
*/
|
|
26
|
+
export declare function useCommandState<T>(initial: T, handlers: {
|
|
27
|
+
[action: string]: (state: T, cmd: Command) => T;
|
|
28
|
+
}): {
|
|
29
|
+
state: Signal<T>;
|
|
30
|
+
dispose: () => void;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* useCommandHistory - undo/redo with reactive state
|
|
34
|
+
*/
|
|
35
|
+
export declare function useCommandHistory(options?: {
|
|
36
|
+
maxSize?: number;
|
|
37
|
+
filter?: (cmd: Command) => boolean;
|
|
38
|
+
}): {
|
|
39
|
+
past: Signal<Command[]>;
|
|
40
|
+
future: Signal<Command[]>;
|
|
41
|
+
canUndo: Signal<boolean>;
|
|
42
|
+
canRedo: Signal<boolean>;
|
|
43
|
+
undo: () => Command | undefined;
|
|
44
|
+
redo: () => Command | undefined;
|
|
45
|
+
clear: () => void;
|
|
46
|
+
dispose: () => void;
|
|
47
|
+
};
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=chamber.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/chamber.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vapor-chamber - Vue Vapor integration
|
|
3
|
+
*
|
|
4
|
+
* Note: Vue Vapor is still in development. This integration uses
|
|
5
|
+
* the expected signal-based API. Update imports when Vapor stabilizes.
|
|
6
|
+
*/
|
|
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
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const signal = getSignalFn();
|
|
30
|
+
/**
|
|
31
|
+
* Shared command bus instance
|
|
32
|
+
*/
|
|
33
|
+
let sharedBus = null;
|
|
34
|
+
export function getCommandBus() {
|
|
35
|
+
if (!sharedBus) {
|
|
36
|
+
sharedBus = createCommandBus();
|
|
37
|
+
}
|
|
38
|
+
return sharedBus;
|
|
39
|
+
}
|
|
40
|
+
export function setCommandBus(bus) {
|
|
41
|
+
sharedBus = bus;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* useCommand - dispatch commands with reactive loading/error state
|
|
45
|
+
*/
|
|
46
|
+
export function useCommand() {
|
|
47
|
+
const bus = getCommandBus();
|
|
48
|
+
const loading = signal(false);
|
|
49
|
+
const lastError = signal(null);
|
|
50
|
+
function dispatch(action, target, payload) {
|
|
51
|
+
loading.value = true;
|
|
52
|
+
lastError.value = null;
|
|
53
|
+
const result = bus.dispatch(action, target, payload);
|
|
54
|
+
loading.value = false;
|
|
55
|
+
if (!result.ok) {
|
|
56
|
+
lastError.value = result.error ?? null;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
dispatch,
|
|
62
|
+
loading,
|
|
63
|
+
lastError,
|
|
64
|
+
register: bus.register,
|
|
65
|
+
use: bus.use,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* useCommandState - create reactive state that updates via commands
|
|
70
|
+
*/
|
|
71
|
+
export function useCommandState(initial, handlers) {
|
|
72
|
+
const bus = getCommandBus();
|
|
73
|
+
const state = signal(initial);
|
|
74
|
+
// Register handlers that update state
|
|
75
|
+
const unregisters = [];
|
|
76
|
+
for (const [action, handler] of Object.entries(handlers)) {
|
|
77
|
+
const unregister = bus.register(action, (cmd) => {
|
|
78
|
+
state.value = handler(state.value, cmd);
|
|
79
|
+
return state.value;
|
|
80
|
+
});
|
|
81
|
+
unregisters.push(unregister);
|
|
82
|
+
}
|
|
83
|
+
// Cleanup function
|
|
84
|
+
const dispose = () => {
|
|
85
|
+
unregisters.forEach(fn => fn());
|
|
86
|
+
};
|
|
87
|
+
return { state, dispose };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* useCommandHistory - undo/redo with reactive state
|
|
91
|
+
*/
|
|
92
|
+
export function useCommandHistory(options = {}) {
|
|
93
|
+
const { maxSize = 50, filter } = options;
|
|
94
|
+
const bus = getCommandBus();
|
|
95
|
+
const past = signal([]);
|
|
96
|
+
const future = signal([]);
|
|
97
|
+
const canUndo = signal(false);
|
|
98
|
+
const canRedo = signal(false);
|
|
99
|
+
// Track commands via afterHook
|
|
100
|
+
const unsubscribe = bus.onAfter((cmd, result) => {
|
|
101
|
+
if (result.ok && (!filter || filter(cmd))) {
|
|
102
|
+
const newPast = [...past.value, cmd];
|
|
103
|
+
if (newPast.length > maxSize)
|
|
104
|
+
newPast.shift();
|
|
105
|
+
past.value = newPast;
|
|
106
|
+
future.value = [];
|
|
107
|
+
canUndo.value = true;
|
|
108
|
+
canRedo.value = false;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
function undo() {
|
|
112
|
+
const p = [...past.value];
|
|
113
|
+
const cmd = p.pop();
|
|
114
|
+
if (cmd) {
|
|
115
|
+
past.value = p;
|
|
116
|
+
future.value = [...future.value, cmd];
|
|
117
|
+
canUndo.value = p.length > 0;
|
|
118
|
+
canRedo.value = true;
|
|
119
|
+
}
|
|
120
|
+
return cmd;
|
|
121
|
+
}
|
|
122
|
+
function redo() {
|
|
123
|
+
const f = [...future.value];
|
|
124
|
+
const cmd = f.pop();
|
|
125
|
+
if (cmd) {
|
|
126
|
+
future.value = f;
|
|
127
|
+
past.value = [...past.value, cmd];
|
|
128
|
+
canUndo.value = true;
|
|
129
|
+
canRedo.value = f.length > 0;
|
|
130
|
+
}
|
|
131
|
+
return cmd;
|
|
132
|
+
}
|
|
133
|
+
function clear() {
|
|
134
|
+
past.value = [];
|
|
135
|
+
future.value = [];
|
|
136
|
+
canUndo.value = false;
|
|
137
|
+
canRedo.value = false;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
past,
|
|
141
|
+
future,
|
|
142
|
+
canUndo,
|
|
143
|
+
canRedo,
|
|
144
|
+
undo,
|
|
145
|
+
redo,
|
|
146
|
+
clear,
|
|
147
|
+
dispose: unsubscribe,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vapor-chamber - Command Bus for Vue Vapor
|
|
3
|
+
* ~1KB - Commands + Plugins + Hooks
|
|
4
|
+
*/
|
|
5
|
+
export type Command = {
|
|
6
|
+
action: string;
|
|
7
|
+
target: any;
|
|
8
|
+
payload?: any;
|
|
9
|
+
};
|
|
10
|
+
export type CommandResult = {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
value?: any;
|
|
13
|
+
error?: Error;
|
|
14
|
+
};
|
|
15
|
+
export type Handler = (cmd: Command) => any;
|
|
16
|
+
export type AsyncHandler = (cmd: Command) => any | Promise<any>;
|
|
17
|
+
export type Plugin = (cmd: Command, next: () => CommandResult) => CommandResult;
|
|
18
|
+
export type AsyncPlugin = (cmd: Command, next: () => CommandResult | Promise<CommandResult>) => CommandResult | Promise<CommandResult>;
|
|
19
|
+
export type Hook = (cmd: Command, result: CommandResult) => void;
|
|
20
|
+
export type AsyncHook = (cmd: Command, result: CommandResult) => void | Promise<void>;
|
|
21
|
+
export interface CommandBus {
|
|
22
|
+
dispatch: (action: string, target: any, payload?: any) => CommandResult;
|
|
23
|
+
register: (action: string, handler: Handler) => () => void;
|
|
24
|
+
use: (plugin: Plugin) => () => void;
|
|
25
|
+
onAfter: (hook: Hook) => () => void;
|
|
26
|
+
}
|
|
27
|
+
export interface AsyncCommandBus {
|
|
28
|
+
dispatch: (action: string, target: any, payload?: any) => Promise<CommandResult>;
|
|
29
|
+
register: (action: string, handler: AsyncHandler) => () => void;
|
|
30
|
+
use: (plugin: AsyncPlugin) => () => void;
|
|
31
|
+
onAfter: (hook: AsyncHook) => () => void;
|
|
32
|
+
}
|
|
33
|
+
export declare function createCommandBus(): CommandBus;
|
|
34
|
+
/**
|
|
35
|
+
* Async command bus - supports async handlers, plugins, and hooks
|
|
36
|
+
*/
|
|
37
|
+
export declare function createAsyncCommandBus(): AsyncCommandBus;
|
|
38
|
+
//# sourceMappingURL=command-bus.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vapor-chamber - Command Bus for Vue Vapor
|
|
3
|
+
* ~1KB - Commands + Plugins + Hooks
|
|
4
|
+
*/
|
|
5
|
+
export function createCommandBus() {
|
|
6
|
+
const handlers = new Map();
|
|
7
|
+
const plugins = [];
|
|
8
|
+
const afterHooks = [];
|
|
9
|
+
function dispatch(action, target, payload) {
|
|
10
|
+
const cmd = { action, target, payload };
|
|
11
|
+
const handler = handlers.get(action);
|
|
12
|
+
// Build execution chain: plugins wrap the handler
|
|
13
|
+
const execute = () => {
|
|
14
|
+
if (!handler) {
|
|
15
|
+
return { ok: false, error: new Error(`No handler: ${action}`) };
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const value = handler(cmd);
|
|
19
|
+
return { ok: true, value };
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
return { ok: false, error: e };
|
|
23
|
+
}
|
|
24
|
+
};
|
|
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
|
|
29
|
+
for (const hook of afterHooks) {
|
|
30
|
+
try {
|
|
31
|
+
hook(cmd, result);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.error('[vapor-chamber] Hook error:', e);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
function register(action, handler) {
|
|
40
|
+
handlers.set(action, handler);
|
|
41
|
+
return () => handlers.delete(action);
|
|
42
|
+
}
|
|
43
|
+
function use(plugin) {
|
|
44
|
+
plugins.push(plugin);
|
|
45
|
+
return () => {
|
|
46
|
+
const i = plugins.indexOf(plugin);
|
|
47
|
+
if (i !== -1)
|
|
48
|
+
plugins.splice(i, 1);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function onAfter(hook) {
|
|
52
|
+
afterHooks.push(hook);
|
|
53
|
+
return () => {
|
|
54
|
+
const i = afterHooks.indexOf(hook);
|
|
55
|
+
if (i !== -1)
|
|
56
|
+
afterHooks.splice(i, 1);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { dispatch, register, use, onAfter };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Async command bus - supports async handlers, plugins, and hooks
|
|
63
|
+
*/
|
|
64
|
+
export function createAsyncCommandBus() {
|
|
65
|
+
const handlers = new Map();
|
|
66
|
+
const plugins = [];
|
|
67
|
+
const afterHooks = [];
|
|
68
|
+
async function dispatch(action, target, payload) {
|
|
69
|
+
const cmd = { action, target, payload };
|
|
70
|
+
const handler = handlers.get(action);
|
|
71
|
+
// Build execution chain: plugins wrap the handler
|
|
72
|
+
const execute = async () => {
|
|
73
|
+
if (!handler) {
|
|
74
|
+
return { ok: false, error: new Error(`No handler: ${action}`) };
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const value = await handler(cmd);
|
|
78
|
+
return { ok: true, value };
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return { ok: false, error: e };
|
|
82
|
+
}
|
|
83
|
+
};
|
|
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
|
|
88
|
+
for (const hook of afterHooks) {
|
|
89
|
+
try {
|
|
90
|
+
await hook(cmd, result);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
console.error('[vapor-chamber] Hook error:', e);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
function register(action, handler) {
|
|
99
|
+
handlers.set(action, handler);
|
|
100
|
+
return () => handlers.delete(action);
|
|
101
|
+
}
|
|
102
|
+
function use(plugin) {
|
|
103
|
+
plugins.push(plugin);
|
|
104
|
+
return () => {
|
|
105
|
+
const i = plugins.indexOf(plugin);
|
|
106
|
+
if (i !== -1)
|
|
107
|
+
plugins.splice(i, 1);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function onAfter(hook) {
|
|
111
|
+
afterHooks.push(hook);
|
|
112
|
+
return () => {
|
|
113
|
+
const i = afterHooks.indexOf(hook);
|
|
114
|
+
if (i !== -1)
|
|
115
|
+
afterHooks.splice(i, 1);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return { dispatch, register, use, onAfter };
|
|
119
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vapor-chamber - Lightweight command bus for Vue Vapor
|
|
3
|
+
*
|
|
4
|
+
* A ~1KB command bus with plugins, hooks, and Vapor-native reactivity.
|
|
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';
|
|
7
|
+
export { logger, validator, history, debounce, throttle, type HistoryState, } from './plugins';
|
|
8
|
+
export { getCommandBus, setCommandBus, useCommand, useCommandState, useCommandHistory, } from './chamber';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vapor-chamber - Lightweight command bus for Vue Vapor
|
|
3
|
+
*
|
|
4
|
+
* A ~1KB command bus with plugins, hooks, and Vapor-native reactivity.
|
|
5
|
+
*/
|
|
6
|
+
// Core
|
|
7
|
+
export { createCommandBus, createAsyncCommandBus, } from './command-bus';
|
|
8
|
+
// Plugins
|
|
9
|
+
export { logger, validator, history, debounce, throttle, } from './plugins';
|
|
10
|
+
// Vapor integration
|
|
11
|
+
export { getCommandBus, setCommandBus, useCommand, useCommandState, useCommandHistory, } from './chamber';
|