abxbus 2.4.16 → 2.4.18

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 (42) hide show
  1. package/dist/cjs/event_bus.js +1 -1
  2. package/dist/cjs/event_bus.js.map +2 -2
  3. package/dist/cjs/event_handler.d.ts +1 -0
  4. package/dist/cjs/event_handler.js +14 -1
  5. package/dist/cjs/event_handler.js.map +2 -2
  6. package/dist/cjs/middleware_otel_tracing.js +3 -3
  7. package/dist/cjs/middleware_otel_tracing.js.map +2 -2
  8. package/dist/cjs/retry.js +39 -0
  9. package/dist/cjs/retry.js.map +2 -2
  10. package/dist/esm/event_bus.js +1 -1
  11. package/dist/esm/event_bus.js.map +2 -2
  12. package/dist/esm/event_handler.js +14 -1
  13. package/dist/esm/event_handler.js.map +2 -2
  14. package/dist/esm/middleware_otel_tracing.js +4 -4
  15. package/dist/esm/middleware_otel_tracing.js.map +2 -2
  16. package/dist/esm/retry.js +39 -0
  17. package/dist/esm/retry.js.map +2 -2
  18. package/dist/types/event_handler.d.ts +1 -0
  19. package/package.json +5 -1
  20. package/src/async_context.ts +70 -0
  21. package/src/base_event.ts +1201 -0
  22. package/src/bridge_jsonl.ts +174 -0
  23. package/src/bridge_nats.ts +104 -0
  24. package/src/bridge_postgres.ts +277 -0
  25. package/src/bridge_redis.ts +194 -0
  26. package/src/bridge_sqlite.ts +289 -0
  27. package/src/bridges.ts +376 -0
  28. package/src/event_bus.ts +1263 -0
  29. package/src/event_handler.ts +379 -0
  30. package/src/event_history.ts +247 -0
  31. package/src/event_result.ts +483 -0
  32. package/src/events_suck.ts +96 -0
  33. package/src/helpers.ts +65 -0
  34. package/src/index.ts +37 -0
  35. package/src/lock_manager.ts +401 -0
  36. package/src/logging.ts +261 -0
  37. package/src/middleware_otel_tracing.ts +201 -0
  38. package/src/middlewares.ts +16 -0
  39. package/src/optional_deps.ts +52 -0
  40. package/src/retry.ts +578 -0
  41. package/src/timing.ts +52 -0
  42. package/src/types.ts +132 -0
@@ -0,0 +1,1263 @@
1
+ import { BaseEvent, type BaseEventJSON } from './base_event.js'
2
+ import { EventHistory } from './event_history.js'
3
+ import { EventResult } from './event_result.js'
4
+ import { captureAsyncContext } from './async_context.js'
5
+ import { _runWithSlowMonitor, _runWithTimeout } from './timing.js'
6
+ import {
7
+ AsyncLock,
8
+ type EventConcurrencyMode,
9
+ type EventHandlerConcurrencyMode,
10
+ type EventHandlerCompletionMode,
11
+ LockManager,
12
+ } from './lock_manager.js'
13
+ import {
14
+ EventHandler,
15
+ EventHandlerAbortedError,
16
+ EventHandlerCancelledError,
17
+ EventHandlerTimeoutError,
18
+ type EphemeralFindEventHandler,
19
+ type EventHandlerJSON,
20
+ } from './event_handler.js'
21
+ import type { EventBusMiddleware, EventBusMiddlewareCtor, EventBusMiddlewareInput } from './middlewares.js'
22
+ import { logTree } from './logging.js'
23
+ import { v7 as uuidv7 } from 'uuid'
24
+ import { monotonicDatetime } from './helpers.js'
25
+
26
+ import { normalizeEventPattern } from './types.js'
27
+ import type { EventClass, EventHandlerCallable, EventPattern, FindOptions, UntypedEventHandlerFunction } from './types.js'
28
+
29
+ export type EventBusOptions = {
30
+ id?: string
31
+ max_history_size?: number | null
32
+ max_history_drop?: boolean
33
+
34
+ // per-event options
35
+ event_concurrency?: EventConcurrencyMode | null
36
+ event_timeout?: number | null // default handler timeout in seconds, applied when event.event_timeout is undefined
37
+ event_slow_timeout?: number | null // threshold before a warning is logged about slow event processing
38
+
39
+ // per-event-handler options
40
+ event_handler_concurrency?: EventHandlerConcurrencyMode | null
41
+ event_handler_completion?: EventHandlerCompletionMode
42
+ event_handler_slow_timeout?: number | null // threshold before a warning is logged about slow handler execution
43
+ event_handler_detect_file_paths?: boolean // autodetect source code file and lineno where handlers are defined for better logs (slightly slower because Error().stack introspection to fine files is expensive)
44
+ middlewares?: EventBusMiddlewareInput[]
45
+ }
46
+
47
+ export type EventBusJSON = {
48
+ id: string
49
+ name: string
50
+ max_history_size: number | null
51
+ max_history_drop: boolean
52
+ event_concurrency: EventConcurrencyMode
53
+ event_timeout: number | null
54
+ event_slow_timeout: number | null
55
+ event_handler_concurrency: EventHandlerConcurrencyMode
56
+ event_handler_completion: EventHandlerCompletionMode
57
+ event_handler_slow_timeout: number | null
58
+ event_handler_detect_file_paths: boolean
59
+ handlers: Record<string, EventHandlerJSON>
60
+ handlers_by_key: Record<string, string[]>
61
+ event_history: Record<string, BaseEventJSON>
62
+ pending_event_queue: string[]
63
+ }
64
+
65
+ // Global registry of all EventBus instances to allow for cross-bus coordination
66
+ // when global-serial concurrency mode is used.
67
+ export class GlobalEventBusRegistry {
68
+ private _bus_refs = new Set<WeakRef<EventBus>>()
69
+
70
+ add(bus: EventBus): void {
71
+ this._bus_refs.add(new WeakRef(bus))
72
+ }
73
+
74
+ discard(bus: EventBus): void {
75
+ for (const ref of this._bus_refs) {
76
+ const current = ref.deref()
77
+ if (!current || current === bus) {
78
+ this._bus_refs.delete(ref)
79
+ }
80
+ }
81
+ }
82
+
83
+ has(bus: EventBus): boolean {
84
+ for (const ref of this._bus_refs) {
85
+ const current = ref.deref()
86
+ if (!current) {
87
+ this._bus_refs.delete(ref)
88
+ continue
89
+ }
90
+ if (current === bus) {
91
+ return true
92
+ }
93
+ }
94
+ return false
95
+ }
96
+
97
+ get size(): number {
98
+ let count = 0
99
+ for (const ref of this._bus_refs) {
100
+ if (ref.deref()) {
101
+ count += 1
102
+ } else {
103
+ this._bus_refs.delete(ref)
104
+ }
105
+ }
106
+ return count
107
+ }
108
+
109
+ *[Symbol.iterator](): IterableIterator<EventBus> {
110
+ for (const ref of this._bus_refs) {
111
+ const bus = ref.deref()
112
+ if (bus) {
113
+ yield bus
114
+ } else {
115
+ this._bus_refs.delete(ref)
116
+ }
117
+ }
118
+ }
119
+
120
+ findBusById(bus_id: string): EventBus | undefined {
121
+ for (const bus of this) {
122
+ if (bus.id === bus_id) {
123
+ return bus
124
+ }
125
+ }
126
+ return undefined
127
+ }
128
+
129
+ findEventById(event_id: string): BaseEvent | null {
130
+ for (const bus of this) {
131
+ const event = bus.event_history.getEvent(event_id)
132
+ if (event) {
133
+ return event
134
+ }
135
+ }
136
+ return null
137
+ }
138
+ }
139
+
140
+ export class EventBus {
141
+ private static _registry_by_constructor = new WeakMap<Function, GlobalEventBusRegistry>()
142
+ private static _global_event_lock_by_constructor = new WeakMap<Function, AsyncLock>()
143
+
144
+ private static getRegistryForConstructor(constructor_fn: Function): GlobalEventBusRegistry {
145
+ const existing_registry = EventBus._registry_by_constructor.get(constructor_fn)
146
+ if (existing_registry) {
147
+ return existing_registry
148
+ }
149
+ const created_registry = new GlobalEventBusRegistry()
150
+ EventBus._registry_by_constructor.set(constructor_fn, created_registry)
151
+ return created_registry
152
+ }
153
+
154
+ private static getGlobalEventLockForConstructor(constructor_fn: Function): AsyncLock {
155
+ const existing_lock = EventBus._global_event_lock_by_constructor.get(constructor_fn)
156
+ if (existing_lock) {
157
+ return existing_lock
158
+ }
159
+ const created_lock = new AsyncLock(1)
160
+ EventBus._global_event_lock_by_constructor.set(constructor_fn, created_lock)
161
+ return created_lock
162
+ }
163
+
164
+ static get all_instances(): GlobalEventBusRegistry {
165
+ return EventBus.getRegistryForConstructor(this)
166
+ }
167
+
168
+ get all_instances(): GlobalEventBusRegistry {
169
+ return EventBus.getRegistryForConstructor(this.constructor as Function)
170
+ }
171
+
172
+ get _lock_for_event_global_serial(): AsyncLock {
173
+ return EventBus.getGlobalEventLockForConstructor(this.constructor as Function)
174
+ }
175
+
176
+ id: string // unique uuidv7 identifier for the event bus
177
+ name: string // name of the event bus, recommended to include the word "Bus" in the name for clarity in logs
178
+
179
+ // configuration options
180
+ event_timeout: number | null
181
+ event_concurrency: EventConcurrencyMode
182
+ event_handler_concurrency: EventHandlerConcurrencyMode
183
+ event_handler_completion: EventHandlerCompletionMode
184
+ event_handler_detect_file_paths: boolean
185
+
186
+ // slow processing warning timeout settings
187
+ event_handler_slow_timeout: number | null
188
+ event_slow_timeout: number | null
189
+
190
+ // public runtime state
191
+ handlers: Map<string, EventHandler> // map of handler uuidv5 ids to EventHandler objects
192
+ handlers_by_key: Map<string, string[]> // map of normalized event_pattern to ordered handler ids
193
+ event_history: EventHistory<BaseEvent> // map of event uuidv7 ids to processed BaseEvent objects
194
+
195
+ // internal runtime state
196
+ pending_event_queue: BaseEvent[] // queue of events that have been emitted to the bus but not yet processed
197
+ in_flight_event_ids: Set<string> // set of event ids that are currently being processed by the bus
198
+ runloop_running: boolean
199
+ locks: LockManager
200
+ find_waiters: Set<EphemeralFindEventHandler> // set of EphemeralFindEventHandler objects that are waiting for a matching future event
201
+ middlewares: EventBusMiddleware[]
202
+
203
+ private static normalizeMiddlewares(middlewares?: EventBusMiddlewareInput[]): EventBusMiddleware[] {
204
+ const normalized: EventBusMiddleware[] = []
205
+ for (const middleware of middlewares ?? []) {
206
+ if (!middleware) {
207
+ continue
208
+ }
209
+ if (typeof middleware === 'function') {
210
+ normalized.push(new (middleware as EventBusMiddlewareCtor)())
211
+ } else {
212
+ normalized.push(middleware as EventBusMiddleware)
213
+ }
214
+ }
215
+ return normalized
216
+ }
217
+
218
+ constructor(name: string = 'EventBus', options: EventBusOptions = {}) {
219
+ this.id = options.id ?? uuidv7()
220
+ this.name = name
221
+
222
+ // set configuration options
223
+ this.event_concurrency = options.event_concurrency ?? 'bus-serial'
224
+ this.event_handler_concurrency = options.event_handler_concurrency ?? 'serial'
225
+ this.event_handler_completion = options.event_handler_completion ?? 'all'
226
+ this.event_handler_detect_file_paths = options.event_handler_detect_file_paths ?? true
227
+ this.event_timeout = options.event_timeout === undefined ? 60 : options.event_timeout
228
+ this.event_handler_slow_timeout = options.event_handler_slow_timeout === undefined ? 30 : options.event_handler_slow_timeout
229
+ this.event_slow_timeout = options.event_slow_timeout === undefined ? 300 : options.event_slow_timeout
230
+
231
+ // initialize runtime state
232
+ this.runloop_running = false
233
+ this.handlers = new Map()
234
+ this.handlers_by_key = new Map()
235
+ this.find_waiters = new Set()
236
+ this.event_history = new EventHistory({
237
+ max_history_size: options.max_history_size === undefined ? 100 : options.max_history_size,
238
+ max_history_drop: options.max_history_drop ?? false,
239
+ })
240
+ this.pending_event_queue = []
241
+ this.in_flight_event_ids = new Set()
242
+ this.locks = new LockManager(this)
243
+ this.middlewares = EventBus.normalizeMiddlewares(options.middlewares)
244
+
245
+ this.all_instances.add(this)
246
+
247
+ this.dispatch = this.dispatch.bind(this)
248
+ this.emit = this.emit.bind(this)
249
+ }
250
+
251
+ toString(): string {
252
+ return `${this.name}#${this.id.slice(-4)}`
253
+ }
254
+
255
+ scheduleMicrotask(fn: () => void): void {
256
+ if (typeof queueMicrotask === 'function') {
257
+ queueMicrotask(fn)
258
+ return
259
+ }
260
+ void Promise.resolve().then(fn)
261
+ }
262
+
263
+ private async _runMiddlewareHook(hook: keyof EventBusMiddleware, args: unknown[]): Promise<void> {
264
+ if (this.middlewares.length === 0) {
265
+ return
266
+ }
267
+ for (const middleware of this.middlewares) {
268
+ const callback = middleware[hook]
269
+ if (!callback) {
270
+ continue
271
+ }
272
+ await (callback as (...hook_args: unknown[]) => void | Promise<void>).apply(middleware, args)
273
+ }
274
+ }
275
+
276
+ async onEventChange(event: BaseEvent, status: 'pending' | 'started' | 'completed'): Promise<void> {
277
+ await this._onEventChange(event, status)
278
+ }
279
+
280
+ async onEventResultChange(event: BaseEvent, result: EventResult, status: 'pending' | 'started' | 'completed'): Promise<void> {
281
+ await this._onEventResultChange(event, result, status)
282
+ }
283
+
284
+ private async _onEventChange(event: BaseEvent, status: 'pending' | 'started' | 'completed'): Promise<void> {
285
+ await this._runMiddlewareHook('onEventChange', [this, event, status])
286
+ }
287
+
288
+ private async _onEventResultChange(event: BaseEvent, result: EventResult, status: 'pending' | 'started' | 'completed'): Promise<void> {
289
+ await this._runMiddlewareHook('onEventResultChange', [this, event, result, status])
290
+ }
291
+
292
+ private async _onBusHandlersChange(handler: EventHandler, registered: boolean): Promise<void> {
293
+ await this._runMiddlewareHook('onBusHandlersChange', [this, handler, registered])
294
+ }
295
+
296
+ private _finalizeEventTimeout(
297
+ event: BaseEvent,
298
+ pending_entries: Array<{
299
+ handler: EventHandler
300
+ result: EventResult
301
+ }>,
302
+ timeout_error: EventHandlerTimeoutError
303
+ ): void {
304
+ const timeout_seconds = timeout_error.timeout_seconds ?? event.event_timeout ?? null
305
+ event._cancelPendingChildProcessing(timeout_error)
306
+
307
+ for (const entry of pending_entries) {
308
+ const result = entry.result
309
+ if (result.status === 'completed') {
310
+ continue
311
+ }
312
+ if (result.status === 'error') {
313
+ continue
314
+ }
315
+ if (result.status === 'started') {
316
+ result._lock?.exitHandlerRun()
317
+ result._releaseQueueJumpPauses()
318
+ const aborted_error = new EventHandlerAbortedError(`Aborted running handler due to event timeout`, {
319
+ event_result: result,
320
+ timeout_seconds,
321
+ cause: timeout_error,
322
+ })
323
+ result._markError(aborted_error)
324
+ result._signalAbort(aborted_error)
325
+ continue
326
+ }
327
+ const cancelled_error = new EventHandlerCancelledError(`Cancelled pending handler due to event timeout`, {
328
+ event_result: result,
329
+ timeout_seconds,
330
+ cause: timeout_error,
331
+ })
332
+ result._markError(cancelled_error)
333
+ }
334
+
335
+ event.event_pending_bus_count = Math.max(0, event.event_pending_bus_count - 1)
336
+ event._markCompleted()
337
+ }
338
+
339
+ private _createEventTimeoutError(
340
+ event: BaseEvent,
341
+ pending_entries: Array<{
342
+ handler: EventHandler
343
+ result: EventResult
344
+ }>,
345
+ timeout_seconds: number
346
+ ): EventHandlerTimeoutError {
347
+ const timeout_anchor =
348
+ pending_entries.find((entry) => entry.result.status === 'started') ??
349
+ pending_entries.find((entry) => entry.result.status === 'pending') ??
350
+ pending_entries[0]!
351
+ return new EventHandlerTimeoutError(
352
+ `${this.toString()}.on(${event.toString()}, ${timeout_anchor.result.handler.toString()}) timed out after ${timeout_seconds}s`,
353
+ {
354
+ event_result: timeout_anchor.result,
355
+ timeout_seconds,
356
+ }
357
+ )
358
+ }
359
+
360
+ private async _runHandlersWithTimeout(
361
+ event: BaseEvent,
362
+ pending_entries: Array<{
363
+ handler: EventHandler
364
+ result: EventResult
365
+ }>,
366
+ event_timeout: number | null,
367
+ fn: () => Promise<void>
368
+ ): Promise<void> {
369
+ try {
370
+ if (event_timeout === null || pending_entries.length === 0) {
371
+ await fn()
372
+ } else {
373
+ await _runWithTimeout(event_timeout, () => this._createEventTimeoutError(event, pending_entries, event_timeout), fn)
374
+ }
375
+ } catch (error) {
376
+ if (error instanceof EventHandlerTimeoutError) {
377
+ this._finalizeEventTimeout(event, pending_entries, error)
378
+ return
379
+ }
380
+ throw error
381
+ }
382
+ }
383
+
384
+ private _markEventCompletedIfNeeded(event: BaseEvent): void {
385
+ if (event.event_status !== 'completed') {
386
+ event.event_pending_bus_count = Math.max(0, event.event_pending_bus_count - 1)
387
+ event._markCompleted(false)
388
+ }
389
+ if (
390
+ this.event_history.max_history_size !== null &&
391
+ this.event_history.max_history_size > 0 &&
392
+ this.event_history.size > this.event_history.max_history_size
393
+ ) {
394
+ this.event_history.trimEventHistory({
395
+ is_event_complete: (candidate_event) => candidate_event.event_status === 'completed',
396
+ on_remove: (candidate_event) => candidate_event._gc(),
397
+ owner_label: this.toString(),
398
+ max_history_size: this.event_history.max_history_size,
399
+ max_history_drop: this.event_history.max_history_drop,
400
+ })
401
+ }
402
+ }
403
+
404
+ toJSON(): EventBusJSON {
405
+ const handlers: Record<string, EventHandlerJSON> = {}
406
+ for (const [handler_id, handler] of this.handlers.entries()) {
407
+ handlers[handler_id] = handler.toJSON()
408
+ }
409
+
410
+ const handlers_by_key: Record<string, string[]> = {}
411
+ for (const [key, ids] of this.handlers_by_key.entries()) {
412
+ handlers_by_key[key] = [...ids]
413
+ }
414
+
415
+ const event_history: Record<string, BaseEventJSON> = {}
416
+ for (const [event_id, event] of this.event_history.entries()) {
417
+ event_history[event_id] = event.toJSON()
418
+ }
419
+
420
+ const pending_event_queue: string[] = []
421
+ for (const event of this.pending_event_queue) {
422
+ const event_id = event.event_id
423
+ if (!event_history[event_id]) {
424
+ event_history[event_id] = event.toJSON()
425
+ }
426
+ pending_event_queue.push(event_id)
427
+ }
428
+
429
+ return {
430
+ id: this.id,
431
+ name: this.name,
432
+ max_history_size: this.event_history.max_history_size,
433
+ max_history_drop: this.event_history.max_history_drop,
434
+ event_concurrency: this.event_concurrency,
435
+ event_timeout: this.event_timeout,
436
+ event_slow_timeout: this.event_slow_timeout,
437
+ event_handler_concurrency: this.event_handler_concurrency,
438
+ event_handler_completion: this.event_handler_completion,
439
+ event_handler_slow_timeout: this.event_handler_slow_timeout,
440
+ event_handler_detect_file_paths: this.event_handler_detect_file_paths,
441
+ handlers,
442
+ handlers_by_key,
443
+ event_history,
444
+ pending_event_queue,
445
+ }
446
+ }
447
+
448
+ private static _stubHandlerFn(): EventHandlerCallable {
449
+ return (() => undefined) as EventHandlerCallable
450
+ }
451
+
452
+ private static _upsertHandlerIndex(bus: EventBus, event_pattern: string, handler_id: string): void {
453
+ const ids = bus.handlers_by_key.get(event_pattern)
454
+ if (ids) {
455
+ if (!ids.includes(handler_id)) {
456
+ ids.push(handler_id)
457
+ }
458
+ return
459
+ }
460
+ bus.handlers_by_key.set(event_pattern, [handler_id])
461
+ }
462
+
463
+ private static _linkEventResultHandlers(event: BaseEvent, bus: EventBus): void {
464
+ for (const [map_key, result] of Array.from(event.event_results.entries())) {
465
+ const handler_id = result.handler_id
466
+ const existing_handler = bus.handlers.get(handler_id)
467
+ if (existing_handler) {
468
+ result.handler = existing_handler
469
+ } else {
470
+ const source = result.handler
471
+ const handler_entry = EventHandler.fromJSON(
472
+ {
473
+ ...source.toJSON(),
474
+ id: handler_id,
475
+ event_pattern: source.event_pattern || event.event_type,
476
+ eventbus_name: source.eventbus_name || bus.name,
477
+ eventbus_id: source.eventbus_id || bus.id,
478
+ },
479
+ EventBus._stubHandlerFn()
480
+ )
481
+ bus.handlers.set(handler_entry.id, handler_entry)
482
+ EventBus._upsertHandlerIndex(bus, handler_entry.event_pattern, handler_entry.id)
483
+ result.handler = handler_entry
484
+ }
485
+
486
+ if (map_key !== handler_id) {
487
+ event.event_results.delete(map_key)
488
+ event.event_results.set(handler_id, result)
489
+ }
490
+ }
491
+ }
492
+
493
+ static fromJSON(data: unknown): EventBus {
494
+ if (!data || typeof data !== 'object') {
495
+ throw new Error('EventBus.fromJSON(data) requires an object')
496
+ }
497
+ const record = data as Record<string, unknown>
498
+ const name = typeof record.name === 'string' ? record.name : 'EventBus'
499
+ const options: EventBusOptions = {}
500
+
501
+ if (typeof record.id === 'string') options.id = record.id
502
+ if (typeof record.max_history_size === 'number' || record.max_history_size === null) options.max_history_size = record.max_history_size
503
+ if (typeof record.max_history_drop === 'boolean') options.max_history_drop = record.max_history_drop
504
+ if (
505
+ record.event_concurrency === 'global-serial' ||
506
+ record.event_concurrency === 'bus-serial' ||
507
+ record.event_concurrency === 'parallel'
508
+ ) {
509
+ options.event_concurrency = record.event_concurrency
510
+ }
511
+ if (typeof record.event_timeout === 'number' || record.event_timeout === null) options.event_timeout = record.event_timeout
512
+ if (typeof record.event_slow_timeout === 'number' || record.event_slow_timeout === null)
513
+ options.event_slow_timeout = record.event_slow_timeout
514
+ if (record.event_handler_concurrency === 'serial' || record.event_handler_concurrency === 'parallel') {
515
+ options.event_handler_concurrency = record.event_handler_concurrency
516
+ }
517
+ if (record.event_handler_completion === 'all' || record.event_handler_completion === 'first') {
518
+ options.event_handler_completion = record.event_handler_completion
519
+ }
520
+ if (typeof record.event_handler_slow_timeout === 'number' || record.event_handler_slow_timeout === null) {
521
+ options.event_handler_slow_timeout = record.event_handler_slow_timeout
522
+ }
523
+ if (typeof record.event_handler_detect_file_paths === 'boolean') {
524
+ options.event_handler_detect_file_paths = record.event_handler_detect_file_paths
525
+ }
526
+ const bus = new EventBus(name, options)
527
+
528
+ if (!record.handlers || typeof record.handlers !== 'object' || Array.isArray(record.handlers)) {
529
+ throw new Error('EventBus.fromJSON(data) requires handlers as an id-keyed object')
530
+ }
531
+ for (const [handler_id, payload] of Object.entries(record.handlers as Record<string, unknown>)) {
532
+ if (!payload || typeof payload !== 'object') {
533
+ continue
534
+ }
535
+ const parsed = EventHandler.fromJSON(
536
+ {
537
+ ...(payload as Record<string, unknown>),
538
+ id: typeof (payload as { id?: unknown }).id === 'string' ? (payload as { id: string }).id : handler_id,
539
+ },
540
+ EventBus._stubHandlerFn()
541
+ )
542
+ bus.handlers.set(parsed.id, parsed)
543
+ }
544
+
545
+ if (!record.handlers_by_key || typeof record.handlers_by_key !== 'object' || Array.isArray(record.handlers_by_key)) {
546
+ throw new Error('EventBus.fromJSON(data) requires handlers_by_key as an object')
547
+ }
548
+ bus.handlers_by_key.clear()
549
+ for (const [raw_key, raw_ids] of Object.entries(record.handlers_by_key as Record<string, unknown>)) {
550
+ if (!Array.isArray(raw_ids)) {
551
+ continue
552
+ }
553
+ const ids = raw_ids.filter((id): id is string => typeof id === 'string')
554
+ bus.handlers_by_key.set(raw_key, ids)
555
+ }
556
+
557
+ if (!record.event_history || typeof record.event_history !== 'object' || Array.isArray(record.event_history)) {
558
+ throw new Error('EventBus.fromJSON(data) requires event_history as an id-keyed object')
559
+ }
560
+ for (const [event_id, payload] of Object.entries(record.event_history as Record<string, unknown>)) {
561
+ if (!payload || typeof payload !== 'object') {
562
+ continue
563
+ }
564
+ const event = BaseEvent.fromJSON({
565
+ ...(payload as Record<string, unknown>),
566
+ event_id: typeof (payload as { event_id?: unknown }).event_id === 'string' ? (payload as { event_id: string }).event_id : event_id,
567
+ })
568
+ event.event_bus = bus
569
+ bus.event_history.set(event.event_id, event)
570
+ }
571
+
572
+ if (!Array.isArray(record.pending_event_queue)) {
573
+ throw new Error('EventBus.fromJSON(data) requires pending_event_queue as an array of event ids')
574
+ }
575
+ const raw_pending_event_queue = record.pending_event_queue
576
+ const pending_event_ids: string[] = []
577
+ for (const item of raw_pending_event_queue) {
578
+ if (typeof item === 'string') {
579
+ pending_event_ids.push(item)
580
+ }
581
+ }
582
+ bus.pending_event_queue = pending_event_ids
583
+ .map((event_id) => bus.event_history.get(event_id))
584
+ .filter((event): event is BaseEvent => Boolean(event))
585
+
586
+ for (const event of bus.event_history.values()) {
587
+ EventBus._linkEventResultHandlers(event, bus)
588
+ }
589
+
590
+ // Reset runtime execution state after restore. Queue/history/handlers are restored,
591
+ // but lock internals should always restart from a clean default state.
592
+ bus.in_flight_event_ids.clear()
593
+ bus.runloop_running = false
594
+ bus.locks.clear()
595
+ bus.find_waiters.clear()
596
+
597
+ return bus
598
+ }
599
+
600
+ get label(): string {
601
+ return `${this.name}#${this.id.slice(-4)}`
602
+ }
603
+
604
+ removeEventFromPendingQueue(event: BaseEvent): number {
605
+ const original_event = event._event_original ?? event
606
+ let removed_count = 0
607
+ for (let index = this.pending_event_queue.length - 1; index >= 0; index -= 1) {
608
+ const queued_event = this.pending_event_queue[index]
609
+ const queued_original = queued_event._event_original ?? queued_event
610
+ if (queued_original.event_id !== original_event.event_id) {
611
+ continue
612
+ }
613
+ this.pending_event_queue.splice(index, 1)
614
+ removed_count += 1
615
+ }
616
+ return removed_count
617
+ }
618
+
619
+ isEventInFlightOrQueued(event_id: string): boolean {
620
+ if (this.in_flight_event_ids.has(event_id)) {
621
+ return true
622
+ }
623
+ for (const queued_event of this.pending_event_queue) {
624
+ const queued_original = queued_event._event_original ?? queued_event
625
+ if (queued_original.event_id === event_id) {
626
+ return true
627
+ }
628
+ }
629
+ return false
630
+ }
631
+
632
+ removeEventFromHistory(event_id: string): boolean {
633
+ return this.event_history.delete(event_id)
634
+ }
635
+
636
+ // destroy the event bus and all its state to allow for garbage collection
637
+ destroy(): void {
638
+ this.all_instances.discard(this)
639
+ this.handlers.clear()
640
+ this.handlers_by_key.clear()
641
+ for (const event of this.event_history.values()) {
642
+ event._gc()
643
+ }
644
+ this.event_history.clear()
645
+ this.pending_event_queue.length = 0
646
+ this.in_flight_event_ids.clear()
647
+ this.find_waiters.clear()
648
+ this.locks.clear()
649
+ }
650
+
651
+ on<T extends BaseEvent>(event_pattern: EventClass<T>, handler: EventHandlerCallable<T>, options?: Partial<EventHandler>): EventHandler
652
+ on<T extends BaseEvent>(
653
+ event_pattern: string | '*',
654
+ handler: UntypedEventHandlerFunction<T>,
655
+ options?: Partial<EventHandler>
656
+ ): EventHandler
657
+ on(
658
+ event_pattern: EventPattern | '*',
659
+ handler: EventHandlerCallable | UntypedEventHandlerFunction,
660
+ options: Partial<EventHandler> = {}
661
+ ): EventHandler {
662
+ const normalized_key = normalizeEventPattern(event_pattern) // get string event_type or '*'
663
+ const handler_name = EventHandler.handlerNameFromCallable(handler as EventHandlerCallable)
664
+ const handler_entry = new EventHandler({
665
+ handler: handler as EventHandlerCallable,
666
+ handler_name,
667
+ handler_registered_at: monotonicDatetime(),
668
+ event_pattern: normalized_key,
669
+ eventbus_name: this.name,
670
+ eventbus_id: this.id,
671
+ ...options,
672
+ })
673
+ if (this.event_handler_detect_file_paths) {
674
+ // optionally perform (expensive) file path detection for the handler using Error().stack introspection
675
+ // makes logs much more useful for debugging, but is expensive to do if not needed
676
+ handler_entry._detectHandlerFilePath()
677
+ }
678
+
679
+ this.handlers.set(handler_entry.id, handler_entry)
680
+ const ids = this.handlers_by_key.get(handler_entry.event_pattern)
681
+ if (ids) ids.push(handler_entry.id)
682
+ else this.handlers_by_key.set(handler_entry.event_pattern, [handler_entry.id])
683
+ this.scheduleMicrotask(() => {
684
+ void this._onBusHandlersChange(handler_entry, true)
685
+ })
686
+ return handler_entry
687
+ }
688
+
689
+ off<T extends BaseEvent>(event_pattern: EventPattern<T> | '*', handler?: EventHandlerCallable<T> | string | EventHandler): void {
690
+ const normalized_key = normalizeEventPattern(event_pattern)
691
+ if (typeof handler === 'object' && handler instanceof EventHandler && handler.id !== undefined) {
692
+ handler = handler.id
693
+ }
694
+ const match_by_id = typeof handler === 'string'
695
+ for (const entry of this.handlers.values()) {
696
+ if (entry.event_pattern !== normalized_key) {
697
+ continue
698
+ }
699
+ const handler_id = entry.id
700
+ if (handler === undefined || (match_by_id ? handler_id === handler : entry.handler === (handler as EventHandlerCallable))) {
701
+ this.handlers.delete(handler_id)
702
+ this._removeIndexedHandler(entry.event_pattern, handler_id)
703
+ this.scheduleMicrotask(() => {
704
+ void this._onBusHandlersChange(entry, false)
705
+ })
706
+ }
707
+ }
708
+ }
709
+
710
+ emit<T extends BaseEvent>(event: T): T {
711
+ const original_event = event._event_original ?? event // if event is a bus-scoped proxy already, get the original underlying event object
712
+ if (!original_event.event_bus) {
713
+ // if we are the first bus to emit this event, set the event_bus property on the original event object
714
+ original_event.event_bus = this
715
+ }
716
+ if (!Array.isArray(original_event.event_path)) {
717
+ original_event.event_path = []
718
+ }
719
+ if (original_event._getDispatchContext() === undefined) {
720
+ // when used in fastify/nextjs/other contexts with tracing based on AsyncLocalStorage in node
721
+ // we want to capture the context at the emit site and use it when running handlers
722
+ // because events may be handled async in a separate context than the emit site
723
+ original_event._setDispatchContext(captureAsyncContext())
724
+ }
725
+ if (original_event.event_path.includes(this.label) || this._hasProcessedEvent(original_event)) {
726
+ return this._getEventProxyScopedToThisBus(original_event) as T
727
+ }
728
+
729
+ if (!original_event.event_path.includes(this.label)) {
730
+ original_event.event_path.push(this.label)
731
+ }
732
+
733
+ if (original_event.event_parent_id && original_event.event_emitted_by_handler_id) {
734
+ const parent_result = original_event.event_parent?.event_results.get(original_event.event_emitted_by_handler_id)
735
+ if (parent_result) {
736
+ parent_result._linkEmittedChildEvent(original_event)
737
+ }
738
+ }
739
+
740
+ if (
741
+ this.event_history.max_history_size !== null &&
742
+ this.event_history.max_history_size > 0 &&
743
+ !this.event_history.max_history_drop &&
744
+ this.event_history.size >= this.event_history.max_history_size
745
+ ) {
746
+ throw new Error(
747
+ `${this.toString()}.emit(${original_event.event_type}) rejected: history limit reached (${this.event_history.size}/${this.event_history.max_history_size}); set event_history.max_history_drop=true to drop old history instead.`
748
+ )
749
+ }
750
+
751
+ this.event_history.addEvent(original_event)
752
+ this.event_history.trimEventHistory({
753
+ is_event_complete: (candidate_event) => candidate_event.event_status === 'completed',
754
+ on_remove: (candidate_event) => candidate_event._gc(),
755
+ owner_label: this.toString(),
756
+ max_history_size: this.event_history.max_history_size,
757
+ max_history_drop: this.event_history.max_history_drop,
758
+ })
759
+ this._resolveFindWaiters(original_event)
760
+
761
+ original_event.event_pending_bus_count += 1
762
+ this.pending_event_queue.push(original_event)
763
+ this._startRunloop()
764
+
765
+ return this._getEventProxyScopedToThisBus(original_event) as T
766
+ }
767
+
768
+ // alias for emit
769
+ dispatch<T extends BaseEvent>(event: T): T {
770
+ return this.emit(event)
771
+ }
772
+
773
+ // find a recent event or wait for a future event that matches some criteria
774
+ find(event_pattern: '*', options?: FindOptions<BaseEvent>): Promise<BaseEvent | null>
775
+ find(event_pattern: '*', where: (event: BaseEvent) => boolean, options?: FindOptions<BaseEvent>): Promise<BaseEvent | null>
776
+ find<T extends BaseEvent>(event_pattern: EventPattern<T>, options?: FindOptions<T>): Promise<T | null>
777
+ find<T extends BaseEvent>(event_pattern: EventPattern<T>, where: (event: T) => boolean, options?: FindOptions<T>): Promise<T | null>
778
+ async find<T extends BaseEvent>(
779
+ event_pattern: EventPattern<T> | '*',
780
+ where_or_options: ((event: T) => boolean) | FindOptions<T> = {},
781
+ maybe_options: FindOptions<T> = {}
782
+ ): Promise<T | null> {
783
+ const where = typeof where_or_options === 'function' ? where_or_options : () => true
784
+ const options = typeof where_or_options === 'function' ? maybe_options : where_or_options
785
+ const match = await this.event_history.find(event_pattern as EventPattern<T> | '*', where, {
786
+ ...options,
787
+ event_is_child_of: (event, ancestor) => this.eventIsChildOf(event, ancestor),
788
+ wait_for_future_match: (normalized_event_pattern, matches, future) =>
789
+ this._waitForFutureMatch(normalized_event_pattern, matches, future),
790
+ })
791
+ if (!match) {
792
+ return null
793
+ }
794
+ return this._getEventProxyScopedToThisBus(match) as T
795
+ }
796
+
797
+ private async _waitForFutureMatch(
798
+ event_pattern: string | '*',
799
+ matches: (event: BaseEvent) => boolean,
800
+ future: boolean | number
801
+ ): Promise<BaseEvent | null> {
802
+ if (future === false) {
803
+ return null
804
+ }
805
+ return await new Promise<BaseEvent | null>((resolve) => {
806
+ const waiter: EphemeralFindEventHandler = {
807
+ event_pattern,
808
+ matches,
809
+ resolve: (event) => resolve(event),
810
+ }
811
+ if (future !== true) {
812
+ const timeout_ms = Math.max(0, Number(future)) * 1000
813
+ waiter.timeout_id = setTimeout(() => {
814
+ this.find_waiters.delete(waiter)
815
+ resolve(null)
816
+ }, timeout_ms)
817
+ }
818
+ this.find_waiters.add(waiter)
819
+ })
820
+ }
821
+
822
+ async waitUntilIdle(timeout: number | null = null): Promise<boolean> {
823
+ return await this.locks.waitForIdle(timeout)
824
+ }
825
+
826
+ // Weak idle check: only checks if handlers are idle, doesnt check that the queue is empty
827
+ isIdle(): boolean {
828
+ for (const event of this.event_history.values()) {
829
+ for (const result of event.event_results.values()) {
830
+ if (result.eventbus_id !== this.id) {
831
+ continue
832
+ }
833
+ if (result.status === 'pending' || result.status === 'started') {
834
+ return false
835
+ }
836
+ }
837
+ }
838
+ return true // no handlers are pending or started
839
+ }
840
+
841
+ // Stronger idle check: no queued work, no in-flight processing, _runloop not
842
+ // active, and no handlers pending/running for this bus.
843
+ isIdleAndQueueEmpty(): boolean {
844
+ return this.pending_event_queue.length === 0 && this.in_flight_event_ids.size === 0 && this.isIdle() && !this.runloop_running
845
+ }
846
+
847
+ eventIsChildOf(child_event: BaseEvent, parent_event: BaseEvent): boolean {
848
+ if (child_event.event_id === parent_event.event_id) {
849
+ return false
850
+ }
851
+
852
+ let current_parent_id = child_event.event_parent_id
853
+ while (current_parent_id) {
854
+ if (current_parent_id === parent_event.event_id) {
855
+ return true
856
+ }
857
+ const parent = this.event_history.get(current_parent_id)
858
+ if (!parent) {
859
+ return false
860
+ }
861
+ current_parent_id = parent.event_parent_id
862
+ }
863
+ return false
864
+ }
865
+
866
+ eventIsParentOf(parent_event: BaseEvent, child_event: BaseEvent): boolean {
867
+ return this.eventIsChildOf(child_event, parent_event)
868
+ }
869
+
870
+ // return a full detailed tree diagram of all events and results on this bus
871
+ logTree(): string {
872
+ return logTree(this)
873
+ }
874
+
875
+ // Resolve an event id from this bus first, then across all known buses.
876
+ findEventById(event_id: string): BaseEvent | null {
877
+ return this.event_history.get(event_id) ?? this.all_instances.findEventById(event_id)
878
+ }
879
+
880
+ private _startRunloop(): void {
881
+ if (this.runloop_running) {
882
+ return
883
+ }
884
+ this.runloop_running = true
885
+ this.scheduleMicrotask(() => {
886
+ void this._runloop()
887
+ })
888
+ }
889
+
890
+ // schedule the processing of an event on the event bus by its normal _runloop
891
+ // optionally using a pre-acquired lock if we're inside handling of a parent event
892
+ private async _processEvent(
893
+ event: BaseEvent,
894
+ options: {
895
+ bypass_event_locks?: boolean
896
+ pre_acquired_lock?: AsyncLock | null
897
+ } = {}
898
+ ): Promise<void> {
899
+ let pending_entries: Array<{
900
+ handler: EventHandler
901
+ result: EventResult
902
+ }> = []
903
+ try {
904
+ if (this._hasProcessedEvent(event)) {
905
+ return
906
+ }
907
+ const scoped_event = this._getEventProxyScopedToThisBus(event)
908
+ await this._onEventChange(scoped_event, 'pending')
909
+ event._markStarted()
910
+ pending_entries = event._createPendingHandlerResults(this)
911
+ const resolved_event_timeout = event.event_timeout ?? this.event_timeout
912
+ if (this.middlewares.length > 0) {
913
+ for (const entry of pending_entries) {
914
+ await this._onEventResultChange(scoped_event, entry.result, 'pending')
915
+ }
916
+ }
917
+ await this.locks._runWithEventLock(
918
+ event,
919
+ () =>
920
+ this._runHandlersWithTimeout(event, pending_entries, resolved_event_timeout, () =>
921
+ _runWithSlowMonitor(event._createSlowEventWarningTimer(), () => scoped_event._runHandlers(pending_entries))
922
+ ),
923
+ options
924
+ )
925
+ this._markEventCompletedIfNeeded(event)
926
+ } finally {
927
+ if (options.pre_acquired_lock) {
928
+ options.pre_acquired_lock.release()
929
+ }
930
+ this.in_flight_event_ids.delete(event.event_id)
931
+ this.locks._notifyIdleListeners()
932
+ }
933
+ }
934
+
935
+ // Called when a handler does `await child.done()` — processes the child event
936
+ // immediately ("queue-jump") instead of waiting for the _runloop to pick it up.
937
+ //
938
+ // Yield-and-reacquire: if the calling handler holds a handler concurrency lock,
939
+ // we temporarily release it so child handlers on the same bus can acquire it
940
+ // (preventing deadlock for serial handler mode). We re-acquire after
941
+ // the child completes so the parent handler can continue with the lock held.
942
+ async _processEventImmediately<T extends BaseEvent>(event: T, handler_result?: EventResult): Promise<T> {
943
+ const original_event = event._event_original ?? event
944
+ // Find the handler result for the current await call site. Proxy-provided
945
+ // context covers event.emit(...); async-local context lets awaited bus.emit(...)
946
+ // events queue-jump without implicitly changing their parentage.
947
+ const proxy_result = handler_result?.status === 'started' ? handler_result : undefined
948
+ const currently_active_event_result = proxy_result ?? this.locks._getActiveHandlerResultForCurrentAsyncContext()
949
+ if (!currently_active_event_result) {
950
+ // Not inside any handler scope — avoid queue-jump, but if this event is
951
+ // next in line we can process it immediately without waiting on the _runloop.
952
+ // We must acquire/revalidate the event lock first to avoid racing the runloop
953
+ // and accidentally reordering/removing the wrong queue head.
954
+ const queue_index = this.pending_event_queue.indexOf(original_event)
955
+ const can_process_now =
956
+ queue_index === 0 &&
957
+ !this.locks._isPaused() &&
958
+ !this.in_flight_event_ids.has(original_event.event_id) &&
959
+ !this._hasProcessedEvent(original_event)
960
+ if (can_process_now) {
961
+ const event_lock = this.locks.getLockForEvent(original_event)
962
+ let pre_acquired_lock: AsyncLock | null = null
963
+ if (event_lock) {
964
+ await event_lock.acquire()
965
+ pre_acquired_lock = event_lock
966
+ }
967
+ const queue_head = this.pending_event_queue[0]
968
+ const queue_head_original = queue_head?._event_original ?? queue_head
969
+ const still_can_process_now =
970
+ queue_head_original === original_event &&
971
+ !this.locks._isPaused() &&
972
+ !this.in_flight_event_ids.has(original_event.event_id) &&
973
+ !this._hasProcessedEvent(original_event)
974
+ if (still_can_process_now) {
975
+ this.pending_event_queue.shift()
976
+ this.in_flight_event_ids.add(original_event.event_id)
977
+ await this._processEvent(original_event, {
978
+ bypass_event_locks: true,
979
+ pre_acquired_lock,
980
+ })
981
+ if (original_event.event_status !== 'completed') {
982
+ await original_event.eventCompleted()
983
+ }
984
+ return event
985
+ }
986
+ if (pre_acquired_lock) {
987
+ pre_acquired_lock.release()
988
+ }
989
+ }
990
+ await original_event.eventCompleted()
991
+ return event
992
+ }
993
+
994
+ const active_parent = currently_active_event_result.event._event_original ?? currently_active_event_result.event
995
+ const is_child_of_active_handler =
996
+ original_event.event_parent_id === active_parent.event_id &&
997
+ original_event.event_emitted_by_handler_id === currently_active_event_result.handler_id &&
998
+ currently_active_event_result.event_children.some((child) => (child._event_original ?? child).event_id === original_event.event_id)
999
+ if (is_child_of_active_handler) {
1000
+ original_event.event_blocks_parent_completion = true
1001
+ }
1002
+
1003
+ // ensure a pause request is set so the bus _runloop pauses and (will resume when the handler exits)
1004
+ currently_active_event_result._ensureQueueJumpPause(this)
1005
+ if (original_event.event_status === 'completed') {
1006
+ return event
1007
+ }
1008
+
1009
+ // re-endter event-level handler lock if needed
1010
+ if (currently_active_event_result._lock) {
1011
+ await currently_active_event_result._lock.runQueueJump(this._processEventImmediatelyAcrossBuses.bind(this, original_event))
1012
+ return event
1013
+ }
1014
+
1015
+ await this._processEventImmediatelyAcrossBuses(original_event)
1016
+ return event
1017
+ }
1018
+
1019
+ // Processes a queue-jumped event across all buses that have it emitted.
1020
+ // Called from _processEventImmediately after the parent handler's lock has been yielded.
1021
+ private async _processEventImmediatelyAcrossBuses(event: BaseEvent): Promise<void> {
1022
+ // Use event_path ordering to pick candidate buses and filter out buses that
1023
+ // haven't seen the event or already processed it.
1024
+ const ordered: EventBus[] = []
1025
+ const seen = new Set<EventBus>()
1026
+ const event_path = Array.isArray(event.event_path) ? event.event_path : []
1027
+ for (const label of event_path) {
1028
+ for (const bus of this.all_instances) {
1029
+ if (bus.label !== label) {
1030
+ continue
1031
+ }
1032
+ if (!bus.event_history.has(event.event_id)) {
1033
+ continue
1034
+ }
1035
+ if (bus._hasProcessedEvent(event)) {
1036
+ continue
1037
+ }
1038
+ if (!seen.has(bus)) {
1039
+ ordered.push(bus)
1040
+ seen.add(bus)
1041
+ }
1042
+ }
1043
+ }
1044
+ if (!seen.has(this) && this.event_history.has(event.event_id)) {
1045
+ ordered.push(this)
1046
+ }
1047
+ if (ordered.length === 0) {
1048
+ await event.eventCompleted()
1049
+ return
1050
+ }
1051
+
1052
+ // Determine which event lock the initiating bus resolves to, so we can
1053
+ // detect when other buses share the same instance (global-serial).
1054
+ const initiating_event_lock = this.locks.getLockForEvent(event)
1055
+ const pause_releases: Array<() => void> = []
1056
+
1057
+ try {
1058
+ for (const bus of ordered) {
1059
+ if (bus !== this) {
1060
+ pause_releases.push(bus.locks._requestRunloopPause())
1061
+ }
1062
+ }
1063
+
1064
+ for (const bus of ordered) {
1065
+ const index = bus.pending_event_queue.indexOf(event)
1066
+ if (index >= 0) {
1067
+ bus.pending_event_queue.splice(index, 1)
1068
+ }
1069
+ if (bus._hasProcessedEvent(event)) {
1070
+ continue
1071
+ }
1072
+ if (bus.in_flight_event_ids.has(event.event_id)) {
1073
+ continue
1074
+ }
1075
+ bus.in_flight_event_ids.add(event.event_id)
1076
+
1077
+ // Bypass event lock on the initiating bus (we're already inside a handler
1078
+ // that acquired it). For other buses, only bypass if they resolve to the same
1079
+ // lock instance (global-serial shares one lock across all buses).
1080
+ const bus_event_lock = bus.locks.getLockForEvent(event)
1081
+ const should_bypass_event_lock = bus === this || (initiating_event_lock !== null && bus_event_lock === initiating_event_lock)
1082
+
1083
+ await bus._processEvent(event, {
1084
+ bypass_event_locks: should_bypass_event_lock,
1085
+ })
1086
+ }
1087
+
1088
+ if (event.event_status !== 'completed') {
1089
+ await event.eventCompleted()
1090
+ }
1091
+ } finally {
1092
+ for (const release of pause_releases) {
1093
+ release()
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ private async _runloop(): Promise<void> {
1099
+ for (;;) {
1100
+ while (this.pending_event_queue.length > 0) {
1101
+ await Promise.resolve()
1102
+ if (this.locks._isPaused()) {
1103
+ await this.locks._waitUntilRunloopResumed()
1104
+ continue
1105
+ }
1106
+ const next_event = this.pending_event_queue[0]
1107
+ if (!next_event) {
1108
+ continue
1109
+ }
1110
+ const original_event = next_event._event_original ?? next_event
1111
+ if (this._hasProcessedEvent(original_event)) {
1112
+ this.pending_event_queue.shift()
1113
+ continue
1114
+ }
1115
+ let pre_acquired_lock: AsyncLock | null = null
1116
+ const event_lock = this.locks.getLockForEvent(original_event)
1117
+ if (event_lock) {
1118
+ await event_lock.acquire()
1119
+ pre_acquired_lock = event_lock
1120
+ }
1121
+ // Queue head may have changed while waiting for the lock
1122
+ // (e.g. done() processing the head immediately). Revalidate
1123
+ // before mutating the queue to avoid removing the wrong event.
1124
+ const current_head = this.pending_event_queue[0]
1125
+ const current_head_original = current_head?._event_original ?? current_head
1126
+ if (current_head_original !== original_event) {
1127
+ if (pre_acquired_lock) {
1128
+ pre_acquired_lock.release()
1129
+ }
1130
+ continue
1131
+ }
1132
+ this.pending_event_queue.shift()
1133
+ if (this.in_flight_event_ids.has(original_event.event_id)) {
1134
+ if (pre_acquired_lock) {
1135
+ pre_acquired_lock.release()
1136
+ }
1137
+ continue
1138
+ }
1139
+ this.in_flight_event_ids.add(original_event.event_id)
1140
+ void this._processEvent(original_event, {
1141
+ bypass_event_locks: true,
1142
+ pre_acquired_lock,
1143
+ })
1144
+ await Promise.resolve()
1145
+ }
1146
+ this.runloop_running = false
1147
+ if (this.pending_event_queue.length > 0) {
1148
+ this._startRunloop()
1149
+ return
1150
+ }
1151
+ this.locks._notifyIdleListeners()
1152
+ return
1153
+ }
1154
+ }
1155
+
1156
+ // check if an event has been processed (and completed) by this bus
1157
+ _hasProcessedEvent(event: BaseEvent): boolean {
1158
+ const results = Array.from(event.event_results.values()).filter((result) => result.eventbus_id === this.id)
1159
+ if (results.length === 0) {
1160
+ return false
1161
+ }
1162
+ return results.every((result) => result.status === 'completed' || result.status === 'error')
1163
+ }
1164
+
1165
+ // get a proxy wrapper around an Event that will automatically link emitted child events to this bus and handler
1166
+ // proxy is what gets passed into the handler, if handler does event.emit(...) to dispatch child events,
1167
+ // the proxy auto-sets event.parent_event_id and event.event_emitted_by_handler_id
1168
+ _getEventProxyScopedToThisBus<T extends BaseEvent>(event: T, handler_result?: EventResult): T {
1169
+ const original_event = event._event_original ?? event
1170
+ const bus = this
1171
+ const parent_event_id = original_event.event_id
1172
+ const bus_proxy = new Proxy(bus, {
1173
+ get(target, prop, receiver) {
1174
+ if (prop === '_processEventImmediately') {
1175
+ const runner = Reflect.get(target, prop, receiver) as EventBus['_processEventImmediately']
1176
+ const process_event_immediately = <TChild extends BaseEvent>(child_event: TChild): Promise<TChild> => {
1177
+ return runner.call(target, child_event, handler_result) as Promise<TChild>
1178
+ }
1179
+ return process_event_immediately
1180
+ }
1181
+ if (prop === 'dispatch' || prop === 'emit') {
1182
+ const emit_child_event = <TChild extends BaseEvent>(child_event: TChild): TChild => {
1183
+ const original_child = child_event._event_original ?? child_event
1184
+ if (handler_result) {
1185
+ handler_result._linkEmittedChildEvent(original_child)
1186
+ } else if (!original_child.event_parent_id && original_child.event_id !== parent_event_id) {
1187
+ // fallback for non-handler scoped emit/dispatch
1188
+ original_child.event_parent_id = parent_event_id
1189
+ }
1190
+ const dispatcher = Reflect.get(target, prop, receiver) as EventBus['dispatch']
1191
+ const dispatched = dispatcher.call(target, original_child)
1192
+ return target._getEventProxyScopedToThisBus(dispatched as TChild, handler_result)
1193
+ }
1194
+ return emit_child_event
1195
+ }
1196
+ return Reflect.get(target, prop, receiver)
1197
+ },
1198
+ })
1199
+ const scoped = new Proxy(original_event, {
1200
+ get(target, prop, receiver) {
1201
+ if (prop === 'event_bus') {
1202
+ return bus_proxy
1203
+ }
1204
+ if (prop === '_event_original') {
1205
+ return target
1206
+ }
1207
+ return Reflect.get(target, prop, receiver)
1208
+ },
1209
+ set(target, prop, value) {
1210
+ if (prop === 'event_bus') {
1211
+ return true
1212
+ }
1213
+ return Reflect.set(target, prop, value, target)
1214
+ },
1215
+ has(target, prop) {
1216
+ if (prop === 'event_bus') {
1217
+ return true
1218
+ }
1219
+ if (prop === '_event_original') {
1220
+ return true
1221
+ }
1222
+ return Reflect.has(target, prop)
1223
+ },
1224
+ })
1225
+
1226
+ return scoped as T
1227
+ }
1228
+
1229
+ private _resolveFindWaiters(event: BaseEvent): void {
1230
+ for (const waiter of Array.from(this.find_waiters)) {
1231
+ if ((waiter.event_pattern !== '*' && event.event_type !== waiter.event_pattern) || !waiter.matches(event)) {
1232
+ continue
1233
+ }
1234
+ if (waiter.timeout_id) {
1235
+ clearTimeout(waiter.timeout_id)
1236
+ }
1237
+ this.find_waiters.delete(waiter)
1238
+ waiter.resolve(event)
1239
+ }
1240
+ }
1241
+
1242
+ _getHandlersForEvent(event: BaseEvent): EventHandler[] {
1243
+ const handlers: EventHandler[] = []
1244
+ for (const key of [event.event_type, '*']) {
1245
+ const ids = this.handlers_by_key.get(key)
1246
+ if (!ids) continue
1247
+ for (const id of ids) {
1248
+ const entry = this.handlers.get(id)
1249
+ if (entry) handlers.push(entry)
1250
+ }
1251
+ }
1252
+ return handlers
1253
+ }
1254
+
1255
+ private _removeIndexedHandler(event_pattern: string | '*', handler_id: string): void {
1256
+ const ids = this.handlers_by_key.get(event_pattern)
1257
+ if (!ids) return
1258
+ const idx = ids.indexOf(handler_id)
1259
+ if (idx < 0) return
1260
+ ids.splice(idx, 1)
1261
+ if (ids.length === 0) this.handlers_by_key.delete(event_pattern)
1262
+ }
1263
+ }