abxbus 2.4.32 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +74 -51
  2. package/dist/cjs/BaseEvent.d.ts +45 -55
  3. package/dist/cjs/BaseEvent.js +350 -169
  4. package/dist/cjs/BaseEvent.js.map +3 -3
  5. package/dist/cjs/EventBus.d.ts +8 -1
  6. package/dist/cjs/EventBus.js +153 -85
  7. package/dist/cjs/EventBus.js.map +2 -2
  8. package/dist/cjs/EventHandler.d.ts +3 -3
  9. package/dist/cjs/EventHandler.js.map +1 -1
  10. package/dist/cjs/EventResult.js +16 -22
  11. package/dist/cjs/EventResult.js.map +2 -2
  12. package/dist/cjs/LockManager.d.ts +1 -0
  13. package/dist/cjs/LockManager.js +4 -1
  14. package/dist/cjs/LockManager.js.map +2 -2
  15. package/dist/cjs/base_event.d.ts +2 -2
  16. package/dist/cjs/bridge_ipc.d.ts +45 -0
  17. package/dist/cjs/event_handler.d.ts +1 -0
  18. package/dist/cjs/events_suck.js +1 -1
  19. package/dist/cjs/events_suck.js.map +2 -2
  20. package/dist/cjs/index.d.ts +1 -0
  21. package/dist/cjs/index.js.map +2 -2
  22. package/dist/cjs/middleware_otel_tracing.d.ts +49 -0
  23. package/dist/cjs/timing.js +1 -1
  24. package/dist/cjs/timing.js.map +2 -2
  25. package/dist/esm/BaseEvent.js +351 -170
  26. package/dist/esm/BaseEvent.js.map +3 -3
  27. package/dist/esm/EventBus.js +153 -85
  28. package/dist/esm/EventBus.js.map +2 -2
  29. package/dist/esm/EventHandler.js.map +1 -1
  30. package/dist/esm/EventResult.js +16 -22
  31. package/dist/esm/EventResult.js.map +2 -2
  32. package/dist/esm/LockManager.js +4 -1
  33. package/dist/esm/LockManager.js.map +2 -2
  34. package/dist/esm/events_suck.js +1 -1
  35. package/dist/esm/events_suck.js.map +2 -2
  36. package/dist/esm/index.js.map +2 -2
  37. package/dist/esm/timing.js +1 -1
  38. package/dist/esm/timing.js.map +2 -2
  39. package/dist/types/BaseEvent.d.ts +45 -55
  40. package/dist/types/EventBus.d.ts +8 -1
  41. package/dist/types/EventHandler.d.ts +3 -3
  42. package/dist/types/LockManager.d.ts +1 -0
  43. package/dist/types/base_event.d.ts +2 -2
  44. package/dist/types/bridge_ipc.d.ts +45 -0
  45. package/dist/types/event_handler.d.ts +1 -0
  46. package/dist/types/index.d.ts +1 -0
  47. package/dist/types/middleware_otel_tracing.d.ts +49 -0
  48. package/package.json +4 -3
  49. package/src/BaseEvent.ts +452 -219
  50. package/src/EventBus.ts +186 -99
  51. package/src/EventHandler.ts +3 -3
  52. package/src/EventResult.ts +18 -22
  53. package/src/LockManager.ts +5 -1
  54. package/src/events_suck.ts +1 -1
  55. package/src/index.ts +1 -0
  56. package/src/timing.ts +1 -1
package/src/EventBus.ts CHANGED
@@ -44,6 +44,10 @@ export type EventBusOptions = {
44
44
  middlewares?: EventBusMiddlewareInput[]
45
45
  }
46
46
 
47
+ export type EventBusDestroyOptions = {
48
+ clear?: boolean
49
+ }
50
+
47
51
  export type EventBusJSON = {
48
52
  id: string
49
53
  name: string
@@ -199,6 +203,7 @@ export class EventBus {
199
203
  locks: LockManager
200
204
  find_waiters: Set<EphemeralFindEventHandler> // set of EphemeralFindEventHandler objects that are waiting for a matching future event
201
205
  middlewares: EventBusMiddleware[]
206
+ private destroyed: boolean
202
207
 
203
208
  private static normalizeMiddlewares(middlewares?: EventBusMiddlewareInput[]): EventBusMiddleware[] {
204
209
  const normalized: EventBusMiddleware[] = []
@@ -224,9 +229,9 @@ export class EventBus {
224
229
  this.event_handler_concurrency = options.event_handler_concurrency ?? 'serial'
225
230
  this.event_handler_completion = options.event_handler_completion ?? 'all'
226
231
  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
232
+ this.event_timeout = options.event_timeout ?? 60
233
+ this.event_handler_slow_timeout = options.event_handler_slow_timeout ?? 30
234
+ this.event_slow_timeout = options.event_slow_timeout ?? 300
230
235
 
231
236
  // initialize runtime state
232
237
  this.runloop_running = false
@@ -241,6 +246,7 @@ export class EventBus {
241
246
  this.in_flight_event_ids = new Set()
242
247
  this.locks = new LockManager(this)
243
248
  this.middlewares = EventBus.normalizeMiddlewares(options.middlewares)
249
+ this.destroyed = false
244
250
 
245
251
  this.all_instances.add(this)
246
252
 
@@ -367,7 +373,7 @@ export class EventBus {
367
373
  fn: () => Promise<void>
368
374
  ): Promise<void> {
369
375
  try {
370
- if (event_timeout === null || pending_entries.length === 0) {
376
+ if (event_timeout === null || event_timeout <= 0 || pending_entries.length === 0) {
371
377
  await fn()
372
378
  } else {
373
379
  await _runWithTimeout(event_timeout, () => this._createEventTimeoutError(event, pending_entries, event_timeout), fn)
@@ -525,10 +531,11 @@ export class EventBus {
525
531
  }
526
532
  const bus = new EventBus(name, options)
527
533
 
528
- if (!record.handlers || typeof record.handlers !== 'object' || Array.isArray(record.handlers)) {
534
+ const raw_handlers = record.handlers ?? {}
535
+ if (!raw_handlers || typeof raw_handlers !== 'object' || Array.isArray(raw_handlers)) {
529
536
  throw new Error('EventBus.fromJSON(data) requires handlers as an id-keyed object')
530
537
  }
531
- for (const [handler_id, payload] of Object.entries(record.handlers as Record<string, unknown>)) {
538
+ for (const [handler_id, payload] of Object.entries(raw_handlers as Record<string, unknown>)) {
532
539
  if (!payload || typeof payload !== 'object') {
533
540
  continue
534
541
  }
@@ -542,11 +549,12 @@ export class EventBus {
542
549
  bus.handlers.set(parsed.id, parsed)
543
550
  }
544
551
 
545
- if (!record.handlers_by_key || typeof record.handlers_by_key !== 'object' || Array.isArray(record.handlers_by_key)) {
552
+ const raw_handlers_by_key = record.handlers_by_key ?? {}
553
+ if (!raw_handlers_by_key || typeof raw_handlers_by_key !== 'object' || Array.isArray(raw_handlers_by_key)) {
546
554
  throw new Error('EventBus.fromJSON(data) requires handlers_by_key as an object')
547
555
  }
548
556
  bus.handlers_by_key.clear()
549
- for (const [raw_key, raw_ids] of Object.entries(record.handlers_by_key as Record<string, unknown>)) {
557
+ for (const [raw_key, raw_ids] of Object.entries(raw_handlers_by_key as Record<string, unknown>)) {
550
558
  if (!Array.isArray(raw_ids)) {
551
559
  continue
552
560
  }
@@ -633,21 +641,51 @@ export class EventBus {
633
641
  return this.event_history.delete(event_id)
634
642
  }
635
643
 
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()
644
+ private _raiseIfDestroyed(): void {
645
+ if (this.destroyed) {
646
+ throw new Error(`${this.toString()} has been destroyed and cannot be used again`)
643
647
  }
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
648
  }
650
649
 
650
+ // destroy the event bus and all its state to allow for garbage collection
651
+ destroy(clear?: boolean): Promise<void>
652
+ destroy(options?: EventBusDestroyOptions): Promise<void>
653
+ destroy(clear_or_options: boolean | EventBusDestroyOptions = true): Promise<void> {
654
+ const clear = typeof clear_or_options === 'object' && clear_or_options !== null ? (clear_or_options.clear ?? true) : clear_or_options
655
+ if (this.destroyed) {
656
+ if (clear) {
657
+ this.handlers.clear()
658
+ this.handlers_by_key.clear()
659
+ this.event_history.clear()
660
+ this.middlewares.length = 0
661
+ }
662
+ return Promise.resolve()
663
+ }
664
+ const finish = (): void => {
665
+ this.destroyed = true
666
+ this.all_instances.discard(this)
667
+ this.runloop_running = false
668
+ for (const waiter of Array.from(this.find_waiters)) {
669
+ if (waiter.timeout_id) {
670
+ clearTimeout(waiter.timeout_id)
671
+ }
672
+ this.find_waiters.delete(waiter)
673
+ waiter.resolve(null)
674
+ }
675
+ this.pending_event_queue.length = 0
676
+ this.in_flight_event_ids.clear()
677
+ this.locks.clear()
678
+ if (!clear) {
679
+ return
680
+ }
681
+ this.handlers.clear()
682
+ this.handlers_by_key.clear()
683
+ this.event_history.clear()
684
+ this.middlewares.length = 0
685
+ }
686
+ finish()
687
+ return Promise.resolve()
688
+ }
651
689
  on<T extends BaseEvent>(event_pattern: EventClass<T>, handler: EventHandlerCallable<T>, options?: Partial<EventHandler>): EventHandler
652
690
  on<T extends BaseEvent>(
653
691
  event_pattern: string | '*',
@@ -659,6 +697,7 @@ export class EventBus {
659
697
  handler: EventHandlerCallable | UntypedEventHandlerFunction,
660
698
  options: Partial<EventHandler> = {}
661
699
  ): EventHandler {
700
+ this._raiseIfDestroyed()
662
701
  const normalized_key = normalizeEventPattern(event_pattern) // get string event_type or '*'
663
702
  const handler_name = EventHandler.handlerNameFromCallable(handler as EventHandlerCallable)
664
703
  const handler_entry = new EventHandler({
@@ -687,6 +726,7 @@ export class EventBus {
687
726
  }
688
727
 
689
728
  off<T extends BaseEvent>(event_pattern: EventPattern<T> | '*', handler?: EventHandlerCallable<T> | string | EventHandler): void {
729
+ this._raiseIfDestroyed()
690
730
  const normalized_key = normalizeEventPattern(event_pattern)
691
731
  if (typeof handler === 'object' && handler instanceof EventHandler && handler.id !== undefined) {
692
732
  handler = handler.id
@@ -708,7 +748,19 @@ export class EventBus {
708
748
  }
709
749
 
710
750
  emit<T extends BaseEvent>(event: T): T {
751
+ this._raiseIfDestroyed()
711
752
  const original_event = event._event_original ?? event // if event is a bus-scoped proxy already, get the original underlying event object
753
+ const current_result = this.locks._getRawActiveHandlerResultForCurrentAsyncContext()
754
+ if (
755
+ current_result &&
756
+ current_result.status !== 'pending' &&
757
+ current_result.status !== 'started' &&
758
+ (current_result.error instanceof EventHandlerTimeoutError ||
759
+ current_result.error instanceof EventHandlerCancelledError ||
760
+ current_result.error instanceof EventHandlerAbortedError)
761
+ ) {
762
+ return original_event as T
763
+ }
712
764
  if (!original_event.event_bus) {
713
765
  // if we are the first bus to emit this event, set the event_bus property on the original event object
714
766
  original_event.event_bus = this
@@ -760,7 +812,11 @@ export class EventBus {
760
812
 
761
813
  original_event.event_pending_bus_count += 1
762
814
  this.pending_event_queue.push(original_event)
763
- this._startRunloop()
815
+ if (this.locks.getLockForEvent(original_event) === null) {
816
+ this._startParallelEventTaskFromQueue(original_event)
817
+ } else {
818
+ this._startRunloop()
819
+ }
764
820
 
765
821
  return this._getEventProxyScopedToThisBus(original_event) as T
766
822
  }
@@ -806,6 +862,7 @@ export class EventBus {
806
862
  where_or_options: ((event: T) => boolean) | FilterOptions<T> = {},
807
863
  maybe_options: FilterOptions<T> = {}
808
864
  ): Promise<T[]> {
865
+ this._raiseIfDestroyed()
809
866
  const where = typeof where_or_options === 'function' ? where_or_options : () => true
810
867
  const options = typeof where_or_options === 'function' ? maybe_options : where_or_options
811
868
  const matches = await this.event_history.filter(event_pattern as EventPattern<T> | '*', where, {
@@ -843,6 +900,7 @@ export class EventBus {
843
900
  }
844
901
 
845
902
  async waitUntilIdle(timeout: number | null = null): Promise<boolean> {
903
+ this._raiseIfDestroyed()
846
904
  return await this.locks.waitForIdle(timeout)
847
905
  }
848
906
 
@@ -901,6 +959,7 @@ export class EventBus {
901
959
  }
902
960
 
903
961
  private _startRunloop(): void {
962
+ this._raiseIfDestroyed()
904
963
  if (this.runloop_running) {
905
964
  return
906
965
  }
@@ -910,6 +969,22 @@ export class EventBus {
910
969
  })
911
970
  }
912
971
 
972
+ private _startParallelEventTaskFromQueue(event: BaseEvent): void {
973
+ if (this.in_flight_event_ids.has(event.event_id)) {
974
+ return
975
+ }
976
+ const queue_index = this.pending_event_queue.indexOf(event)
977
+ if (queue_index >= 0) {
978
+ this.pending_event_queue.splice(queue_index, 1)
979
+ } else if (event.event_status === 'completed') {
980
+ return
981
+ }
982
+ this.in_flight_event_ids.add(event.event_id)
983
+ this.scheduleMicrotask(() => {
984
+ void this._processEvent(event)
985
+ })
986
+ }
987
+
913
988
  // schedule the processing of an event on the event bus by its normal _runloop
914
989
  // optionally using a pre-acquired lock if we're inside handling of a parent event
915
990
  private async _processEvent(
@@ -932,6 +1007,7 @@ export class EventBus {
932
1007
  event._markStarted()
933
1008
  pending_entries = event._createPendingHandlerResults(this)
934
1009
  const resolved_event_timeout = event.event_timeout ?? this.event_timeout
1010
+ const resolved_event_slow_timeout = event.event_slow_timeout ?? this.event_slow_timeout
935
1011
  if (this.middlewares.length > 0) {
936
1012
  for (const entry of pending_entries) {
937
1013
  await this._onEventResultChange(scoped_event, entry.result, 'pending')
@@ -941,7 +1017,9 @@ export class EventBus {
941
1017
  event,
942
1018
  () =>
943
1019
  this._runHandlersWithTimeout(event, pending_entries, resolved_event_timeout, () =>
944
- _runWithSlowMonitor(event._createSlowEventWarningTimer(), () => scoped_event._runHandlers(pending_entries))
1020
+ _runWithSlowMonitor(event._createSlowEventWarningTimer(resolved_event_slow_timeout, this.name), () =>
1021
+ scoped_event._runHandlers(pending_entries)
1022
+ )
945
1023
  ),
946
1024
  options
947
1025
  )
@@ -955,7 +1033,7 @@ export class EventBus {
955
1033
  }
956
1034
  }
957
1035
 
958
- // Called when a handler does `await child.done()` — processes the child event
1036
+ // Called when a handler does `await child.now()` — processes the child event
959
1037
  // immediately ("queue-jump") instead of waiting for the _runloop to pick it up.
960
1038
  //
961
1039
  // Yield-and-reacquire: if the calling handler holds a handler concurrency lock,
@@ -970,18 +1048,15 @@ export class EventBus {
970
1048
  const proxy_result = handler_result?.status === 'started' ? handler_result : undefined
971
1049
  const currently_active_event_result = proxy_result ?? this.locks._getActiveHandlerResultForCurrentAsyncContext()
972
1050
  if (!currently_active_event_result) {
973
- // Not inside any handler scope — avoid queue-jump, but if this event is
974
- // next in line we can process it immediately without waiting on the _runloop.
975
- // We must acquire/revalidate the event lock first to avoid racing the runloop
976
- // and accidentally reordering/removing the wrong queue head.
977
1051
  const queue_index = this.pending_event_queue.indexOf(original_event)
978
- const can_process_now =
1052
+ const event_lock = this.locks.getLockForEvent(original_event)
1053
+ const can_process_queue_head_normally =
979
1054
  queue_index === 0 &&
980
1055
  !this.locks._isPaused() &&
981
1056
  !this.in_flight_event_ids.has(original_event.event_id) &&
982
- !this._hasProcessedEvent(original_event)
983
- if (can_process_now) {
984
- const event_lock = this.locks.getLockForEvent(original_event)
1057
+ !this._hasProcessedEvent(original_event) &&
1058
+ (event_lock === null || event_lock.in_use === 0)
1059
+ if (can_process_queue_head_normally) {
985
1060
  let pre_acquired_lock: AsyncLock | null = null
986
1061
  if (event_lock) {
987
1062
  await event_lock.acquire()
@@ -989,12 +1064,12 @@ export class EventBus {
989
1064
  }
990
1065
  const queue_head = this.pending_event_queue[0]
991
1066
  const queue_head_original = queue_head?._event_original ?? queue_head
992
- const still_can_process_now =
1067
+ if (
993
1068
  queue_head_original === original_event &&
994
1069
  !this.locks._isPaused() &&
995
1070
  !this.in_flight_event_ids.has(original_event.event_id) &&
996
1071
  !this._hasProcessedEvent(original_event)
997
- if (still_can_process_now) {
1072
+ ) {
998
1073
  this.pending_event_queue.shift()
999
1074
  this.in_flight_event_ids.add(original_event.event_id)
1000
1075
  await this._processEvent(original_event, {
@@ -1002,15 +1077,13 @@ export class EventBus {
1002
1077
  pre_acquired_lock,
1003
1078
  })
1004
1079
  if (original_event.event_status !== 'completed') {
1005
- await original_event.eventCompleted()
1080
+ await this._processEventImmediatelyAcrossBuses(original_event)
1006
1081
  }
1007
1082
  return event
1008
1083
  }
1009
- if (pre_acquired_lock) {
1010
- pre_acquired_lock.release()
1011
- }
1084
+ pre_acquired_lock?.release()
1012
1085
  }
1013
- await original_event.eventCompleted()
1086
+ await this._processEventImmediatelyAcrossBuses(original_event)
1014
1087
  return event
1015
1088
  }
1016
1089
 
@@ -1047,78 +1120,83 @@ export class EventBus {
1047
1120
  // Processes a queue-jumped event across all buses that have it emitted.
1048
1121
  // Called from _processEventImmediately after the parent handler's lock has been yielded.
1049
1122
  private async _processEventImmediatelyAcrossBuses(event: BaseEvent): Promise<void> {
1050
- // Use event_path ordering to pick candidate buses and filter out buses that
1051
- // haven't seen the event or already processed it.
1052
- const ordered: EventBus[] = []
1053
- const seen = new Set<EventBus>()
1054
- const event_path = Array.isArray(event.event_path) ? event.event_path : []
1055
- for (const label of event_path) {
1056
- for (const bus of this.all_instances) {
1057
- if (bus.label !== label) {
1058
- continue
1059
- }
1060
- if (!bus.event_history.has(event.event_id)) {
1061
- continue
1062
- }
1063
- if (bus._hasProcessedEvent(event)) {
1064
- continue
1065
- }
1066
- if (!seen.has(bus)) {
1067
- ordered.push(bus)
1068
- seen.add(bus)
1069
- }
1070
- }
1071
- }
1072
- if (!seen.has(this) && this.event_history.has(event.event_id)) {
1073
- ordered.push(this)
1074
- }
1075
- if (ordered.length === 0) {
1076
- await event.eventCompleted()
1077
- return
1078
- }
1079
-
1080
1123
  // Determine which event lock the initiating bus resolves to, so we can
1081
1124
  // detect when other buses share the same instance (global-serial).
1082
1125
  const initiating_event_lock = this.locks.getLockForEvent(event)
1083
- const pause_releases: Array<() => void> = []
1084
1126
 
1085
- try {
1086
- for (const bus of ordered) {
1087
- if (bus !== this) {
1088
- pause_releases.push(bus.locks._requestRunloopPause())
1127
+ for (;;) {
1128
+ // Use event_path ordering to pick candidate buses and filter out buses
1129
+ // that haven't seen the event or already processed it. Forwarding
1130
+ // handlers can append new buses while this method is already running, so
1131
+ // this list must be rebuilt until the event is fully complete.
1132
+ const ordered: EventBus[] = []
1133
+ const seen = new Set<EventBus>()
1134
+ const event_path = Array.isArray(event.event_path) ? event.event_path : []
1135
+ for (const label of event_path) {
1136
+ for (const bus of this.all_instances) {
1137
+ if (bus.label !== label) {
1138
+ continue
1139
+ }
1140
+ if (!bus.event_history.has(event.event_id)) {
1141
+ continue
1142
+ }
1143
+ if (bus._hasProcessedEvent(event)) {
1144
+ continue
1145
+ }
1146
+ if (!seen.has(bus)) {
1147
+ ordered.push(bus)
1148
+ seen.add(bus)
1149
+ }
1089
1150
  }
1090
1151
  }
1152
+ if (!seen.has(this) && this.event_history.has(event.event_id) && !this._hasProcessedEvent(event)) {
1153
+ ordered.push(this)
1154
+ }
1091
1155
 
1092
- for (const bus of ordered) {
1093
- const index = bus.pending_event_queue.indexOf(event)
1094
- if (index >= 0) {
1095
- bus.pending_event_queue.splice(index, 1)
1096
- }
1097
- if (bus._hasProcessedEvent(event)) {
1098
- continue
1099
- }
1100
- if (bus.in_flight_event_ids.has(event.event_id)) {
1101
- continue
1156
+ const pause_releases: Array<() => void> = []
1157
+ let processed_bus = false
1158
+ try {
1159
+ for (const bus of ordered) {
1160
+ if (bus !== this) {
1161
+ pause_releases.push(bus.locks._requestRunloopPause())
1162
+ }
1102
1163
  }
1103
- bus.in_flight_event_ids.add(event.event_id)
1104
1164
 
1105
- // Bypass event lock on the initiating bus (we're already inside a handler
1106
- // that acquired it). For other buses, only bypass if they resolve to the same
1107
- // lock instance (global-serial shares one lock across all buses).
1108
- const bus_event_lock = bus.locks.getLockForEvent(event)
1109
- const should_bypass_event_lock = bus === this || (initiating_event_lock !== null && bus_event_lock === initiating_event_lock)
1165
+ for (const bus of ordered) {
1166
+ const index = bus.pending_event_queue.indexOf(event)
1167
+ if (index >= 0) {
1168
+ bus.pending_event_queue.splice(index, 1)
1169
+ }
1170
+ if (bus._hasProcessedEvent(event)) {
1171
+ continue
1172
+ }
1173
+ if (bus.in_flight_event_ids.has(event.event_id)) {
1174
+ continue
1175
+ }
1176
+ bus.in_flight_event_ids.add(event.event_id)
1177
+ processed_bus = true
1178
+
1179
+ // Bypass event lock on the initiating bus (we're already inside a handler
1180
+ // that acquired it). For other buses, only bypass if they resolve to the same
1181
+ // lock instance (global-serial shares one lock across all buses).
1182
+ const bus_event_lock = bus.locks.getLockForEvent(event)
1183
+ const should_bypass_event_lock = bus === this || (initiating_event_lock !== null && bus_event_lock === initiating_event_lock)
1110
1184
 
1111
- await bus._processEvent(event, {
1112
- bypass_event_locks: should_bypass_event_lock,
1113
- })
1185
+ await bus._processEvent(event, {
1186
+ bypass_event_locks: should_bypass_event_lock,
1187
+ })
1188
+ }
1189
+ } finally {
1190
+ for (const release of pause_releases) {
1191
+ release()
1192
+ }
1114
1193
  }
1115
1194
 
1116
- if (event.event_status !== 'completed') {
1117
- await event.eventCompleted()
1195
+ if (event.event_status === 'completed') {
1196
+ return
1118
1197
  }
1119
- } finally {
1120
- for (const release of pause_releases) {
1121
- release()
1198
+ if (!processed_bus) {
1199
+ await new Promise((resolve) => setTimeout(resolve, 1))
1122
1200
  }
1123
1201
  }
1124
1202
  }
@@ -1147,7 +1225,7 @@ export class EventBus {
1147
1225
  pre_acquired_lock = event_lock
1148
1226
  }
1149
1227
  // Queue head may have changed while waiting for the lock
1150
- // (e.g. done() processing the head immediately). Revalidate
1228
+ // (e.g. now() processing the head immediately). Revalidate
1151
1229
  // before mutating the queue to avoid removing the wrong event.
1152
1230
  const current_head = this.pending_event_queue[0]
1153
1231
  const current_head_original = current_head?._event_original ?? current_head
@@ -1209,7 +1287,16 @@ export class EventBus {
1209
1287
  if (prop === 'dispatch' || prop === 'emit') {
1210
1288
  const emit_child_event = <TChild extends BaseEvent>(child_event: TChild): TChild => {
1211
1289
  const original_child = child_event._event_original ?? child_event
1212
- if (handler_result) {
1290
+ const handler_result_is_terminal = handler_result && handler_result.status !== 'pending' && handler_result.status !== 'started'
1291
+ if (
1292
+ handler_result_is_terminal &&
1293
+ (handler_result.error instanceof EventHandlerTimeoutError ||
1294
+ handler_result.error instanceof EventHandlerCancelledError ||
1295
+ handler_result.error instanceof EventHandlerAbortedError)
1296
+ ) {
1297
+ return original_child as TChild
1298
+ }
1299
+ if (handler_result && !handler_result_is_terminal) {
1213
1300
  handler_result._linkEmittedChildEvent(original_child)
1214
1301
  } else if (!original_child.event_parent_id && original_child.event_id !== parent_event_id) {
1215
1302
  // fallback for non-handler scoped emit/dispatch
@@ -24,7 +24,7 @@ export type EphemeralFindEventHandler = {
24
24
  // Resolved on dispatch, ephemeral, and never shows up in the processing tree.
25
25
  event_pattern: string | '*'
26
26
  matches: (event: BaseEvent) => boolean
27
- resolve: (event: BaseEvent) => void
27
+ resolve: (event: BaseEvent | null) => void
28
28
  timeout_id?: ReturnType<typeof setTimeout>
29
29
  }
30
30
 
@@ -49,7 +49,7 @@ export class FindWaiter {
49
49
  data: unknown,
50
50
  overrides: {
51
51
  matches?: (event: BaseEvent) => boolean
52
- resolve?: (event: BaseEvent) => void
52
+ resolve?: (event: BaseEvent | null) => void
53
53
  } = {}
54
54
  ): EphemeralFindEventHandler {
55
55
  const record = FindWaiterJSONSchema.parse(data)
@@ -70,7 +70,7 @@ export class FindWaiter {
70
70
  data: unknown,
71
71
  overrides: {
72
72
  matches?: (event: BaseEvent) => boolean
73
- resolve?: (event: BaseEvent) => void
73
+ resolve?: (event: BaseEvent | null) => void
74
74
  } = {}
75
75
  ): EphemeralFindEventHandler[] {
76
76
  if (!Array.isArray(data)) {
@@ -25,8 +25,8 @@ export const EventResultJSONSchema = z
25
25
  handler_id: z.string(),
26
26
  handler_name: z.string(),
27
27
  handler_file_path: z.string().nullable().optional(),
28
- handler_timeout: z.number().nullable().optional(),
29
- handler_slow_timeout: z.number().nullable().optional(),
28
+ handler_timeout: z.number().nonnegative().nullable().optional(),
29
+ handler_slow_timeout: z.number().nonnegative().nullable().optional(),
30
30
  handler_registered_at: z.string().datetime().optional(),
31
31
  handler_event_pattern: z.union([z.string(), z.literal('*')]).optional(),
32
32
  eventbus_name: z.string(),
@@ -171,18 +171,24 @@ export class EventResult<TEvent extends BaseEvent = BaseEvent> {
171
171
  return this.result
172
172
  }
173
173
 
174
- // Resolve handler timeout in seconds using precedence: handler -> event -> bus defaults.
174
+ // Resolve handler timeout in seconds using event-local values plus the executing bus defaults.
175
175
  get handler_timeout(): number | null {
176
176
  const original = this.event._event_original ?? this.event
177
- const resolved_event_timeout = original.event_timeout ?? this.bus.event_timeout
177
+ const raw_event_timeout = original.event_timeout ?? this.bus.event_timeout
178
+ const resolved_event_timeout =
179
+ raw_event_timeout !== null && raw_event_timeout !== undefined && raw_event_timeout > 0 ? raw_event_timeout : null
178
180
 
179
181
  let resolved_handler_timeout: number | null
180
- if (this.handler.handler_timeout !== undefined) {
182
+ if (this.handler.handler_timeout !== undefined && this.handler.handler_timeout !== null) {
181
183
  resolved_handler_timeout = this.handler.handler_timeout
182
- } else if (original.event_handler_timeout !== undefined) {
184
+ } else if (original.event_handler_timeout !== undefined && original.event_handler_timeout !== null) {
183
185
  resolved_handler_timeout = original.event_handler_timeout
184
186
  } else {
185
- resolved_handler_timeout = this.bus.event_timeout
187
+ resolved_handler_timeout = resolved_event_timeout
188
+ }
189
+
190
+ if (resolved_handler_timeout !== null && resolved_handler_timeout <= 0) {
191
+ resolved_handler_timeout = null
186
192
  }
187
193
 
188
194
  if (resolved_handler_timeout === null && resolved_event_timeout === null) {
@@ -197,31 +203,21 @@ export class EventResult<TEvent extends BaseEvent = BaseEvent> {
197
203
  return Math.min(resolved_handler_timeout, resolved_event_timeout)
198
204
  }
199
205
 
200
- // Resolve slow handler warning threshold in seconds using precedence: handler -> event -> bus defaults.
206
+ // Resolve slow handler warning threshold in seconds using event-local values plus the executing bus defaults.
201
207
  get handler_slow_timeout(): number | null {
202
208
  const original = this.event._event_original ?? this.event
203
209
 
204
- if (this.handler.handler_slow_timeout !== undefined) {
210
+ if (this.handler.handler_slow_timeout !== undefined && this.handler.handler_slow_timeout !== null) {
205
211
  return this.handler.handler_slow_timeout
206
212
  }
207
- if (original.event_handler_slow_timeout !== undefined) {
208
- return original.event_handler_slow_timeout
209
- }
210
- const event_slow_timeout = (original as { event_slow_timeout?: number | null }).event_slow_timeout
211
- if (event_slow_timeout !== undefined) {
212
- return event_slow_timeout
213
- }
214
- if (this.bus?.event_handler_slow_timeout !== undefined) {
215
- return this.bus.event_handler_slow_timeout
216
- }
217
- return this.bus?.event_slow_timeout ?? null
213
+ return original.event_handler_slow_timeout ?? this.bus.event_handler_slow_timeout ?? null
218
214
  }
219
215
 
220
216
  // Create a slow-handler warning timer that logs if the handler runs too long.
221
217
  _createSlowHandlerWarningTimer(effective_timeout: number | null): ReturnType<typeof setTimeout> | null {
222
218
  const handler_warn_timeout = this.handler_slow_timeout
223
- const warn_ms = handler_warn_timeout === null ? null : handler_warn_timeout * 1000
224
- const should_warn = warn_ms !== null && (effective_timeout === null || effective_timeout * 1000 > warn_ms)
219
+ const warn_ms = handler_warn_timeout === null || handler_warn_timeout <= 0 ? null : handler_warn_timeout * 1000
220
+ const should_warn = warn_ms !== null && (effective_timeout === null || effective_timeout <= 0 || effective_timeout * 1000 > warn_ms)
225
221
  if (!should_warn || warn_ms === null) {
226
222
  return null
227
223
  }
@@ -246,10 +246,14 @@ export class LockManager {
246
246
  }
247
247
 
248
248
  _getActiveHandlerResultForCurrentAsyncContext(): EventResult | undefined {
249
- const result = handler_context_storage?.getStore() as EventResult | undefined
249
+ const result = this._getRawActiveHandlerResultForCurrentAsyncContext()
250
250
  return result?.status === 'started' ? result : undefined
251
251
  }
252
252
 
253
+ _getRawActiveHandlerResultForCurrentAsyncContext(): EventResult | undefined {
254
+ return handler_context_storage?.getStore() as EventResult | undefined
255
+ }
256
+
253
257
  _getActiveHandlerResults(): EventResult[] {
254
258
  return [...this.active_handler_results]
255
259
  }
@@ -75,7 +75,7 @@ export const wrap = <TEvents extends EventMap>(class_name: string, methods: TEve
75
75
  Object.defineProperty(WrappedClient.prototype, method_name, {
76
76
  value: async function (this: DynamicWrappedClient, init?: Record<string, unknown>, extra?: Record<string, unknown>) {
77
77
  const payload = { ...(init ?? {}), ...(extra ?? {}) }
78
- return await this.bus.emit(new EventCtor(payload)).first()
78
+ return await this.bus.emit(new EventCtor(payload)).now({ first_result: true }).eventResult()
79
79
  },
80
80
  writable: true,
81
81
  configurable: true,
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { BaseEvent, BaseEventSchema } from './BaseEvent.js'
2
+ export type { EventResultInclude, EventResultOptions, EventWaitOptions, EventWaitPromise } from './BaseEvent.js'
2
3
  export { EventHistory } from './EventHistory.js'
3
4
  export type { EventHistoryFilterOptions, EventHistoryFindOptions, EventHistoryTrimOptions } from './EventHistory.js'
4
5
  export { EventResult } from './EventResult.js'
package/src/timing.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export async function _runWithTimeout<T>(timeout_seconds: number | null, on_timeout: () => Error, fn: () => Promise<T>): Promise<T> {
2
2
  const task = Promise.resolve().then(fn)
3
- if (timeout_seconds === null) {
3
+ if (timeout_seconds === null || timeout_seconds <= 0) {
4
4
  return await task
5
5
  }
6
6
  const timeout_ms = timeout_seconds * 1000