bubus 1.8.1 → 2.2.1

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.
Files changed (67) hide show
  1. package/README.md +510 -75
  2. package/dist/esm/async_context.js +39 -0
  3. package/dist/esm/async_context.js.map +7 -0
  4. package/dist/esm/base_event.js +825 -0
  5. package/dist/esm/base_event.js.map +7 -0
  6. package/dist/esm/bridge_jsonl.js +150 -0
  7. package/dist/esm/bridge_jsonl.js.map +7 -0
  8. package/dist/esm/bridge_nats.js +88 -0
  9. package/dist/esm/bridge_nats.js.map +7 -0
  10. package/dist/esm/bridge_postgres.js +231 -0
  11. package/dist/esm/bridge_postgres.js.map +7 -0
  12. package/dist/esm/bridge_redis.js +155 -0
  13. package/dist/esm/bridge_redis.js.map +7 -0
  14. package/dist/esm/bridge_sqlite.js +235 -0
  15. package/dist/esm/bridge_sqlite.js.map +7 -0
  16. package/dist/esm/bridges.js +306 -0
  17. package/dist/esm/bridges.js.map +7 -0
  18. package/dist/esm/event_bus.js +1046 -0
  19. package/dist/esm/event_bus.js.map +7 -0
  20. package/dist/esm/event_handler.js +279 -0
  21. package/dist/esm/event_handler.js.map +7 -0
  22. package/dist/esm/event_history.js +172 -0
  23. package/dist/esm/event_history.js.map +7 -0
  24. package/dist/esm/event_result.js +426 -0
  25. package/dist/esm/event_result.js.map +7 -0
  26. package/dist/esm/events_suck.js +39 -0
  27. package/dist/esm/events_suck.js.map +7 -0
  28. package/dist/esm/helpers.js +64 -0
  29. package/dist/esm/helpers.js.map +7 -0
  30. package/dist/esm/index.js +32 -16559
  31. package/dist/esm/index.js.map +4 -4
  32. package/dist/esm/lock_manager.js +323 -0
  33. package/dist/esm/lock_manager.js.map +7 -0
  34. package/dist/esm/logging.js +196 -0
  35. package/dist/esm/logging.js.map +7 -0
  36. package/dist/esm/middlewares.js +1 -0
  37. package/dist/esm/middlewares.js.map +7 -0
  38. package/dist/esm/optional_deps.js +34 -0
  39. package/dist/esm/optional_deps.js.map +7 -0
  40. package/dist/esm/retry.js +237 -0
  41. package/dist/esm/retry.js.map +7 -0
  42. package/dist/esm/timing.js +56 -0
  43. package/dist/esm/timing.js.map +7 -0
  44. package/dist/esm/types.js +84 -0
  45. package/dist/esm/types.js.map +7 -0
  46. package/dist/types/async_context.d.ts +1 -1
  47. package/dist/types/base_event.d.ts +96 -79
  48. package/dist/types/bridge_jsonl.d.ts +26 -0
  49. package/dist/types/bridge_nats.d.ts +20 -0
  50. package/dist/types/bridge_postgres.d.ts +31 -0
  51. package/dist/types/bridge_redis.d.ts +34 -0
  52. package/dist/types/bridge_sqlite.d.ts +30 -0
  53. package/dist/types/bridges.d.ts +49 -0
  54. package/dist/types/event_bus.d.ts +88 -41
  55. package/dist/types/event_handler.d.ts +47 -18
  56. package/dist/types/event_history.d.ts +45 -0
  57. package/dist/types/event_result.d.ts +37 -33
  58. package/dist/types/events_suck.d.ts +40 -0
  59. package/dist/types/helpers.d.ts +1 -0
  60. package/dist/types/index.d.ts +10 -1
  61. package/dist/types/lock_manager.d.ts +27 -18
  62. package/dist/types/logging.d.ts +4 -1
  63. package/dist/types/middlewares.d.ts +13 -0
  64. package/dist/types/optional_deps.d.ts +3 -0
  65. package/dist/types/timing.d.ts +3 -0
  66. package/dist/types/types.d.ts +18 -7
  67. package/package.json +25 -11
package/README.md CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  <img width="200" alt="image" src="https://github.com/user-attachments/assets/b3525c24-51ba-496c-b327-ccdfe46a7362" align="right" />
4
4
 
5
- [![DeepWiki: Python](https://img.shields.io/badge/DeepWiki-bbus%2FPython-yellow.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/pirate/bbus) ![PyPI - Version](https://img.shields.io/pypi/v/bubus) ![GitHub License](https://img.shields.io/github/license/pirate/bbus) ![GitHub last commit](https://img.shields.io/github/last-commit/pirate/bbus)
5
+ [![DeepWiki: Python](https://img.shields.io/badge/DeepWiki-bbus%2FPython-yellow.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/pirate/bbus) [![PyPI - Version](https://img.shields.io/pypi/v/bubus)](https://pypi.org/project/bubus/) [![GitHub License](https://img.shields.io/github/license/pirate/bbus)](https://github.com/pirate/bbus) ![GitHub last commit](https://img.shields.io/github/last-commit/pirate/bbus)
6
6
 
7
- [![DeepWiki: TS](https://img.shields.io/badge/DeepWiki-bbus%2FTypescript-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/pirate/bbus/3-typescript-implementation) ![NPM Version](https://img.shields.io/npm/v/bubus)
7
+ [![DeepWiki: TS](https://img.shields.io/badge/DeepWiki-bbus%2FTypescript-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/pirate/bbus/3-typescript-implementation) [![NPM Version](https://img.shields.io/npm/v/bubus)](https://www.npmjs.com/package/bubus)
8
8
 
9
- Bubus is an in-memory event bus library for async Python and TS (node/browser).
9
+ Bubus is an in-memory event bus library for async Python and TS (node/bun/deno/browser).
10
10
 
11
11
  It's designed for quickly building resilient, predictable, complex event-driven apps.
12
12
 
13
- It "just works" with an intuitive, but powerful event JSON format + dispatch API that's consistent across both languages and scales consistently from one event up to millions:
13
+ It "just works" with an intuitive, but powerful event JSON format + emit API that's consistent across both languages and scales consistently from one event to millions (~0.2ms/event):
14
14
 
15
15
  ```python
16
16
  bus.on(SomeEvent, some_function)
@@ -21,7 +21,7 @@ It's async native, has proper automatic nested event tracking, and powerful conc
21
21
 
22
22
  - nice Zod / Pydantic schemas for events that can be exchanged between both languages
23
23
  - automatic UUIDv7s and monotonic nanosecond timestamps for ordering events globally
24
- - built in locking options to force strict global FIFO procesing or fully parallel processing
24
+ - built in locking options to force strict global FIFO processing or fully parallel processing
25
25
 
26
26
  ---
27
27
 
@@ -37,7 +37,7 @@ It's async native, has proper automatic nested event tracking, and powerful conc
37
37
  ## 🔢 Quickstart
38
38
 
39
39
  ```bash
40
- pnpm add bubus
40
+ npm install bubus
41
41
  ```
42
42
 
43
43
  ```ts
@@ -46,7 +46,7 @@ import { z } from 'zod'
46
46
 
47
47
  const CreateUserEvent = BaseEvent.extend('CreateUserEvent', {
48
48
  email: z.string(),
49
- event_result_schema: z.object({ user_id: z.string() }),
49
+ event_result_type: z.object({ user_id: z.string() }),
50
50
  })
51
51
 
52
52
  const bus = new EventBus('MyAuthEventBus')
@@ -58,7 +58,7 @@ bus.on(CreateUserEvent, async (event) => {
58
58
 
59
59
  const event = bus.emit(CreateUserEvent({ email: 'someuser@example.com' }))
60
60
  await event.done()
61
- console.log(event.first_result) // { user_id: 'some-user-uuid' }
61
+ console.log(event.event_result) // { user_id: 'some-user-uuid' }
62
62
  ```
63
63
 
64
64
  <br/>
@@ -69,6 +69,9 @@ console.log(event.first_result) // { user_id: 'some-user-uuid' }
69
69
 
70
70
  ## ✨ Features
71
71
 
72
+ <details>
73
+ <summary><strong>See the core TypeScript features and how they map to Python.</strong></summary>
74
+
72
75
  The features offered in TS are broadly similar to the ones offered in the python library.
73
76
 
74
77
  - Typed events with Zod schemas (cross-compatible with Pydantic events from python library)
@@ -81,6 +84,8 @@ The features offered in TS are broadly similar to the ones offered in the python
81
84
 
82
85
  See the [Python README](../README.md) for more details.
83
86
 
87
+ </details>
88
+
84
89
  <br/>
85
90
 
86
91
  ---
@@ -89,39 +94,248 @@ See the [Python README](../README.md) for more details.
89
94
 
90
95
  ## 📚 API Documentation
91
96
 
92
- ### `EventBus`
97
+ <details>
98
+ <summary><strong>Review bus construction, defaults, and core lifecycle methods.</strong></summary>
99
+
100
+ The main bus class that registers handlers, schedules events, and tracks results.
93
101
 
94
- Create a bus:
102
+ Constructor:
95
103
 
96
104
  ```ts
97
- const bus = new EventBus('MyBus', {
98
- max_history_size: 100, // keep small, copy events to external store manually if you want to persist/query long-term logs
99
- event_concurrency: 'bus-serial', // 'global-serial' | 'bus-serial' (default) | 'parallel'
100
- event_handler_concurrency: 'serial', // 'serial' (default) | 'parallel'
101
- event_handler_completion: 'all', // 'all' (default) | 'first' (stop handlers after the first non-undefined result from any handler)
102
- event_timeout: 60, // default hard timeout for event handlers before they are marked result.status = 'error' w/ result.error = HandlerTimeoutError(...)
103
- event_handler_slow_timeout: 30, // default timeout before a console.warn("Slow event handler bus.on(SomeEvent, someHandler()) has taken more than 30s"
104
- event_slow_timeout: 300, // default timeout before a console.warn("Slow event processing: bus.on(SomeEvent, ...4 handlers) have taken more than 300s"
105
+ new EventBus(name?: string, options?: {
106
+ id?: string
107
+ max_history_size?: number | null
108
+ event_concurrency?: 'global-serial' | 'bus-serial' | 'parallel' | null
109
+ event_timeout?: number | null
110
+ event_slow_timeout?: number | null
111
+ event_handler_concurrency?: 'serial' | 'parallel' | null
112
+ event_handler_completion?: 'all' | 'first'
113
+ event_handler_slow_timeout?: number | null
114
+ event_handler_detect_file_paths?: boolean
105
115
  })
106
116
  ```
107
117
 
108
- Core methods:
118
+ #### Constructor options
119
+
120
+ | Option | Type | Default | Purpose |
121
+ | --------------------------------- | ------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
+ | `id` | `string` | `uuidv7()` | Override bus UUID (mostly for serialization/tests). |
123
+ | `max_history_size` | `number \| null` | `100` | Max events kept in `event_history`; `null` = unbounded; `0` = keep only in-flight events and drop completed events immediately. |
124
+ | `max_history_drop` | `boolean` | `false` | If `true`, when history is full drop oldest history entries (including uncompleted if needed). If `false`, reject new emits when history reaches `max_history_size`. |
125
+ | `event_concurrency` | `'global-serial' \| 'bus-serial' \| 'parallel' \| null` | `'bus-serial'` | Event-level scheduling policy. |
126
+ | `event_handler_concurrency` | `'serial' \| 'parallel' \| null` | `'serial'` | Per-event handler scheduling policy. |
127
+ | `event_handler_completion` | `'all' \| 'first'` | `'all'` | Event completion mode if event does not override it. |
128
+ | `event_timeout` | `number \| null` | `60` | Default per-handler timeout budget in seconds (unless overridden). |
129
+ | `event_handler_slow_timeout` | `number \| null` | `30` | Slow handler warning threshold (seconds). |
130
+ | `event_slow_timeout` | `number \| null` | `300` | Slow event warning threshold (seconds). |
131
+ | `event_handler_detect_file_paths` | `boolean` | `true` | Capture source file:line for handlers (slower, better logs). |
132
+
133
+ #### Runtime state properties
134
+
135
+ - `id: string`
136
+ - `name: string`
137
+ - `label: string` (`${name}#${id.slice(-4)}`)
138
+ - `handlers: Map<string, EventHandler>`
139
+ - `handlers_by_key: Map<string, string[]>`
140
+ - `event_history: Map<string, BaseEvent>`
141
+ - `pending_event_queue: BaseEvent[]`
142
+ - `in_flight_event_ids: Set<string>`
143
+ - `locks: LockManager`
144
+
145
+ #### `on()`
109
146
 
110
- - `bus.emit(event)` aka `bus.dispatch(event)`
111
- - `bus.on(eventKey, handler, options?)`
112
- - `bus.off(eventKey, handler)`
113
- - `bus.find(eventKey, options?)`
114
- - `bus.waitUntilIdle()`
115
- - `bus.destroy()`
147
+ ```ts
148
+ on<T extends BaseEvent>(
149
+ event_pattern: string | '*' | EventClass<T>,
150
+ handler: EventHandlerCallable<T>,
151
+ options?: Partial<EventHandler>
152
+ ): EventHandler
153
+ ```
154
+
155
+ Use during startup/composition to register handlers.
156
+
157
+ Advanced `options` fields, these can be used to override defaults per-handler if needed:
158
+
159
+ - `handler_timeout?: number | null` hard delay before handler execution is aborted with a `HandlerTimeoutError`
160
+ - `handler_slow_timeout?: number | null` delay before emitting a slow handler warning log line
161
+ - `handler_name?: string` optional name to use instead of `anonymous` if handler is an unnamed arrow function
162
+ - `handler_file_path?: string | null` optional path/to/source/file.js:lineno where the handler is defined, used for logging only
163
+ - `id?: string` unique UUID for the handler (normally a hash of bus_id + event_pattern + handler_name + handler_registered_at)
116
164
 
117
165
  Notes:
118
166
 
119
- - String matching of event types using `bus.on('SomeEvent', ...)` and `bus.on('*', ...)` wildcard matching is supported
120
- - Prefer passing event class to (`bus.on(MyEvent, handler)`) over string-based maching for strictest type inference
167
+ - Prefer class/factory keys (`bus.on(MyEvent, handler)`) for typed payload/result inference.
168
+ - String and `'*'` matching are supported (`bus.on('MyEvent', ...)`, `bus.on('*', ...)`).
169
+ - Returns an `EventHandler` object you can later pass to `off()` to de-register the handler if needed.
170
+
171
+ #### `off()`
172
+
173
+ ```ts
174
+ off<T extends BaseEvent>(
175
+ event_pattern: EventPattern<T> | '*',
176
+ handler?: EventHandlerCallable<T> | string | EventHandler
177
+ ): void
178
+ ```
179
+
180
+ Use when tearing down subscriptions (tests, plugin unload, hot-reload).
181
+
182
+ - Omit `handler` to remove all handlers for `event_pattern`.
183
+ - Pass handler function reference to remove one by function identity.
184
+ - Pass handler id (`string`) or `EventHandler` object to remove by id.
185
+ - use `bus.off('*')` to remove _all_ registered handlers from the bus
186
+
187
+ #### `emit()`
188
+
189
+ ```ts
190
+ emit<T extends BaseEvent>(event: T): T
191
+ ```
192
+
193
+ Behavior notes:
194
+
195
+ - Per-event config fields stay on the event as provided; when unset (`null`/`undefined`), each bus resolves its own defaults at processing time.
196
+ - If same event ends up forwarded through multiple buses, it is loop-protected using `event_path`.
197
+ - Emit is synchronous and returns immediately with the same event object (`event.event_status` is initially `'pending'`).
198
+
199
+ Normal lifecycle:
200
+
201
+ 1. Create event instance (`const event = MyEvent({...})`).
202
+ 2. Emit (`const queued = bus.emit(event)`).
203
+ 3. Await with `await queued.done()` (immediate/queue-jump semantics) or `await queued.eventCompleted()` (bus queue order).
204
+ 4. Inspect `queued.event_results`, `queued.event_result`, `queued.event_errors`, etc. if you need to access handler return values
205
+
206
+ #### `find()`
207
+
208
+ ```ts
209
+ find<T extends BaseEvent>(event_pattern: EventPattern<T> | '*', options?: FindOptions): Promise<T | null>
210
+ find<T extends BaseEvent>(
211
+ event_pattern: EventPattern<T> | '*',
212
+ where: (event: T) => boolean,
213
+ options?: FindOptions
214
+ ): Promise<T | null>
215
+ ```
216
+
217
+ Where:
121
218
 
122
- ### `BaseEvent`
219
+ ```ts
220
+ type FindOptions = {
221
+ past?: boolean | number // true to look through all past events, or number in seconds to filter time range
222
+ future?: boolean | number // true to wait for event to appear indefinitely, or number in seconds to wait for event to appear
223
+ child_of?: BaseEvent | null // filter to only match events that are a child_of: some_parent_event
224
+ } & {
225
+ // event_status: 'pending' | 'started' | 'completed'
226
+ // event_id: 'some-exact-event-uuid-here',
227
+ // event_started_at: string | null (exact iso datetime string or null)
228
+ // ... any event field can be passed to filter events using simple equality checks
229
+ [key: string]: unknown
230
+ }
231
+ ```
123
232
 
124
- Define typed events:
233
+ `bus.find()` returns the first matching event (in emit timestamp order).
234
+ To find multiple matching events, iterate through `bus.event_history.filter((event) => ...some condition...)` manually.
235
+
236
+ `where` behavior:
237
+ Any filter predicate function in the form of `(event) => true | false`, returning true to consider the event a match.
238
+
239
+ ```ts
240
+ const matching_event = bus.find(SomeEvent, (event) => event.some_field == 123)
241
+ // or to match all event types:
242
+ const matching_event = bus.find('*', (event) => event.some_field == 123)
243
+ ```
244
+
245
+ `past` behavior:
246
+
247
+ - `true`: search all history.
248
+ - `false`: skip searching past event history.
249
+ - `number`: search events emitted within last `N` seconds.
250
+
251
+ `future` behavior:
252
+
253
+ - `true`: wait forever for future match.
254
+ - `false`: do not wait.
255
+ - `number`: wait up to `N` seconds.
256
+
257
+ Lifecycle use:
258
+
259
+ - Use for idempotency / de-dupe before emit (`past: ...`).
260
+ - Use for synchronization/waiting (`future: ...`).
261
+ - Combine both to "check recent then wait".
262
+ - Add `child_of` to constrain by parent/ancestor event chain.
263
+ - Add any event field (e.g. `event_status`, `event_id`, `event_timeout`, `user_id`) to filter by strict equality.
264
+ - Use wildcard matching with predicates when you want to search all event types: `bus.find('*', (event) => ...)`.
265
+
266
+ Debouncing expensive events with `find()`:
267
+
268
+ ```ts
269
+ const some_expensive_event = (await bus.find(ExpensiveEvent, { past: 15, future: 5 })) ?? bus.emit(ExpensiveEvent({}))
270
+ await some_expensive_event.done()
271
+ ```
272
+
273
+ Important semantics:
274
+
275
+ - Past lookup matches any emitted events, not just completed events.
276
+ - Past/future matches resolve as soon as event is emitted. If you need the completed event, await `event.done()` or pass `{event_status: 'completed'}` to filter only for completed events.
277
+ - If both `past` and `future` are omitted, defaults are `past: true, future: false`.
278
+ - If both `past` and `future` are `false`, it returns `null` immediately.
279
+ - Detailed behavior matrix is covered in `bubus-ts/tests/eventbus_find.test.ts`.
280
+
281
+ #### `waitUntilIdle(timeout?)`
282
+
283
+ `await bus.waitUntilIdle()` is the normal "drain bus work" call to wait until bus is done processing everything queued.
284
+ Pass an optional timeout in seconds (`await bus.waitUntilIdle(5)`) for a bounded wait.
285
+
286
+ ```ts
287
+ bus.emit(OneEvent(...))
288
+ bus.emit(TwoEvent(...))
289
+ bus.emit(ThreeEvent(...))
290
+ await bus.waitUntilIdle() // this resolves once all three events have finished processing
291
+ await bus.waitUntilIdle(5) // wait up to 5 seconds, then continue even if work is still in-flight
292
+ ```
293
+
294
+ #### Parent/child/event lookup helpers
295
+
296
+ ```ts
297
+ eventIsChildOf(child_event: BaseEvent, paret_event: BaseEvent): boolean
298
+ eventIsParentOf(parent_event: BaseEvent, child_event: BaseEvent): boolean
299
+ findEventById(event_id: string): BaseEvent | null
300
+ ```
301
+
302
+ #### `toString()` / `toJSON()` / `fromJSON()`
303
+
304
+ ```ts
305
+ toString(): string
306
+ toJSON(): EventBusJSON
307
+ EventBus.fromJSON(data: unknown): EventBus
308
+ ```
309
+
310
+ - `toString()` returns `BusName#abcd` style labels used in logs/errors.
311
+ - `toJSON()` exports full bus state snapshot (config, handlers, indexes, event_history, pending queue, in-flight ids, find-waiter snapshots).
312
+ - `fromJSON()` restores a new bus instance from that payload (handler functions are restored as no-op stubs).
313
+
314
+ #### `logTree()`
315
+
316
+ ```ts
317
+ logTree(): string
318
+ ```
319
+
320
+ - `logTree()` returns a full event log hierarchy tree diagram for debugging.
321
+
322
+ #### `destroy()`
323
+
324
+ ```ts
325
+ destroy(): void
326
+ ```
327
+
328
+ - `destroy()` clears handlers/history/locks and removes this bus from global weak registry.
329
+ - `destroy()`/GC behavior is exercised in `bubus-ts/tests/eventbus.test.ts` and `bubus-ts/tests/eventbus_performance.test.ts`.
330
+
331
+ </details>
332
+
333
+ <details>
334
+ <summary><strong>Review event fields, runtime state, and helper methods.</strong></summary>
335
+
336
+ Base class + factory builder for typed event models.
337
+
338
+ Define your own strongly typed events with `BaseEvent.extend('EventName', {...zod fields...})`:
125
339
 
126
340
  ```ts
127
341
  const MyEvent = BaseEvent.extend('MyEvent', {
@@ -131,53 +345,235 @@ const MyEvent = BaseEvent.extend('MyEvent', {
131
345
  // any other payload fields you want to include can go here
132
346
 
133
347
  // fields that start with event_* are reserved for metadata used by the library
134
- event_result_schema: z.string().optional(),
348
+ event_result_type: z.string().optional(),
135
349
  event_timeout: 60,
136
350
  // ...
137
351
  })
138
352
 
139
- const pending_event: MyEvent = MyEvent({ some_key: 'abc', some_other_key: 234 })
140
- const queued_event: MyEvent = bus.emit(pending_event)
141
- const completed_event: MyEvent = queued_event.done()
353
+ const pending_event = MyEvent({ some_key: 'abc', some_other_key: 234 })
354
+ const queued_event = bus.emit(pending_event)
355
+ const completed_event = await queued_event.done()
142
356
  ```
143
357
 
144
- Special fields that change how the event is processed:
358
+ API behavior and lifecycle examples:
359
+
360
+ - `bubus-ts/examples/simple.ts`
361
+ - `bubus-ts/examples/immediate_event_processing.ts`
362
+ - `bubus-ts/examples/forwarding_between_busses.ts`
363
+ - `bubus-ts/tests/eventbus.test.ts`
364
+ - `bubus-ts/tests/eventbus_find.test.ts`
365
+ - `bubus-ts/tests/event_handler_first.test.ts`
366
+ - `bubus-ts/tests/base_event_event_bus_proxy.test.ts`
367
+ - `bubus-ts/tests/eventbus_timeout.test.ts`
368
+ - `bubus-ts/tests/event_result.test.ts`
369
+
370
+ #### Event configuration fields
371
+
372
+ Special configuration fields you can set on each event to control processing:
373
+
374
+ - `event_result_type?: z.ZodTypeAny | String | Number | Boolean | Array | Object`
375
+ - `event_version?: string` (default: `'0.0.1'`; useful for your own schema/data migrations)
376
+ - `event_timeout?: number | null`
377
+ - `event_handler_timeout?: number | null`
378
+ - `event_handler_slow_timeout?: number | null`
379
+ - `event_concurrency?: 'global-serial' | 'bus-serial' | 'parallel' | null`
380
+ - `event_handler_concurrency?: 'serial' | 'parallel' | null`
381
+ - `event_handler_completion?: 'all' | 'first'`
382
+
383
+ #### Runtime state fields
384
+
385
+ - `event_id`, `event_type`, `event_version`
386
+ - `event_path: string[]` (bus labels like `BusName#ab12`)
387
+ - `event_parent_id: string | null`
388
+ - `event_emitted_by_handler_id: string | null`
389
+ - `event_status: 'pending' | 'started' | 'completed'`
390
+ - `event_results: Map<string, EventResult>`
391
+ - `event_pending_bus_count: number`
392
+ - `event_created_at: string`
393
+ - `event_started_at: string | null`
394
+ - `event_completed_at: string | null`
395
+
396
+ #### Read-only attributes
397
+
398
+ - `event_parent` -> `BaseEvent | undefined`
399
+ - `event_children` -> `BaseEvent[]`
400
+ - `event_descendants` -> `BaseEvent[]`
401
+ - `event_errors` -> `Error[]`
402
+ - `event_result` -> `EventResultType<this> | undefined`
145
403
 
146
- - `event_result_schema` defines the type to enforce for handler return values
147
- - `event_concurrency`, `event_handler_concurrency`, `event_handler_completion`
148
- - `event_timeout`, `event_handler_timeout`, `event_handler_slow_timeout`
404
+ #### `done()`
149
405
 
150
- Common methods:
406
+ ```ts
407
+ done(): Promise<this>
408
+ ```
151
409
 
152
- - `await event.done()`
153
- - `await event.first()`
154
- - `event.toJSON()` (serialization format is compatible with python library)
155
- - `event.fromJSON()`
410
+ - If called from inside a running handler, it queue-jumps child processing immediately.
411
+ - If called outside handler context, it waits for normal completion (or processes immediately if already next).
412
+ - Rejects if event is not attached to a bus (`event has no bus attached`).
413
+ - Queue-jump behavior is demonstrated in `bubus-ts/examples/immediate_event_processing.ts` and `bubus-ts/tests/base_event_event_bus_proxy.test.ts`.
156
414
 
157
- #### `done()`
415
+ #### `eventCompleted()`
416
+
417
+ ```ts
418
+ eventCompleted(): Promise<this>
419
+ ```
158
420
 
159
- - Runs the event with completion mode `'all'` and waits for all handlers/buses to finish.
160
- - Returns the same event instance in completed state so you can inspect `event_results`, `event_errors`, etc.
161
- - Want to dispatch and await an event like a function call? simply `await event.done()` and it will process immediately, skipping queued events.
162
- - Want to wait for normal processing in the order it was originally queued? use `await event.waitForCompletion()`
421
+ - Waits for completion in normal runloop order.
422
+ - Use inside handlers when you explicitly do not want queue-jump behavior.
163
423
 
164
424
  #### `first()`
165
425
 
166
- - Runs the event with completion mode `'first'`.
167
- - Returns the temporally first non-`undefined` handler result (not registration order).
168
- - If all handlers return `undefined` (or only error), it resolves to `undefined`.
169
- - Remaining handlers are cancelled after the winning result is found.
426
+ ```ts
427
+ first(): Promise<EventResultType<this> | undefined>
428
+ ```
429
+
430
+ - Forces `event_handler_completion = 'first'` for this run.
431
+ - Returns temporally first non-`undefined` successful handler result.
432
+ - Cancels pending/running losing handlers on the same bus.
433
+ - Returns `undefined` when no handler produces a successful non-`undefined` value.
434
+ - Cancellation and winner-selection behavior is covered in `bubus-ts/tests/event_handler_first.test.ts`.
435
+
436
+ #### `eventResultsList(include?, options?)`
437
+
438
+ ```ts
439
+ eventResultsList(
440
+ include?: (result: EventResultType<this> | undefined, event_result: EventResult<this>) => boolean,
441
+ options?: {
442
+ timeout?: number | null
443
+ include?: (result: EventResultType<this> | undefined, event_result: EventResult<this>) => boolean
444
+ raise_if_any?: boolean
445
+ raise_if_none?: boolean
446
+ }
447
+ ): Promise<Array<EventResultType<this> | undefined>>
448
+ ```
449
+
450
+ - Returns handler result values in `event_results` order.
451
+ - Default filter includes completed non-`null`/non-`undefined` non-error, non-forwarded (`BaseEvent`) values.
452
+ - `raise_if_any` defaults to `true` and throws when any handler result has an error.
453
+ - `raise_if_none` defaults to `true` and throws when no results match `include`.
454
+ - `timeout` is in seconds and bounds how long to wait for completion.
455
+ - Examples:
456
+ - `await event.eventResultsList({ raise_if_any: false, raise_if_none: false })`
457
+ - `await event.eventResultsList((result) => typeof result === 'object', { raise_if_any: false })`
458
+
459
+ #### `eventResultUpdate(handler, options?)`
460
+
461
+ ```ts
462
+ eventResultUpdate(
463
+ handler: EventHandler | EventHandlerCallable<this>,
464
+ options?: {
465
+ eventbus?: EventBus
466
+ status?: 'pending' | 'started' | 'completed' | 'error'
467
+ result?: EventResultType<this> | BaseEvent | undefined
468
+ error?: unknown
469
+ }
470
+ ): EventResult<this>
471
+ ```
472
+
473
+ - Creates (if missing) or updates one `event_results` entry for the given handler id.
474
+ - Useful for deterministic seeding/rehydration paths before resuming normal dispatch.
475
+ - Example:
476
+ - `const seeded = event.eventResultUpdate(handler_entry, { eventbus: bus, status: 'pending' })`
477
+ - `seeded.update({ status: 'completed', result: 'seeded' })`
478
+
479
+ #### `reset()`
480
+
481
+ ```ts
482
+ reset(): this
483
+ ```
484
+
485
+ - Returns a fresh event copy with runtime state reset to pending so it can be emitted again safely.
486
+ - Original event object is unchanged.
487
+ - Generates a new UUIDv7 `event_id` for the returned copy.
488
+ - Clears runtime completion state (`event_results`, status/timestamps, captured async context, done signal, local bus binding).
489
+
490
+ #### `toString()` / `toJSON()` / `fromJSON()`
491
+
492
+ ```ts
493
+ toString(): string
494
+ toJSON(): BaseEventData
495
+ BaseEvent.fromJSON(data: unknown): BaseEvent
496
+ EventFactory.fromJSON?.(data: unknown): TypedEvent
497
+ ```
498
+
499
+ - JSON format is cross-language compatible with Python implementation.
500
+ - `event_result_type` is serialized as JSON Schema when possible and rehydrated on `fromJSON`.
501
+ - In TypeScript-only usage, `event_result_type` can be any Zod schema shape or base type like `number | string | boolean | etc.`. For cross-language roundtrips, object-like schemas (including Python `TypedDict`/`dataclass`-style shapes) are reconstructed on Python as Pydantic models, JSON object keys are always strings, and some fine-grained string-shape constraints may be normalized between Zod and Pydantic.
502
+ - Round-trip coverage is in `bubus-ts/tests/event_result_typed_results.test.ts` and `bubus-ts/tests/eventbus.test.ts`.
170
503
 
171
- ### `EventResult`
504
+ </details>
172
505
 
173
- Each handler run produces an `EventResult` stored in `event.event_results` with:
506
+ <details>
507
+ <summary><strong>Review per-handler status, timing, outputs, and captured errors.</strong></summary>
174
508
 
175
- - `status`: `pending | started | completed | error`
176
- - `result: EventType.event_result_schema` or `error: Error | undefined`
177
- - handler metadata (`handler_id`, `handler_name`, bus metadata)
178
- - `event_children` list of any sub-events that were emitted during handling
509
+ Each handler execution creates one `EventResult` stored in `event.event_results`.
179
510
 
180
- The event aggregates these via `event.event_results` and exposes the values from them via getters like `event.first_result`, `event.event_errors`, and others.
511
+ #### Main fields
512
+
513
+ - `id: string` (uuidv7 string)
514
+ - `status: 'pending' | 'started' | 'completed' | 'error'`
515
+ - `event: BaseEvent`
516
+ - `handler: EventHandler`
517
+ - `result: EventResultType<this> | undefined`
518
+ - `error: unknown | undefined`
519
+ - `started_at: string | null` (ISO datetime string)
520
+ - `completed_at: string | null` (ISO datetime string)
521
+ - `event_children: BaseEvent[]`
522
+
523
+ #### Read-only getters
524
+
525
+ - `event_id` -> `string` uuiv7 of the event the result is for
526
+ - `bus` -> `EventBus` instance it's associated with
527
+ - `handler_id` -> `string` uuidv5 of the `EventHandler`
528
+ - `handler_name` -> `string | 'anonymous'` function name of the handler method
529
+ - `handler_file_path` -> `string | null` path/to/file.js:lineno where the handler method is defined
530
+ - `eventbus_name` -> `string` name, same as `this.bus.name`
531
+ - `eventbus_id` -> `string` uuidv7, same as `this.bus.id`
532
+ - `eventbus_label` -> `string` label, same as `this.bus.label`
533
+ - `value` -> `EventResultType<this> | undefined` alias of `this.result`
534
+ - `raw_value` -> `any` raw result value before schema validation, available when handler return value validation fails
535
+ - `handler_timeout` -> `number` seconds before handler execution is aborted (precedence: handler config -> event config -> bus level defaults)
536
+ - `handler_slow_timeout` -> `number` seconds before logging a slow execution warning (same prececence as `handler_timeout`)
537
+
538
+ #### `toString()` / `toJSON()` / `fromJSON()`
539
+
540
+ ```ts
541
+ toString(): string
542
+ toJSON(): EventResultJSON
543
+ EventResult.fromJSON(event, data): EventResult
544
+ ```
545
+
546
+ </details>
547
+
548
+ <details>
549
+ <summary><strong>Review handler metadata, registration fields, and serialization helpers.</strong></summary>
550
+
551
+ Represents one registered handler entry on a bus. You usually get these from `bus.on(...)`, then pass them to `bus.off(...)` to remove.
552
+
553
+ #### Main fields
554
+
555
+ - `id` unique handler UUIDv5 (deterministic hash from bus/event/handler metadata unless overridden)
556
+ - `handler` function reference that executes for matching events
557
+ - `handler_name` function name (or `'anonymous'`)
558
+ - `handler_file_path` detected source path (`~/path/file.ts:line`) or `null`
559
+ - `handler_timeout` optional timeout override in seconds (`null` disables timeout limit)
560
+ - `handler_slow_timeout` optional slow-warning threshold in seconds (`null` disables slow warning)
561
+ - `handler_registered_at` ISO timestamp
562
+ - `event_pattern` subscribed key (`'SomeEvent'` or `'*'`)
563
+ - `eventbus_name` bus name where this handler was registered
564
+ - `eventbus_id` bus UUID where this handler was registered
565
+
566
+ #### `toString()` / `toJSON()` / `fromJSON()`
567
+
568
+ ```ts
569
+ toString(): string
570
+ toJSON(): EventHandlerJSON
571
+ EventHandler.fromJSON(data: unknown, handler?: EventHandlerCallable): EventHandler
572
+ ```
573
+
574
+ - `toString()` returns `handlerName() (path:line)` when path/name are available, otherwise `function#abcd()`.
575
+ - `toJSON()` emits only serializable handler metadata (never function bodies).
576
+ - `fromJSON()` reconstructs the handler entry and accepts an optional real function to re-bind execution behavior.
181
577
 
182
578
  <br/>
183
579
 
@@ -185,6 +581,8 @@ The event aggregates these via `event.event_results` and exposes the values from
185
581
 
186
582
  <br/>
187
583
 
584
+ </details>
585
+
188
586
  ## 🧵 Advanced Concurrency Control
189
587
 
190
588
  ### Concurrency Config Options
@@ -192,7 +590,11 @@ The event aggregates these via `event.event_results` and exposes the values from
192
590
  #### Bus-level config options (`new EventBus(name, {...options...})`)
193
591
 
194
592
  - `max_history_size?: number | null` (default: `100`)
195
- - Max completed events kept in history. `null` = unlimited. `bus.find(...)` uses this log to query recently completed events
593
+ - Max events kept in history. `null` = unlimited. `bus.find(...)` uses this log to query recently emitted events
594
+ - `0` keeps only pending/in-flight events; each event is removed from history immediately after completion.
595
+ - `max_history_drop?: boolean` (default: `false`)
596
+ - If `true`, drop oldest history entries when history is full (including uncompleted entries if needed).
597
+ - If `false`, reject new emits when history is full.
196
598
  - `event_concurrency?: 'global-serial' | 'bus-serial' | 'parallel' | null` (default: `'bus-serial'`)
197
599
  - Event-level scheduling policy (`global-serial`: FIFO across all buses, `bus-serial`: FIFO per bus, `parallel`: concurrent events per bus).
198
600
  - `event_handler_concurrency?: 'serial' | 'parallel' | null` (default: `'serial'`)
@@ -254,21 +656,21 @@ Timeout resolution for each handler run:
254
656
 
255
657
  Additional timeout nuance:
256
658
 
257
- - `BaseEvent.event_timeout` starts as `null` unless set; dispatch applies bus default timeout when still unset.
659
+ - `BaseEvent.event_timeout` starts as `null` unless set; each processing bus resolves its own `event_timeout` default when still unset.
258
660
  - Bus/event timeouts are outer budgets for handler execution; use `@retry({ timeout })` for per-attempt timeouts.
259
661
 
260
662
  Use `@retry` for per-handler execution timeout/retry/backoff/semaphore control. Keep bus/event timeouts as outer execution budgets.
261
663
 
262
664
  ### Runtime lifecycle (bus -> event -> handler)
263
665
 
264
- Dispatch flow:
666
+ Emit flow:
265
667
 
266
- 1. `dispatch()` normalizes to original event and captures async context when available.
267
- 2. Bus applies defaults and appends itself to `event_path`.
668
+ 1. `emit()` normalizes to original event and captures async context when available.
669
+ 2. Bus appends itself to `event_path` and records runtime ownership for this processing pass.
268
670
  3. Event enters `event_history`, `pending_event_queue`, and runloop starts.
269
671
  4. Runloop dequeues and calls `processEvent()`.
270
672
  5. Event-level semaphore (`event_concurrency`) is applied.
271
- 6. Handler results are created and executed under handler-level semaphore (`event_handler_concurrency`).
673
+ 6. Handler results are created and executed under handler-level semaphore (`event_handler_concurrency`), with timeout/concurrency defaults resolved at processing time on the current bus when event fields are unset.
272
674
  7. Event completion and child completion propagate through `event_pending_bus_count` and result states.
273
675
  8. History trimming evicts completed events first; if still over limit, oldest pending events can be dropped (with warning), then cleanup runs.
274
676
 
@@ -280,7 +682,7 @@ Locking model:
280
682
 
281
683
  ### Queue-jumping (`await event.done()` inside handlers)
282
684
 
283
- Want to dispatch and await an event like a function call? simply `await event.done()`.
685
+ Want to emit and await an event like a function call? simply `await event.done()`.
284
686
  When called inside a handler, the awaited event is processed immediately (queue-jump behavior) before normal queued work continues.
285
687
 
286
688
  ### `@retry` Decorator
@@ -292,7 +694,7 @@ When called inside a handler, the awaited event is processed immediately (queue-
292
694
  Retry and timeout belong on handlers, not emit sites:
293
695
 
294
696
  - Handlers fail; events are messages.
295
- - Handler-level retries preserve replay semantics (one event dispatch, internal retry attempts).
697
+ - Handler-level retries preserve replay semantics (one event emit, internal retry attempts).
296
698
  - Bus concurrency and retry concerns are orthogonal and compose cleanly.
297
699
 
298
700
  #### Recommended pattern: `@retry()` on class methods
@@ -372,7 +774,7 @@ Use bus/event timeouts for outer deadlines and `retry({ timeout })` for per-hand
372
774
 
373
775
  #### Discouraged: retrying emit sites
374
776
 
375
- Avoid wrapping `emit()/done()` in `retry()` unless you intentionally want multiple event dispatches (a new event for every retry).
777
+ Avoid wrapping `emit()/done()` in `retry()` unless you intentionally want multiple event emits (a new event for every retry).
376
778
  Keep retries on handlers so that your logs represent the original high-level intent, with a single event per call even if handling it took multiple tries.
377
779
  Emitting a new event for each retry is only recommended if you are using the logs for debugging more than for replayability / time-travel.
378
780
 
@@ -382,6 +784,35 @@ Emitting a new event for each retry is only recommended if you are using the log
382
784
 
383
785
  <br/>
384
786
 
787
+ ## Bridges
788
+
789
+ Bridges are optional extra connectors provided that allow you to send/receive events from an external service, and you do not need to use a bridge to use bubus since it's normally purely in-memory. These are just simple helpers to forward bubus events JSON to storage engines / other processes / other machines; they prevent loops automatically, but beyond that it's only basic forwarding with no handler pickling or anything fancy.
790
+
791
+ Bridges all expose a very simple bus-like API with only `.emit()` and `.on()`.
792
+
793
+ **Example usage: link a bus to a redis pub/sub channel**
794
+
795
+ ```ts
796
+ const bridge = new RedisEventBridge('redis://redis@localhost:6379')
797
+
798
+ bus.on('*', bridge.emit) // listen for all events on bus and send them to redis channel
799
+ bridge.on('*', bus.emit) // listen for new events in redis channel and emit them on our bus
800
+ ```
801
+
802
+ - `new SocketEventBridge('/tmp/bubus_events.sock')`
803
+ - `new HTTPEventBridge({ send_to: 'https://127.0.0.1:8001/bubus_events', listen_on: 'http://0.0.0.0:8002/bubus_events' })`
804
+ - `new JSONLEventBridge('/tmp/bubus_events.jsonl')`
805
+ - `new SQLiteEventBridge('/tmp/bubus_events.sqlite3')`
806
+ - `new PostgresEventBridge('postgresql://user:pass@localhost:5432/dbname/bubus_events')`
807
+ - `new RedisEventBridge('redis://user:pass@localhost:6379/1/bubus_events')`
808
+ - `new NATSEventBridge('nats://localhost:4222', 'bubus_events')`
809
+
810
+ <br/>
811
+
812
+ ---
813
+
814
+ <br/>
815
+
385
816
  ## 🏃 Runtimes
386
817
 
387
818
  `bubus-ts` supports all major JS runtimes.
@@ -394,7 +825,7 @@ Emitting a new event for each retry is only recommended if you are using the log
394
825
  ### Browser support notes
395
826
 
396
827
  - The package output is ESM (`./dist/esm`) which is supported by all browsers [released after 2018](https://caniuse.com/?search=ESM)
397
- - `AsyncLocalStorage` is preserved at dispatch and used during handling when availabe (Node/Bun), otel/tracing context will work normally in those environments
828
+ - `AsyncLocalStorage` is preserved at emit and used during handling when available (Node/Bun), otel/tracing context will work normally in those environments
398
829
 
399
830
  ### Performance comparison (local run, per-event)
400
831
 
@@ -405,18 +836,18 @@ Measured locally on an `Apple M4 Pro` with:
405
836
  - `pnpm run perf:deno` (`deno v2.6.8`)
406
837
  - `pnpm run perf:browser` (`chrome v145.0.7632.6`)
407
838
 
408
- | Runtime | 1 bus x 50k events x 1 handler | 500 busses x 100 events x 1 handler | 1 bus x 1 event x 50k parallel handlers | 1 bus x 50k events x 50k one-off handlers | Worst case (N busses x N events x N handlers) |
409
- | ------------------ | ------------------------------ | ----------------------------------- | --------------------------------------- | ----------------------------------------- | --------------------------------------------- |
410
- | Node | `0.015ms/event`, `0.6kb/event` | `0.058ms/event`, `0.1kb/event` | `0.021ms/handler`, `189792.0kb/event` | `0.028ms/event`, `0.6kb/event` | `0.442ms/event`, `0.9kb/event` |
411
- | Bun | `0.011ms/event`, `2.5kb/event` | `0.054ms/event`, `1.0kb/event` | `0.006ms/handler`, `223296.0kb/event` | `0.019ms/event`, `2.8kb/event` | `0.441ms/event`, `3.1kb/event` |
412
- | Deno | `0.018ms/event`, `1.2kb/event` | `0.063ms/event`, `0.4kb/event` | `0.024ms/handler`, `156752.0kb/event` | `0.064ms/event`, `2.6kb/event` | `0.461ms/event`, `7.9kb/event` |
413
- | Browser (Chromium) | `0.030ms/event` | `0.197ms/event` | `0.022ms/handler` | `0.022ms/event` | `1.566ms/event` |
839
+ | Runtime | 1 bus x 50k events x 1 handler | 500 buses x 100 events x 1 handler | 1 bus x 1 event x 50k parallel handlers | 1 bus x 50k events x 50k one-off handlers | Worst case (N buses x N events x N handlers) |
840
+ | ------------------ | ------------------------------ | ---------------------------------- | --------------------------------------- | ----------------------------------------- | -------------------------------------------- |
841
+ | Node | `0.015ms/event`, `0.6kb/event` | `0.058ms/event`, `0.1kb/event` | `0.021ms/handler`, `3.8kb/handler` | `0.028ms/event`, `0.6kb/event` | `0.442ms/event`, `0.9kb/event` |
842
+ | Bun | `0.011ms/event`, `2.5kb/event` | `0.054ms/event`, `1.0kb/event` | `0.006ms/handler`, `4.5kb/handler` | `0.019ms/event`, `2.8kb/event` | `0.441ms/event`, `3.1kb/event` |
843
+ | Deno | `0.018ms/event`, `1.2kb/event` | `0.063ms/event`, `0.4kb/event` | `0.024ms/handler`, `3.1kb/handler` | `0.064ms/event`, `2.6kb/event` | `0.461ms/event`, `7.9kb/event` |
844
+ | Browser (Chromium) | `0.030ms/event` | `0.197ms/event` | `0.022ms/handler` | `0.022ms/event` | `1.566ms/event` |
414
845
 
415
846
  Notes:
416
847
 
417
848
  - `kb/event` is peak RSS delta per event during active processing (most representative of OS-visible RAM in Activity Monitor / Task Manager, with `EventBus.max_history_size=1`)
418
849
  - In `1 bus x 1 event x 50k parallel handlers` stats are shown per-handler for clarity, `0.02ms/handler * 50k handlers ~= 1000ms` for the entire event
419
- - Browser runtime does not expose memory usage easily, in practice memory performance in-browser is comparable to Node (they both use V8)
850
+ - Browser runtime does not expose memory usage directly, in practice memory performance in-browser is comparable to Node (they both use V8)
420
851
 
421
852
  <br/>
422
853
 
@@ -431,6 +862,10 @@ git clone https://github.com/pirate/bbus bubus && cd bubus
431
862
 
432
863
  cd ./bubus-ts
433
864
  pnpm install
865
+
866
+ prek install # install pre-commit hooks
867
+ prek run --all-files # run pre-commit hooks on all files manually
868
+
434
869
  pnpm lint
435
870
  pnpm test
436
871
  ```