@yoltra/core 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 +21 -0
- package/README.es.md +473 -0
- package/README.md +468 -0
- package/dist/types/eventBus/EventBus.d.ts +127 -0
- package/dist/types/eventBus/LooseEventBus.d.ts +219 -0
- package/dist/types/eventBus/index.d.ts +2 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/reducer/Reducer.d.ts +81 -0
- package/dist/types/store/Store.d.ts +834 -0
- package/dist/types/types.d.ts +939 -0
- package/dist/types/utils/detectChangedProps.d.ts +67 -0
- package/dist/types/utils/immutability.d.ts +47 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/yoltra.cjs.js +8 -0
- package/dist/yoltra.esm.js +1557 -0
- package/dist/yoltra.umd.js +8 -0
- package/package.json +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# @yoltra/core
|
|
4
|
+
|
|
5
|
+
> [ π²π½ VersiΓ³n en EspaΓ±ol](https://github.com/yoltra/yoltra/blob/main/packages/core/README.es.md)
|
|
6
|
+
> | π πΊπΈ English Version
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
**Framework-agnostic event-driven state container with fine-grained path subscriptions.**
|
|
12
|
+
|
|
13
|
+
`@yoltra/core` is the foundation of
|
|
14
|
+
[yoltra](https://github.com/yoltra/yoltra/blob/main/README.md). It provides the store, event
|
|
15
|
+
pipeline, middleware, effects, and the `connect()` subscription system. Zero framework
|
|
16
|
+
dependencies.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @yoltra/core
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## The Event Pipeline
|
|
29
|
+
|
|
30
|
+
Every `emit()` call flows through a deterministic pipeline:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
emit(channel, type, payload)
|
|
34
|
+
β
|
|
35
|
+
ββ 1. Dedup βββ Skip if identical fingerprint within time window
|
|
36
|
+
β
|
|
37
|
+
ββ 2. Middleware βββ Pre-reducer hooks (can reject β "uncommitted" event)
|
|
38
|
+
β
|
|
39
|
+
ββ 3. Reducers βββ Synchronous state updates, fine-grained path change detection
|
|
40
|
+
β
|
|
41
|
+
ββ 4. Event subscribers βββ Committed/uncommitted event notifications
|
|
42
|
+
β
|
|
43
|
+
ββ 5. Effects βββ Async side-effects (post-reducer, keyed for O(1) lookup)
|
|
44
|
+
β
|
|
45
|
+
ββ 6. Coarse subscribers βββ External store listeners (useSyncExternalStore, etc.)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Every stage is hook-able. Middleware can cancel events, creating "uncommitted" events that the
|
|
49
|
+
UI can still react to. Effects run after reducers and see the final state.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Core Concepts
|
|
54
|
+
|
|
55
|
+
### Channel-based events
|
|
56
|
+
|
|
57
|
+
Events are `(channel, type, payload)` tuples. Channels provide natural namespacing that scales
|
|
58
|
+
in large codebases:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
await store.emit("auth", "login", credentials);
|
|
62
|
+
await store.emit("analytics", "track", { event: "page_view" });
|
|
63
|
+
await store.emit("ui", "toast", { message: "Saved!" });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Fine-grained subscriptions via `connect()`
|
|
67
|
+
|
|
68
|
+
Subscribe to exact state paths using dotted notation. Supports `*` (one segment) and `**` (zero
|
|
69
|
+
or more segments) wildcards:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Exact path β fires when items[0].title changes
|
|
73
|
+
store.connect({ reducer: "todos", property: "items.0.title" }, (change) =>
|
|
74
|
+
console.log("title:", change.oldValue, "β", change.newValue),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Single-segment wildcard β fires when ANY item's title changes
|
|
78
|
+
store.connect({ reducer: "todos", property: "items.*.title" }, (change) =>
|
|
79
|
+
console.log("some title changed at", change.path),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Deep wildcard β fires when anything under items changes
|
|
83
|
+
store.connect({ reducer: "todos", property: "items.**" }, (change) =>
|
|
84
|
+
console.log("items tree changed at", change.path),
|
|
85
|
+
);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Immutability
|
|
89
|
+
|
|
90
|
+
State is deep-frozen before committing. Mutations throw in strict mode:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const state = store.getState();
|
|
94
|
+
state.counter.value = 999; // TypeError: Cannot assign to read-only property
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Event Targeting with `When` Matchers
|
|
100
|
+
|
|
101
|
+
Reducers, effects, and middleware use a unified `When` matcher to declare which events they
|
|
102
|
+
respond to:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { createStore, eventKeys } from "@yoltra/core";
|
|
106
|
+
|
|
107
|
+
type AppEM = {
|
|
108
|
+
ui: { increment: number; decrement: number; reset: void };
|
|
109
|
+
admin: { setCounter: number };
|
|
110
|
+
system: { init: void; shutdown: void };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Match specific event keys (recommended β preserves type correlation)
|
|
114
|
+
const counterReducer = {
|
|
115
|
+
state: { value: 0 },
|
|
116
|
+
when: {
|
|
117
|
+
keys: eventKeys<AppEM>()([
|
|
118
|
+
["ui", "increment"],
|
|
119
|
+
["ui", "decrement"],
|
|
120
|
+
]),
|
|
121
|
+
},
|
|
122
|
+
reducer: (state, event) => {
|
|
123
|
+
if (event.type === "increment") return { value: state.value + event.payload };
|
|
124
|
+
if (event.type === "decrement") return { value: state.value - event.payload };
|
|
125
|
+
return state;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Match all events in a channel
|
|
130
|
+
const uiLogger = {
|
|
131
|
+
when: { channel: "ui" },
|
|
132
|
+
effect: (event) => console.log("UI event:", event.type),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Match events across multiple channels
|
|
136
|
+
const auditTrail = {
|
|
137
|
+
when: { channels: ["ui", "admin"] },
|
|
138
|
+
effect: (event) => logToAuditTrail(event),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Match ALL events
|
|
142
|
+
const globalLogger = {
|
|
143
|
+
when: { any: true },
|
|
144
|
+
middleware: (state, event) => {
|
|
145
|
+
console.log(`[${event.channel}] ${event.type}`);
|
|
146
|
+
return true;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Middleware
|
|
154
|
+
|
|
155
|
+
Middleware runs **before** reducers and can cancel event propagation. Supports both raw
|
|
156
|
+
functions (legacy) and `MiddlewareSpec` objects with targeting:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import type { MiddlewareSpec } from "@yoltra/core";
|
|
160
|
+
|
|
161
|
+
// Targeted middleware β only runs for admin channel events
|
|
162
|
+
const adminGuard: MiddlewareSpec<AppState, AppEM> = {
|
|
163
|
+
when: { channel: "admin" },
|
|
164
|
+
middleware: (state, event) => {
|
|
165
|
+
if (!state.auth.isAdmin) return false; // Reject β creates "uncommitted" event
|
|
166
|
+
return true;
|
|
167
|
+
},
|
|
168
|
+
meta: { type: "middleware", name: "adminGuard" },
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Global middleware β runs for all events
|
|
172
|
+
const logger = async (state, event, emit) => {
|
|
173
|
+
console.log("Event:", event.channel, event.type);
|
|
174
|
+
return true;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const store = createStore({
|
|
178
|
+
name: "App",
|
|
179
|
+
reducer: {
|
|
180
|
+
/* ... */
|
|
181
|
+
},
|
|
182
|
+
middleware: [adminGuard, logger],
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Dynamic middleware
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
const off = store.registerMiddleware(async (state, event) => {
|
|
190
|
+
return event.type !== "forbidden";
|
|
191
|
+
});
|
|
192
|
+
off(); // Remove later
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Effects
|
|
198
|
+
|
|
199
|
+
Effects run **after** reducers and see the final state. They are keyed by event for O(1) lookup:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
// Via store spec
|
|
203
|
+
const store = createStore({
|
|
204
|
+
name: "App",
|
|
205
|
+
reducer: {
|
|
206
|
+
/* ... */
|
|
207
|
+
},
|
|
208
|
+
effects: [
|
|
209
|
+
{
|
|
210
|
+
when: {
|
|
211
|
+
keys: eventKeys<AppEM>()([
|
|
212
|
+
["todos", "add"],
|
|
213
|
+
["todos", "delete"],
|
|
214
|
+
]),
|
|
215
|
+
},
|
|
216
|
+
effect: async (event, getState, emit) => {
|
|
217
|
+
await saveToServer(getState());
|
|
218
|
+
},
|
|
219
|
+
meta: { type: "effect", name: "syncToServer" },
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Dynamic registration
|
|
225
|
+
const off = store.registerEffect({
|
|
226
|
+
when: { channel: "analytics" },
|
|
227
|
+
effect: async (event) => sendToAnalytics(event),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Convenience helper for single event
|
|
231
|
+
const off2 = store.onEffect("ui", "save", async (payload, getState, emit) => {
|
|
232
|
+
await saveToCloud(payload);
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Event Subscriptions
|
|
239
|
+
|
|
240
|
+
Subscribe to events (not state) from the view layer. Useful for notifications, animations, and
|
|
241
|
+
responding to rejected events:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// Committed events (default) β events that passed middleware
|
|
245
|
+
const off = store.onEvent("ui", "save", (event, getState, emit, phase) => {
|
|
246
|
+
console.log("Save committed:", event.payload);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Uncommitted events β events rejected by middleware
|
|
250
|
+
store.onEvent(
|
|
251
|
+
"ui",
|
|
252
|
+
"delete",
|
|
253
|
+
(event, getState, emit, phase) => {
|
|
254
|
+
console.log("Delete was rejected");
|
|
255
|
+
},
|
|
256
|
+
"uncommitted",
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// All events β both committed and uncommitted
|
|
260
|
+
store.onEvent(
|
|
261
|
+
"ui",
|
|
262
|
+
"action",
|
|
263
|
+
(event, getState, emit, phase) => {
|
|
264
|
+
console.log(`Action ${phase}:`, event.type);
|
|
265
|
+
},
|
|
266
|
+
"all",
|
|
267
|
+
);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Event Deduplication
|
|
273
|
+
|
|
274
|
+
Yoltra automatically deduplicates identical events within a configurable time window. This
|
|
275
|
+
prevents double-processing in React Strict Mode:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const store = createStore({
|
|
279
|
+
name: "Yoltra_Rocks",
|
|
280
|
+
reducer: {
|
|
281
|
+
/* ... */
|
|
282
|
+
},
|
|
283
|
+
dedupWindowMs: 100, // default: 50ms dev, 100ms prod
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Dynamic Reducers
|
|
290
|
+
|
|
291
|
+
Add or remove reducer slices at runtime:
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
const dispose = store.registerReducer("filters", {
|
|
295
|
+
state: { q: "" },
|
|
296
|
+
when: { keys: eventKeys<AppEM>()([["ui", "setQuery"]]) },
|
|
297
|
+
reducer: (state, event) => (event.type === "setQuery" ? { q: event.payload } : state),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Later: remove the slice and its state
|
|
301
|
+
dispose();
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Hot Module Replacement
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
if (import.meta.hot) {
|
|
310
|
+
import.meta.hot.accept("./reducers", (mod) => {
|
|
311
|
+
store.replaceReducers(mod.reducers, { preserveState: true });
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
import.meta.hot.accept("./middleware", (mod) => {
|
|
315
|
+
store.replaceMiddleware(mod.middleware);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
import.meta.hot.accept("./effects", (mod) => {
|
|
319
|
+
store.replaceEffects(mod.effects);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Or replace everything at once
|
|
323
|
+
store.hotReplace({
|
|
324
|
+
reducer: newReducers,
|
|
325
|
+
middleware: newMiddleware,
|
|
326
|
+
effects: newEffects,
|
|
327
|
+
preserveState: true,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Best Practices
|
|
335
|
+
|
|
336
|
+
### Always await `emit()`
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
await emit("todo", "add", todo);
|
|
340
|
+
const state = store.getState(); // Guaranteed to reflect the new todo
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Keep reducers fast
|
|
344
|
+
|
|
345
|
+
Reducers are synchronous and block the event queue. Move expensive work to effects:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// Reducer: just set a loading flag
|
|
349
|
+
reducer: ((state, event) => ({ ...state, loading: true }),
|
|
350
|
+
// Effect: do the heavy lifting
|
|
351
|
+
store.onEffect("data", "compute", async (payload, getState, emit) => {
|
|
352
|
+
const result = await computeAsync();
|
|
353
|
+
await emit("data", "computeComplete", result);
|
|
354
|
+
}));
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Handle effect errors
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
store.registerEffect({
|
|
361
|
+
when: { channel: "data" },
|
|
362
|
+
effect: async (event, getState, emit) => {
|
|
363
|
+
try {
|
|
364
|
+
const data = await fetch(url);
|
|
365
|
+
await emit("data", "loadSuccess", data);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
await emit("data", "loadFailure", { error: error.message });
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## API Overview
|
|
376
|
+
|
|
377
|
+
### Store Creation
|
|
378
|
+
|
|
379
|
+
| API | Description |
|
|
380
|
+
| ----------------------------------------------- | ---------------------------------------------- |
|
|
381
|
+
| `createStore(spec)` | Create a store (types inferred from reducers) |
|
|
382
|
+
| `createStore<S, EM>(spec)` | Create a store with explicit state/event types |
|
|
383
|
+
| `store.emit(channel, type, payload)` | Emit an event (returns a promise) |
|
|
384
|
+
| `store.getState()` | Get current readonly state snapshot |
|
|
385
|
+
| `store.subscribe(listener)` | Coarse subscription (any state change) |
|
|
386
|
+
| `store.connect(spec, handler)` | Fine-grained path subscription with wildcards |
|
|
387
|
+
| `store.onEvent(channel, type, handler, phase?)` | Event subscription (committed/uncommitted/all) |
|
|
388
|
+
| `store.onEffect(channel, type, handler)` | Single-event effect shorthand |
|
|
389
|
+
| `store.dispose()` | Cleanup timers and resources |
|
|
390
|
+
|
|
391
|
+
### Dynamic Registration
|
|
392
|
+
|
|
393
|
+
| API | Description |
|
|
394
|
+
| ----------------------------------- | ------------------------- |
|
|
395
|
+
| `store.registerReducer(name, spec)` | Add a slice at runtime |
|
|
396
|
+
| `store.registerMiddleware(fn)` | Add middleware at runtime |
|
|
397
|
+
| `store.registerEffect(spec)` | Add an effect at runtime |
|
|
398
|
+
|
|
399
|
+
### HMR
|
|
400
|
+
|
|
401
|
+
| API | Description |
|
|
402
|
+
| --------------------------------------- | -------------------------- |
|
|
403
|
+
| `store.replaceReducers(reducers, opts)` | Replace all reducers |
|
|
404
|
+
| `store.replaceMiddleware(middleware)` | Replace all middleware |
|
|
405
|
+
| `store.replaceEffects(effects)` | Replace all effects |
|
|
406
|
+
| `store.hotReplace(partial)` | Replace any subset at once |
|
|
407
|
+
|
|
408
|
+
### Helpers
|
|
409
|
+
|
|
410
|
+
| API | Description |
|
|
411
|
+
| ------------------------ | --------------------------------------------- |
|
|
412
|
+
| `eventKeys<EM>()([...])` | Type-safe event key arrays without `as const` |
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Performance
|
|
417
|
+
|
|
418
|
+
| Metric | Value |
|
|
419
|
+
| ------------------ | ------------------------------ |
|
|
420
|
+
| **Bundle size** | ~8KB (minified + gzipped) |
|
|
421
|
+
| **Tree-shakeable** | Yes (ES modules) |
|
|
422
|
+
| **Dependencies** | Zero |
|
|
423
|
+
| **TypeScript** | Full type definitions included |
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Documentation
|
|
428
|
+
|
|
429
|
+
- **[yoltra Root README](https://github.com/yoltra/yoltra/blob/main/README.md)** β Overview and
|
|
430
|
+
quick start
|
|
431
|
+
- **[@yoltra/react](https://github.com/yoltra/yoltra/blob/main/packages/react/README.md)** β
|
|
432
|
+
React hooks and Suspense
|
|
433
|
+
- **[Quick Start Guide](https://github.com/yoltra/yoltra/blob/main/docs/en/QUICK_START_GUIDE.md)**
|
|
434
|
+
β Five steps to a working app
|
|
435
|
+
- **[Event Queue Architecture](https://github.com/yoltra/yoltra/blob/main/docs/en/design/event-queue-architecture.md)**
|
|
436
|
+
β Technical deep-dive
|
|
437
|
+
- **[Library Comparison](https://github.com/yoltra/yoltra/blob/main/docs/en/design/state-management-library-comparison.md)**
|
|
438
|
+
β Architectural comparison
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Examples
|
|
443
|
+
|
|
444
|
+
- **[Todo App](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-react)** β Full
|
|
445
|
+
CRUD with performance profiling
|
|
446
|
+
- **[Kinetic Logo](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-kinetic-logo)**
|
|
447
|
+
β 3000 circles with physics simulation
|
|
448
|
+
- **[Next.js Integration](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-nextjs)**
|
|
449
|
+
β SSR + App Router + theme switcher
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Contributing
|
|
454
|
+
|
|
455
|
+
- [Monorepo Root](https://github.com/yoltra/yoltra/blob/main/README.md)
|
|
456
|
+
- [Contributing Guide](https://github.com/yoltra/yoltra/blob/main/CONTRIBUTING.md)
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Status
|
|
461
|
+
|
|
462
|
+
**Release Candidate** β APIs are stable, used in production, minor changes possible before v1.0.
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## License
|
|
467
|
+
|
|
468
|
+
**MIT** β Free to use in commercial and open-source projects.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { EventMapBase } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal, synchronous pub/sub event bus keyed by **channel** and **type**.
|
|
4
|
+
*
|
|
5
|
+
* @typeParam EM - Event map shape:
|
|
6
|
+
* ```ts
|
|
7
|
+
* type EventMapBase = Record<string, Record<string, unknown>>;
|
|
8
|
+
* // Example:
|
|
9
|
+
* type EM = {
|
|
10
|
+
* ui: { toggle: boolean };
|
|
11
|
+
* data: { loaded: { items: string[] } };
|
|
12
|
+
* };
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* - Handlers are stored per `(channel, type)` and invoked **synchronously** in subscription order.
|
|
17
|
+
* - Exceptions thrown by a handler are **caught and logged**, and do **not** stop other handlers.
|
|
18
|
+
* - Intended for in-memory, single-process usage (no cross-tab/process broadcasting).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* type EM = {
|
|
23
|
+
* ui: { toggle: boolean };
|
|
24
|
+
* data: { loaded: { items: string[] } };
|
|
25
|
+
* };
|
|
26
|
+
*
|
|
27
|
+
* const bus = new EventBus<EM>();
|
|
28
|
+
*
|
|
29
|
+
* // Subscribe
|
|
30
|
+
* const off = bus.on('ui', 'toggle', (on) => {
|
|
31
|
+
* console.log('UI toggled:', on);
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Emit
|
|
35
|
+
* bus.emit('ui', 'toggle', true); // logs: "UI toggled: true"
|
|
36
|
+
*
|
|
37
|
+
* // Unsubscribe
|
|
38
|
+
* off();
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export declare class EventBus<EM extends EventMapBase> {
|
|
44
|
+
/**
|
|
45
|
+
* Internal registry: `channel β type β Set<handler>`.
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
private handlers;
|
|
49
|
+
/**
|
|
50
|
+
* Subscribes a handler to an exact `(channel, type)`.
|
|
51
|
+
*
|
|
52
|
+
* @typeParam C - Channel key (must be a string key of `EM`).
|
|
53
|
+
* @typeParam T - Type key within channel `C` (must be a string key of `EM[C]`).
|
|
54
|
+
* @param channel - Channel name to subscribe to.
|
|
55
|
+
* @param type - Event type within the channel.
|
|
56
|
+
* @param handler - Function invoked with the payload type `EM[C][T]`.
|
|
57
|
+
* @returns An **unsubscribe** function that removes this handler.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const off = bus.on('data', 'loaded', ({ items }) => {
|
|
62
|
+
* console.log('Loaded', items.length, 'items');
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* // Later, stop listening:
|
|
66
|
+
* off();
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
on<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, handler: (payload: EM[C][T]) => void): () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Removes a specific handler previously added with {@link EventBus.on | `on`}.
|
|
74
|
+
*
|
|
75
|
+
* @typeParam C - Channel key (string key of `EM`).
|
|
76
|
+
* @typeParam T - Type key within channel `C` (string key of `EM[C]`).
|
|
77
|
+
* @param channel - Channel name of the subscription to remove.
|
|
78
|
+
* @param type - Event type of the subscription to remove.
|
|
79
|
+
* @param handler - The same handler reference that was passed to `on`.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* const h = (n: number) => console.log('inc', n);
|
|
84
|
+
* bus.on('math', 'inc', h);
|
|
85
|
+
*
|
|
86
|
+
* // Explicitly remove this handler:
|
|
87
|
+
* bus.off('math', 'inc', h);
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @public
|
|
91
|
+
*/
|
|
92
|
+
off<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, handler: (payload: EM[C][T]) => void): void;
|
|
93
|
+
/**
|
|
94
|
+
* Emits an event to all subscribers of the exact `(channel, type)`.
|
|
95
|
+
*
|
|
96
|
+
* Handlers are invoked **synchronously**. Any exception thrown by a handler is
|
|
97
|
+
* caught and logged, and other handlers still run.
|
|
98
|
+
*
|
|
99
|
+
* @typeParam C - Channel key (string key of `EM`).
|
|
100
|
+
* @typeParam T - Type key within channel `C` (string key of `EM[C]`).
|
|
101
|
+
* @param channel - Channel name to emit on.
|
|
102
|
+
* @param type - Event type to emit.
|
|
103
|
+
* @param payload - Payload matching `EM[C][T]`.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* bus.emit('ui', 'toggle', false);
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @public
|
|
111
|
+
*/
|
|
112
|
+
emit<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, payload: EM[C][T]): void;
|
|
113
|
+
/**
|
|
114
|
+
* Clears **all** listeners across all channels/types.
|
|
115
|
+
*
|
|
116
|
+
* Useful for tests or during HMR teardown to avoid duplicate handlers.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* // In a test teardown:
|
|
121
|
+
* afterEach(() => bus.clear());
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @public
|
|
125
|
+
*/
|
|
126
|
+
clear(): void;
|
|
127
|
+
}
|