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,1201 @@
1
+ import { z } from 'zod'
2
+ import { v7 as uuidv7 } from 'uuid'
3
+
4
+ import { EventBus } from './event_bus.js'
5
+ import { EventResult } from './event_result.js'
6
+ import { EventHandler, EventHandlerAbortedError, EventHandlerCancelledError, EventHandlerTimeoutError } from './event_handler.js'
7
+ import type { EventConcurrencyMode, EventHandlerConcurrencyMode, EventHandlerCompletionMode, Deferred } from './lock_manager.js'
8
+ import {
9
+ AsyncLock,
10
+ EVENT_CONCURRENCY_MODES,
11
+ EVENT_HANDLER_CONCURRENCY_MODES,
12
+ EVENT_HANDLER_COMPLETION_MODES,
13
+ withResolvers,
14
+ } from './lock_manager.js'
15
+ import { _runWithTimeout } from './timing.js'
16
+ import { extractZodShape, normalizeEventResultType, toJsonSchema } from './types.js'
17
+ import type { EventHandlerCallable, EventResultType } from './types.js'
18
+ import { monotonicDatetime } from './helpers.js'
19
+
20
+ const RESERVED_USER_EVENT_FIELDS = new Set(['bus', 'emit', 'first', 'toString', 'toJSON', 'fromJSON'])
21
+
22
+ function assertNoReservedUserEventFields(data: Record<string, unknown>, context: string): void {
23
+ for (const field_name of RESERVED_USER_EVENT_FIELDS) {
24
+ if (Object.prototype.hasOwnProperty.call(data, field_name)) {
25
+ throw new Error(`${context} field "${field_name}" is reserved for EventBus runtime context and cannot be set in event payload`)
26
+ }
27
+ }
28
+ }
29
+
30
+ function assertNoUnknownEventPrefixedFields(data: Record<string, unknown>, context: string): void {
31
+ for (const field_name of Object.keys(data)) {
32
+ if (field_name.startsWith('event_') && !KNOWN_BASE_EVENT_FIELDS.has(field_name)) {
33
+ throw new Error(`${context} field "${field_name}" starts with "event_" but is not a recognized BaseEvent field`)
34
+ }
35
+ }
36
+ }
37
+
38
+ function assertNoModelPrefixedFields(data: Record<string, unknown>, context: string): void {
39
+ for (const field_name of Object.keys(data)) {
40
+ if (field_name.startsWith('model_')) {
41
+ throw new Error(`${context} field "${field_name}" starts with "model_" and is reserved for model internals`)
42
+ }
43
+ }
44
+ }
45
+
46
+ function compareIsoDatetime(left: string | null | undefined, right: string | null | undefined): number {
47
+ const left_value = left ?? ''
48
+ const right_value = right ?? ''
49
+ if (left_value === right_value) {
50
+ return 0
51
+ }
52
+ return left_value < right_value ? -1 : 1
53
+ }
54
+
55
+ export const BaseEventSchema = z
56
+ .object({
57
+ event_id: z.string().uuid(),
58
+ event_created_at: z.string().datetime(),
59
+ event_type: z.string(),
60
+ event_version: z.string().default('0.0.1'),
61
+ event_timeout: z.number().positive().nullable(),
62
+ event_slow_timeout: z.number().positive().nullable().optional(),
63
+ event_handler_timeout: z.number().positive().nullable().optional(),
64
+ event_handler_slow_timeout: z.number().positive().nullable().optional(),
65
+ event_blocks_parent_completion: z.boolean().optional(),
66
+ event_parent_id: z.string().uuid().nullable().optional(),
67
+ event_path: z.array(z.string()).optional(),
68
+ event_result_type: z.unknown().optional(),
69
+ event_emitted_by_handler_id: z.string().uuid().nullable().optional(),
70
+ event_pending_bus_count: z.number().nonnegative().optional(),
71
+ event_status: z.enum(['pending', 'started', 'completed']).optional(),
72
+ event_started_at: z.string().datetime().nullable().optional(),
73
+ event_completed_at: z.string().datetime().nullable().optional(),
74
+ event_results: z.array(z.unknown()).optional(),
75
+ event_concurrency: z.enum(EVENT_CONCURRENCY_MODES).nullable().optional(),
76
+ event_handler_concurrency: z.enum(EVENT_HANDLER_CONCURRENCY_MODES).nullable().optional(),
77
+ event_handler_completion: z.enum(EVENT_HANDLER_COMPLETION_MODES).nullable().optional(),
78
+ })
79
+ .loose()
80
+
81
+ const KNOWN_BASE_EVENT_FIELDS = new Set(Object.keys(BaseEventSchema.shape))
82
+
83
+ export type BaseEventData = z.infer<typeof BaseEventSchema>
84
+ export type BaseEventJSON = BaseEventData & Record<string, unknown>
85
+ type BaseEventFields = Pick<
86
+ BaseEventData,
87
+ | 'event_id'
88
+ | 'event_created_at'
89
+ | 'event_type'
90
+ | 'event_version'
91
+ | 'event_timeout'
92
+ | 'event_slow_timeout'
93
+ | 'event_handler_timeout'
94
+ | 'event_handler_slow_timeout'
95
+ | 'event_blocks_parent_completion'
96
+ | 'event_parent_id'
97
+ | 'event_path'
98
+ | 'event_result_type'
99
+ | 'event_emitted_by_handler_id'
100
+ | 'event_pending_bus_count'
101
+ | 'event_status'
102
+ | 'event_started_at'
103
+ | 'event_completed_at'
104
+ | 'event_results'
105
+ | 'event_concurrency'
106
+ | 'event_handler_concurrency'
107
+ | 'event_handler_completion'
108
+ >
109
+
110
+ export type BaseEventInit<TFields extends Record<string, unknown>> = TFields & Partial<BaseEventFields>
111
+
112
+ type BaseEventSchemaShape = typeof BaseEventSchema.shape
113
+
114
+ export type EventSchema<TShape extends z.ZodRawShape> = z.ZodObject<BaseEventSchemaShape & TShape>
115
+ type EventPayload<TShape extends z.ZodRawShape> = TShape extends Record<string, never> ? {} : z.infer<z.ZodObject<TShape>>
116
+
117
+ type EventInput<TShape extends z.ZodRawShape> = z.input<EventSchema<TShape>>
118
+ export type EventInit<TShape extends z.ZodRawShape> = Omit<EventInput<TShape>, keyof BaseEventFields> & Partial<BaseEventFields>
119
+
120
+ type EventWithResultSchema<TResult> = BaseEvent & { __event_result_type__?: TResult }
121
+
122
+ type ResultTypeFromEventResultTypeInput<TInput> = TInput extends z.ZodTypeAny
123
+ ? z.infer<TInput>
124
+ : TInput extends StringConstructor
125
+ ? string
126
+ : TInput extends NumberConstructor
127
+ ? number
128
+ : TInput extends BooleanConstructor
129
+ ? boolean
130
+ : TInput extends ArrayConstructor
131
+ ? unknown[]
132
+ : TInput extends ObjectConstructor
133
+ ? Record<string, unknown>
134
+ : unknown
135
+
136
+ type ResultSchemaFromShape<TShape> = TShape extends { event_result_type: infer S } ? ResultTypeFromEventResultTypeInput<S> : unknown
137
+ type EventResultsListInclude<TEvent extends BaseEvent> = (
138
+ result: EventResultType<TEvent> | undefined,
139
+ event_result: EventResult<TEvent>
140
+ ) => boolean
141
+ type EventResultsListOptions<TEvent extends BaseEvent> = {
142
+ timeout?: number | null
143
+ include?: EventResultsListInclude<TEvent>
144
+ raise_if_any?: boolean
145
+ raise_if_none?: boolean
146
+ }
147
+ type EventDoneOptions = {
148
+ raise_if_any?: boolean
149
+ }
150
+ type EventResultUpdateOptions<TEvent extends BaseEvent> = {
151
+ eventbus?: EventBus
152
+ status?: 'pending' | 'started' | 'completed' | 'error'
153
+ result?: EventResultType<TEvent> | BaseEvent | undefined
154
+ error?: unknown
155
+ }
156
+
157
+ const EVENT_CLASS_DEFAULTS = new WeakMap<Function, Record<string, unknown>>()
158
+ const ROOT_EVENTBUS_ID = '00000000-0000-0000-0000-000000000000'
159
+
160
+ export type EventFactory<TShape extends z.ZodRawShape, TResult = unknown> = {
161
+ (data: EventInit<TShape>): EventWithResultSchema<TResult> & EventPayload<TShape>
162
+ new (data: EventInit<TShape>): EventWithResultSchema<TResult> & EventPayload<TShape>
163
+ schema: EventSchema<TShape>
164
+ class?: new (data: EventInit<TShape>) => EventWithResultSchema<TResult> & EventPayload<TShape>
165
+ event_type?: string
166
+ event_version?: string
167
+ event_result_type?: z.ZodTypeAny
168
+ fromJSON?: (data: unknown) => EventWithResultSchema<TResult> & EventPayload<TShape>
169
+ }
170
+
171
+ type ZodShapeFrom<TShape extends Record<string, unknown>> = {
172
+ [K in keyof TShape as K extends 'event_result_type' ? never : TShape[K] extends z.ZodTypeAny ? K : never]: Extract<
173
+ TShape[K],
174
+ z.ZodTypeAny
175
+ >
176
+ }
177
+
178
+ export class BaseEvent {
179
+ // event metadata fields
180
+ event_id!: string // unique uuidv7 identifier for the event
181
+ event_created_at!: string
182
+ event_type!: string // should match the class name of the event, e.g. BaseEvent.extend("MyEvent").event_type === "MyEvent"
183
+ event_version!: string // event schema/version tag managed by callers for migration-friendly payload handling
184
+ event_timeout!: number | null // maximum time in seconds that the event is allowed to run before it is aborted
185
+ event_slow_timeout?: number | null // optional per-event slow warning threshold in seconds
186
+ event_handler_timeout?: number | null // optional per-event handler timeout override in seconds
187
+ event_handler_slow_timeout?: number | null // optional per-event slow handler warning threshold in seconds
188
+ event_blocks_parent_completion!: boolean // true only for children explicitly awaited via done()/eventCompleted()
189
+ event_parent_id!: string | null // id of the parent event that triggered this event, if this event was emitted during handling of another event, else null
190
+ event_path!: string[] // list of bus labels (name#id) that the event has been dispatched to, including the current bus
191
+ event_result_type?: z.ZodTypeAny // optional zod schema to enforce the shape of return values from handlers
192
+ event_results!: Map<string, EventResult<this>> // map of handler ids to EventResult objects for the event
193
+ event_emitted_by_handler_id!: string | null // if event was emitted inside a handler while it was running, this is set to the enclosing handler's handler id, else null
194
+ event_pending_bus_count!: number // number of buses that have accepted this event and not yet finished processing or removed it from their queues (for queue-jump processing)
195
+ event_status!: 'pending' | 'started' | 'completed' // processing status of the event as a whole, no separate 'error' state because events can not error, only individual handlers can
196
+ event_started_at!: string | null
197
+ event_completed_at!: string | null
198
+ event_concurrency?: EventConcurrencyMode | null // concurrency mode for the event as a whole in relation to other events
199
+ event_handler_concurrency?: EventHandlerConcurrencyMode | null // concurrency mode for the handlers within the event
200
+ event_handler_completion?: EventHandlerCompletionMode | null // completion strategy: 'all' (default) waits for every handler, 'first' returns earliest non-undefined result and cancels the rest
201
+
202
+ static event_type?: string // class name of the event, e.g. BaseEvent.extend("MyEvent").event_type === "MyEvent"
203
+ static event_version = '0.0.1'
204
+ static schema = BaseEventSchema // zod schema for the event data fields, used to parse and validate event data when creating a new event
205
+
206
+ // internal runtime state
207
+ event_bus?: EventBus // bus that dispatched this event, also used by event.emit(child)
208
+ _event_original?: BaseEvent // underlying event object that was dispatched, if this is a bus-scoped proxy wrapping it
209
+ _event_dispatch_context?: unknown | null // captured AsyncLocalStorage context at dispatch site, used to restore that context when running handlers
210
+
211
+ _event_completed_signal: Deferred<this> | null
212
+ _lock_for_event_handler: AsyncLock | null
213
+ constructor(data: BaseEventInit<Record<string, unknown>> = {}) {
214
+ assertNoReservedUserEventFields(data as Record<string, unknown>, 'BaseEvent')
215
+ assertNoUnknownEventPrefixedFields(data as Record<string, unknown>, 'BaseEvent')
216
+ assertNoModelPrefixedFields(data as Record<string, unknown>, 'BaseEvent')
217
+ const ctor = this.constructor as typeof BaseEvent & {
218
+ event_version?: string
219
+ event_result_type?: z.ZodTypeAny
220
+ }
221
+ const ctor_defaults = EVENT_CLASS_DEFAULTS.get(ctor) ?? {}
222
+ const merged_data = {
223
+ ...ctor_defaults,
224
+ ...data,
225
+ } as BaseEventInit<Record<string, unknown>>
226
+ const event_type = merged_data.event_type ?? ctor.event_type ?? ctor.name
227
+ const event_version = merged_data.event_version ?? ctor.event_version ?? '0.0.1'
228
+ const raw_event_result_type = merged_data.event_result_type ?? ctor.event_result_type
229
+ const event_result_type = normalizeEventResultType(raw_event_result_type)
230
+ const event_id = merged_data.event_id ?? uuidv7()
231
+ const event_created_at = monotonicDatetime(merged_data.event_created_at)
232
+ const event_timeout = merged_data.event_timeout ?? null
233
+ const event_blocks_parent_completion = merged_data.event_blocks_parent_completion ?? false
234
+
235
+ const base_data = {
236
+ ...merged_data,
237
+ event_id,
238
+ event_created_at,
239
+ event_type,
240
+ event_version,
241
+ event_timeout,
242
+ event_blocks_parent_completion,
243
+ event_result_type,
244
+ }
245
+
246
+ const schema = ctor.schema ?? BaseEventSchema
247
+ const parsed = schema.parse(base_data) as BaseEventData & Record<string, unknown>
248
+
249
+ Object.assign(this, parsed)
250
+
251
+ const parsed_path = (parsed as { event_path?: string[] }).event_path
252
+ this.event_path = Array.isArray(parsed_path) ? [...parsed_path] : []
253
+
254
+ // load event results from potentially raw objects from JSON to proper EventResult objects
255
+ this.event_results = hydrateEventResults(this, (parsed as { event_results?: unknown }).event_results)
256
+ this.event_pending_bus_count =
257
+ typeof (parsed as { event_pending_bus_count?: unknown }).event_pending_bus_count === 'number'
258
+ ? Math.max(0, Number((parsed as { event_pending_bus_count?: number }).event_pending_bus_count))
259
+ : 0
260
+ const parsed_status = (parsed as { event_status?: unknown }).event_status
261
+ this.event_status =
262
+ parsed_status === 'pending' || parsed_status === 'started' || parsed_status === 'completed' ? parsed_status : 'pending'
263
+
264
+ this.event_started_at =
265
+ parsed.event_started_at === null || parsed.event_started_at === undefined ? null : monotonicDatetime(parsed.event_started_at)
266
+ this.event_completed_at =
267
+ parsed.event_completed_at === null || parsed.event_completed_at === undefined ? null : monotonicDatetime(parsed.event_completed_at)
268
+ this.event_parent_id =
269
+ typeof (parsed as { event_parent_id?: unknown }).event_parent_id === 'string'
270
+ ? (parsed as { event_parent_id: string }).event_parent_id
271
+ : null
272
+ this.event_emitted_by_handler_id =
273
+ typeof (parsed as { event_emitted_by_handler_id?: unknown }).event_emitted_by_handler_id === 'string'
274
+ ? (parsed as { event_emitted_by_handler_id: string }).event_emitted_by_handler_id
275
+ : null
276
+
277
+ this.event_result_type = event_result_type
278
+
279
+ this._event_completed_signal = null
280
+ this._lock_for_event_handler = null
281
+ this._event_dispatch_context = undefined
282
+ }
283
+
284
+ // "MyEvent#a48f"
285
+ toString(): string {
286
+ return `${this.event_type}#${this.event_id.slice(-4)}`
287
+ }
288
+
289
+ // main entry point for users to define their own event types
290
+ // BaseEvent.extend("MyEvent", { some_custom_field: z.string(), event_result_type: z.string(), event_timeout: 25, ... }) -> MyEvent
291
+ static extend<TShape extends z.ZodRawShape>(event_type: string, shape?: TShape): EventFactory<TShape, ResultSchemaFromShape<TShape>>
292
+ static extend<TShape extends Record<string, unknown>>(
293
+ event_type: string,
294
+ shape?: TShape
295
+ ): EventFactory<ZodShapeFrom<TShape>, ResultSchemaFromShape<TShape>>
296
+ static extend<TShape extends Record<string, unknown>>(
297
+ event_type: string,
298
+ shape: TShape = {} as TShape
299
+ ): EventFactory<ZodShapeFrom<TShape>, ResultSchemaFromShape<TShape>> {
300
+ const raw_shape = shape as Record<string, unknown>
301
+ assertNoReservedUserEventFields(raw_shape, `BaseEvent.extend(${event_type})`)
302
+ assertNoUnknownEventPrefixedFields(raw_shape, `BaseEvent.extend(${event_type})`)
303
+ assertNoModelPrefixedFields(raw_shape, `BaseEvent.extend(${event_type})`)
304
+ const raw_event_result_type = raw_shape.event_result_type
305
+ const event_result_type = normalizeEventResultType(raw_event_result_type)
306
+ const event_version = typeof raw_shape.event_version === 'string' ? raw_shape.event_version : undefined
307
+ const event_defaults = Object.fromEntries(
308
+ Object.entries(raw_shape).filter(
309
+ ([key, value]) => key !== 'event_result_type' && key !== 'event_version' && !(value instanceof z.ZodType)
310
+ )
311
+ )
312
+
313
+ const zod_shape = extractZodShape(raw_shape)
314
+ const full_schema = BaseEventSchema.extend(zod_shape)
315
+
316
+ // create a new event class that extends BaseEvent and adds the custom fields
317
+ class ExtendedEvent extends BaseEvent {
318
+ static schema = full_schema as unknown as typeof BaseEvent.schema
319
+ static event_type = event_type
320
+ static event_version = event_version ?? BaseEvent.event_version
321
+ static event_result_type = event_result_type
322
+
323
+ constructor(data: EventInit<ZodShapeFrom<TShape>>) {
324
+ super(data as BaseEventInit<Record<string, unknown>>)
325
+ }
326
+ }
327
+
328
+ type FactoryResult = EventWithResultSchema<ResultSchemaFromShape<TShape>> & EventPayload<ZodShapeFrom<TShape>>
329
+
330
+ function EventFactory(data: EventInit<ZodShapeFrom<TShape>>): FactoryResult {
331
+ return new ExtendedEvent(data) as FactoryResult
332
+ }
333
+
334
+ EventFactory.schema = full_schema as EventSchema<ZodShapeFrom<TShape>>
335
+ EventFactory.event_type = event_type
336
+ EventFactory.event_version = event_version ?? BaseEvent.event_version
337
+ EventFactory.event_result_type = event_result_type
338
+ EventFactory.class = ExtendedEvent as unknown as new (
339
+ data: EventInit<ZodShapeFrom<TShape>>
340
+ ) => EventWithResultSchema<ResultSchemaFromShape<TShape>> & EventPayload<ZodShapeFrom<TShape>>
341
+ EventFactory.fromJSON = (data: unknown) => (ExtendedEvent.fromJSON as (data: unknown) => FactoryResult)(data)
342
+ EventFactory.prototype = ExtendedEvent.prototype
343
+ EVENT_CLASS_DEFAULTS.set(ExtendedEvent, event_defaults)
344
+
345
+ return EventFactory as unknown as EventFactory<ZodShapeFrom<TShape>, ResultSchemaFromShape<TShape>>
346
+ }
347
+
348
+ static fromJSON<T extends typeof BaseEvent>(this: T, data: unknown): InstanceType<T> {
349
+ if (!data || typeof data !== 'object') {
350
+ const schema = this.schema ?? BaseEventSchema
351
+ const parsed = schema.parse(data)
352
+ return new this(parsed) as InstanceType<T>
353
+ }
354
+ const record = { ...(data as Record<string, unknown>) }
355
+ if (record.event_result_type !== undefined && record.event_result_type !== null) {
356
+ record.event_result_type = normalizeEventResultType(record.event_result_type)
357
+ }
358
+ return new this(record as BaseEventInit<Record<string, unknown>>) as InstanceType<T>
359
+ }
360
+
361
+ static toJSONArray(events: Iterable<BaseEvent>): BaseEventJSON[] {
362
+ return Array.from(events, (event) => {
363
+ const original = event._event_original ?? event
364
+ return original.toJSON()
365
+ })
366
+ }
367
+
368
+ static fromJSONArray(data: unknown): BaseEvent[] {
369
+ if (!Array.isArray(data)) {
370
+ return []
371
+ }
372
+ return data.map((item) => BaseEvent.fromJSON(item))
373
+ }
374
+
375
+ toJSON(): BaseEventJSON {
376
+ const record: Record<string, unknown> = {}
377
+ for (const [key, value] of Object.entries(this as unknown as Record<string, unknown>)) {
378
+ if (key.startsWith('_') || key === 'bus' || key === 'event_bus' || key === 'event_results') continue
379
+ if (value === undefined || typeof value === 'function') continue
380
+ record[key] = value
381
+ }
382
+ const event_results = Array.from(this.event_results.values()).map((result) => result.toJSON())
383
+
384
+ return {
385
+ ...record,
386
+ event_id: this.event_id,
387
+ event_type: this.event_type,
388
+ event_version: this.event_version,
389
+ event_result_type: this.event_result_type ? toJsonSchema(this.event_result_type) : this.event_result_type,
390
+
391
+ // static configuration options
392
+ event_timeout: this.event_timeout,
393
+ event_slow_timeout: this.event_slow_timeout,
394
+ event_concurrency: this.event_concurrency,
395
+ event_handler_concurrency: this.event_handler_concurrency,
396
+ event_handler_completion: this.event_handler_completion,
397
+ event_handler_slow_timeout: this.event_handler_slow_timeout,
398
+ event_handler_timeout: this.event_handler_timeout,
399
+ event_blocks_parent_completion: this.event_blocks_parent_completion,
400
+
401
+ // mutable parent/child/bus tracking runtime state
402
+ event_parent_id: this.event_parent_id,
403
+ event_path: this.event_path,
404
+ event_emitted_by_handler_id: this.event_emitted_by_handler_id,
405
+ event_pending_bus_count: this.event_pending_bus_count,
406
+
407
+ // mutable runtime status and timestamps
408
+ event_status: this.event_status,
409
+ event_created_at: this.event_created_at,
410
+ event_started_at: this.event_started_at ?? null,
411
+ event_completed_at: this.event_completed_at ?? null,
412
+
413
+ // mutable result state
414
+ ...(event_results.length > 0 ? { event_results } : {}),
415
+ }
416
+ }
417
+
418
+ _createSlowEventWarningTimer(): ReturnType<typeof setTimeout> | null {
419
+ const event_slow_timeout = this.event_slow_timeout ?? this.event_bus?.event_slow_timeout ?? null
420
+ const event_warn_ms = event_slow_timeout === null ? null : event_slow_timeout * 1000
421
+ if (event_warn_ms === null) {
422
+ return null
423
+ }
424
+ const name = this.event_bus?.name ?? 'EventBus'
425
+ return setTimeout(() => {
426
+ if (this.event_status === 'completed') {
427
+ return
428
+ }
429
+ const running_handler_count = [...this.event_results.values()].filter((result) => result.status === 'started').length
430
+ const started_at = this.event_started_at ?? this.event_created_at
431
+ const elapsed_ms = Math.max(0, Date.now() - Date.parse(started_at))
432
+ const elapsed_seconds = (elapsed_ms / 1000).toFixed(2)
433
+ console.warn(
434
+ `[abxbus] Slow event processing: ${name}.on(${this.event_type}#${this.event_id.slice(-4)}, ${running_handler_count} handlers) still running after ${elapsed_seconds}s`
435
+ )
436
+ }, event_warn_ms)
437
+ }
438
+
439
+ eventResultUpdate(handler: EventHandler | EventHandlerCallable<this>, options: EventResultUpdateOptions<this> = {}): EventResult<this> {
440
+ const original_event = (this._event_original ?? this) as this
441
+ let resolved_eventbus = options.eventbus
442
+ let handler_entry: EventHandler
443
+
444
+ if (handler instanceof EventHandler) {
445
+ handler_entry = handler
446
+ if (!resolved_eventbus && handler_entry.eventbus_id !== ROOT_EVENTBUS_ID && original_event.event_bus) {
447
+ resolved_eventbus =
448
+ original_event.event_bus.all_instances.findBusById(handler_entry.eventbus_id) ??
449
+ (original_event.event_bus.id === handler_entry.eventbus_id ? original_event.event_bus : undefined)
450
+ }
451
+ } else {
452
+ handler_entry = EventHandler.fromCallable({
453
+ handler,
454
+ event_pattern: original_event.event_type,
455
+ eventbus_name: resolved_eventbus?.name ?? 'EventBus',
456
+ eventbus_id: resolved_eventbus?.id ?? ROOT_EVENTBUS_ID,
457
+ })
458
+ }
459
+
460
+ const scoped_event = resolved_eventbus ? resolved_eventbus._getEventProxyScopedToThisBus(original_event) : original_event
461
+ const handler_id = handler_entry.id
462
+ const existing = original_event.event_results.get(handler_id)
463
+ const event_result: EventResult<this> =
464
+ existing ?? (new EventResult({ event: scoped_event as this, handler: handler_entry }) as EventResult<this>)
465
+ if (!existing) {
466
+ original_event.event_results.set(handler_id, event_result)
467
+ } else {
468
+ if (existing.event !== scoped_event) {
469
+ existing.event = scoped_event as this
470
+ }
471
+ if (existing.handler.id !== handler_entry.id) {
472
+ existing.handler = handler_entry
473
+ }
474
+ }
475
+
476
+ if (options.status !== undefined || options.result !== undefined || options.error !== undefined) {
477
+ const update_params: Parameters<EventResult<this>['update']>[0] = {}
478
+ if (options.status !== undefined) update_params.status = options.status
479
+ if (options.result !== undefined) update_params.result = options.result
480
+ if (options.error !== undefined) update_params.error = options.error
481
+ event_result.update(update_params)
482
+ if (event_result.status === 'started' && event_result.started_at !== null) {
483
+ original_event._markStarted(event_result.started_at, false)
484
+ }
485
+ if (options.status === 'pending' || options.status === 'started') {
486
+ original_event.event_completed_at = null
487
+ }
488
+ }
489
+
490
+ return event_result
491
+ }
492
+
493
+ _createPendingHandlerResults(bus: EventBus): Array<{
494
+ handler: EventHandler
495
+ result: EventResult
496
+ }> {
497
+ const original_event = this._event_original ?? this
498
+ const scoped_event = bus._getEventProxyScopedToThisBus(original_event)
499
+ const handlers = bus._getHandlersForEvent(original_event)
500
+ return handlers.map((entry) => {
501
+ const handler_id = entry.id
502
+ const existing = original_event.event_results.get(handler_id)
503
+ const result = existing ?? new EventResult({ event: scoped_event, handler: entry })
504
+ if (!existing) {
505
+ original_event.event_results.set(handler_id, result)
506
+ } else if (existing.event !== scoped_event) {
507
+ existing.event = scoped_event
508
+ }
509
+ return { handler: entry, result }
510
+ })
511
+ }
512
+
513
+ private _collectPendingResults(
514
+ original: BaseEvent,
515
+ pending_entries?: Array<{
516
+ handler: EventHandler
517
+ result: EventResult
518
+ }>
519
+ ): EventResult[] {
520
+ if (pending_entries) {
521
+ return pending_entries.map((entry) => entry.result)
522
+ }
523
+ if (!this.event_bus?.id) {
524
+ return Array.from(original.event_results.values())
525
+ }
526
+ return Array.from(original.event_results.values()).filter((result) => result.eventbus_id === this.event_bus!.id)
527
+ }
528
+
529
+ private _isFirstModeWinningResult(entry: EventResult): boolean {
530
+ return entry.status === 'completed' && entry.result !== undefined && entry.result !== null && !(entry.result instanceof BaseEvent)
531
+ }
532
+
533
+ private _markFirstModeWinnerIfNeeded(original: BaseEvent, entry: EventResult, first_state: { found: boolean }): void {
534
+ if (first_state.found || !this._isFirstModeWinningResult(entry)) {
535
+ return
536
+ }
537
+ first_state.found = true
538
+ original._markRemainingFirstModeResultCancelled(entry)
539
+ }
540
+
541
+ private async _runHandlerWithLock(original: BaseEvent, entry: EventResult): Promise<void> {
542
+ if (!this.event_bus) {
543
+ throw new Error('event has no bus attached')
544
+ }
545
+ await this.event_bus.locks._runWithHandlerLock(original, this.event_bus.event_handler_concurrency, async (handler_lock) => {
546
+ await entry.runHandler(handler_lock)
547
+ })
548
+ }
549
+
550
+ // Run all pending handler results for the current bus context.
551
+ async _runHandlers(
552
+ pending_entries?: Array<{
553
+ handler: EventHandler
554
+ result: EventResult
555
+ }>
556
+ ): Promise<void> {
557
+ const original = this._event_original ?? this
558
+ const pending_results = this._collectPendingResults(original, pending_entries)
559
+ if (pending_results.length === 0) {
560
+ return
561
+ }
562
+ const resolved_completion = original.event_handler_completion ?? this.event_bus?.event_handler_completion ?? 'all'
563
+ if (resolved_completion === 'first') {
564
+ if (original._getHandlerLock(this.event_bus?.event_handler_concurrency) !== null) {
565
+ for (const entry of pending_results) {
566
+ await this._runHandlerWithLock(original, entry)
567
+ if (!this._isFirstModeWinningResult(entry)) {
568
+ continue
569
+ }
570
+ original._markRemainingFirstModeResultCancelled(entry)
571
+ break
572
+ }
573
+ return
574
+ }
575
+ const first_state = { found: false }
576
+ const handler_promises = pending_results.map((entry) => this._runHandlerWithLock(original, entry))
577
+ const monitored = pending_results.map((entry, index) =>
578
+ handler_promises[index].then(() => {
579
+ this._markFirstModeWinnerIfNeeded(original, entry, first_state)
580
+ })
581
+ )
582
+ await Promise.all(monitored)
583
+ return
584
+ } else {
585
+ const handler_promises = pending_results.map((entry) => this._runHandlerWithLock(original, entry))
586
+ await Promise.all(handler_promises)
587
+ }
588
+ }
589
+
590
+ _getHandlerLock(default_concurrency?: EventHandlerConcurrencyMode): AsyncLock | null {
591
+ const original = this._event_original ?? this
592
+ const resolved = original.event_handler_concurrency ?? default_concurrency ?? original.event_bus?.event_handler_concurrency ?? 'serial'
593
+ if (resolved === 'parallel') {
594
+ return null
595
+ }
596
+ if (!original._lock_for_event_handler) {
597
+ original._lock_for_event_handler = new AsyncLock(1)
598
+ }
599
+ return original._lock_for_event_handler
600
+ }
601
+
602
+ _setHandlerLock(lock: AsyncLock | null): void {
603
+ const original = this._event_original ?? this
604
+ original._lock_for_event_handler = lock
605
+ }
606
+
607
+ _getDispatchContext(): unknown | null | undefined {
608
+ const original = this._event_original ?? this
609
+ return original._event_dispatch_context
610
+ }
611
+
612
+ _setDispatchContext(dispatch_context: unknown | null | undefined): void {
613
+ const original = this._event_original ?? this
614
+ original._event_dispatch_context = dispatch_context
615
+ }
616
+
617
+ // Get parent event object from event_parent_id (checks across all buses)
618
+ get event_parent(): BaseEvent | undefined {
619
+ const original = this._event_original ?? this
620
+ const parent_id = original.event_parent_id
621
+ if (!parent_id) {
622
+ return undefined
623
+ }
624
+ return original.event_bus?.findEventById(parent_id) ?? undefined
625
+ }
626
+
627
+ // get all direct children of this event
628
+ get event_children(): BaseEvent[] {
629
+ const children: BaseEvent[] = []
630
+ const seen = new Set<string>()
631
+ for (const result of this.event_results.values()) {
632
+ for (const child of result.event_children) {
633
+ if (!seen.has(child.event_id)) {
634
+ seen.add(child.event_id)
635
+ children.push(child)
636
+ }
637
+ }
638
+ }
639
+ return children
640
+ }
641
+
642
+ // get all children grandchildren etc. recursively
643
+ get event_descendants(): BaseEvent[] {
644
+ const descendants: BaseEvent[] = []
645
+ const visited = new Set<string>()
646
+ const root_id = this.event_id
647
+ const stack = [...this.event_children]
648
+
649
+ while (stack.length > 0) {
650
+ const child = stack.pop()
651
+ if (!child) {
652
+ continue
653
+ }
654
+ const child_id = child.event_id
655
+ if (child_id === root_id) {
656
+ continue
657
+ }
658
+ if (visited.has(child_id)) {
659
+ continue
660
+ }
661
+ visited.add(child_id)
662
+ descendants.push(child)
663
+ if (child.event_children.length > 0) {
664
+ stack.push(...child.event_children)
665
+ }
666
+ }
667
+
668
+ return descendants
669
+ }
670
+
671
+ emit<T extends BaseEvent>(event: T): T {
672
+ const original_parent = this._event_original ?? this
673
+ const original_child = event._event_original ?? event
674
+ if (!original_child.event_parent_id && original_child.event_id !== original_parent.event_id) {
675
+ original_child.event_parent_id = original_parent.event_id
676
+ }
677
+ if (!this.event_bus) {
678
+ throw new Error('event has no bus attached')
679
+ }
680
+ return this.event_bus.emit(original_child as T)
681
+ }
682
+
683
+ // force-abort processing of all pending descendants of an event regardless of whether they have already started
684
+ _cancelPendingChildProcessing(reason: unknown): void {
685
+ const original = this._event_original ?? this
686
+ const cancellation_cause =
687
+ reason instanceof EventHandlerTimeoutError
688
+ ? reason
689
+ : reason instanceof EventHandlerCancelledError || reason instanceof EventHandlerAbortedError
690
+ ? reason.cause instanceof Error
691
+ ? reason.cause
692
+ : reason
693
+ : reason instanceof Error
694
+ ? reason
695
+ : new Error(String(reason))
696
+ const visited = new Set<string>()
697
+ const cancelChildEvent = (child: BaseEvent): void => {
698
+ const original_child = child._event_original ?? child
699
+ if (visited.has(original_child.event_id)) {
700
+ return
701
+ }
702
+ visited.add(original_child.event_id)
703
+
704
+ // Depth-first: cancel grandchildren before parent so
705
+ // _areAllChildrenComplete() returns true when we get back up.
706
+ for (const grandchild of original_child.event_children) {
707
+ const original_grandchild = grandchild._event_original ?? grandchild
708
+ if (!original_grandchild.event_blocks_parent_completion) {
709
+ continue
710
+ }
711
+ cancelChildEvent(grandchild)
712
+ }
713
+
714
+ original_child._markCancelled(cancellation_cause)
715
+
716
+ // Force-complete the child event. In JS we can't stop running async
717
+ // handlers, but _markCompleted() resolves the done() promise so callers
718
+ // aren't blocked waiting for background work to finish. The background
719
+ // handler's eventual _markCompleted/_markError is a no-op (terminal guard).
720
+ if (original_child.event_status !== 'completed') {
721
+ original_child._markCompleted()
722
+ }
723
+ }
724
+
725
+ for (const child of original.event_children) {
726
+ const original_child = child._event_original ?? child
727
+ if (!original_child.event_blocks_parent_completion) {
728
+ continue
729
+ }
730
+ cancelChildEvent(child)
731
+ }
732
+ }
733
+
734
+ // Cancel all handler results for an event except the winner, used by first() mode.
735
+ // Cancels pending handlers immediately, aborts started handlers via _signalAbort(),
736
+ // and cancels any child events emitted by the losing handlers.
737
+ _markRemainingFirstModeResultCancelled(winner: EventResult): void {
738
+ const cause = new Error('first() resolved: another handler returned a result first')
739
+ const bus_id = winner.eventbus_id
740
+
741
+ for (const result of this.event_results.values()) {
742
+ if (result === winner) continue
743
+ if (result.eventbus_id !== bus_id) continue
744
+
745
+ if (result.status === 'pending') {
746
+ result._markError(
747
+ new EventHandlerCancelledError(`Cancelled: first() resolved`, {
748
+ event_result: result,
749
+ cause,
750
+ })
751
+ )
752
+ } else if (result.status === 'started') {
753
+ // Cancel child events emitted by this handler before aborting it
754
+ for (const child of result.event_children) {
755
+ const original_child = child._event_original ?? child
756
+ if (!original_child.event_blocks_parent_completion) {
757
+ continue
758
+ }
759
+ original_child._cancelPendingChildProcessing(cause)
760
+ original_child._markCancelled(cause)
761
+ }
762
+
763
+ // Abort the handler itself
764
+ result._lock?.exitHandlerRun()
765
+ const aborted_error = new EventHandlerAbortedError(`Aborted: first() resolved`, {
766
+ event_result: result,
767
+ cause,
768
+ })
769
+ result._markError(aborted_error)
770
+ result._signalAbort(aborted_error)
771
+ }
772
+ }
773
+ }
774
+
775
+ // force-abort processing of this event regardless of whether it is pending or has already started
776
+ _markCancelled(cause: Error): void {
777
+ const original = this._event_original ?? this
778
+ if (!this.event_bus) {
779
+ if (original.event_status !== 'completed') {
780
+ original._markCompleted()
781
+ }
782
+ return
783
+ }
784
+ const path = Array.isArray(original.event_path) ? original.event_path : []
785
+ const buses_to_cancel = new Set<string>(path)
786
+ for (const bus of this.event_bus.all_instances) {
787
+ if (!buses_to_cancel.has(bus.label)) {
788
+ continue
789
+ }
790
+
791
+ const handler_entries = original._createPendingHandlerResults(bus)
792
+ let updated = false
793
+ for (const entry of handler_entries) {
794
+ if (entry.result.status === 'pending') {
795
+ const cancelled_error = new EventHandlerCancelledError(`Cancelled pending handler due to parent error: ${cause.message}`, {
796
+ event_result: entry.result,
797
+ cause,
798
+ })
799
+ entry.result._markError(cancelled_error)
800
+ updated = true
801
+ } else if (entry.result.status === 'started') {
802
+ entry.result._lock?.exitHandlerRun()
803
+ const aborted_error = new EventHandlerAbortedError(`Aborted running handler due to parent error: ${cause.message}`, {
804
+ event_result: entry.result,
805
+ cause,
806
+ })
807
+ entry.result._markError(aborted_error)
808
+ entry.result._signalAbort(aborted_error)
809
+ updated = true
810
+ }
811
+ }
812
+
813
+ const removed = bus.removeEventFromPendingQueue(original)
814
+
815
+ if (removed > 0 && !bus.isEventInFlightOrQueued(original.event_id)) {
816
+ original.event_pending_bus_count = Math.max(0, original.event_pending_bus_count - 1)
817
+ }
818
+
819
+ if (updated || removed > 0) {
820
+ original._markCompleted(false)
821
+ }
822
+ }
823
+
824
+ if (original.event_status !== 'completed') {
825
+ original._markCompleted()
826
+ }
827
+ }
828
+
829
+ _notifyEventParentsOfCompletion(): void {
830
+ const original = this._event_original ?? this
831
+ if (!this.event_bus) {
832
+ return
833
+ }
834
+ const visited = new Set<string>()
835
+ let parent_id = original.event_parent_id
836
+ while (parent_id && !visited.has(parent_id)) {
837
+ visited.add(parent_id)
838
+ const parent = this.event_bus.findEventById(parent_id)
839
+ if (!parent) {
840
+ break
841
+ }
842
+ parent._markCompleted(false, false)
843
+ if (parent.event_status !== 'completed') {
844
+ break
845
+ }
846
+ parent_id = parent.event_parent_id
847
+ }
848
+ }
849
+
850
+ // awaitable that triggers immediate (queue-jump) processing of the event on all buses where it is queued
851
+ // use eventCompleted() to wait for normal queue-order completion without queue-jumping.
852
+ done(options: EventDoneOptions = {}): Promise<this> {
853
+ if (!this.event_bus) {
854
+ return Promise.reject(new Error('event has no bus attached'))
855
+ }
856
+ const original = this._event_original ?? this
857
+ original._markBlocksParentCompletionIfAwaitedFromEmittingHandler()
858
+ const raise_if_any = options.raise_if_any ?? true
859
+ const completion_promise =
860
+ this.event_status === 'completed' ? Promise.resolve(original as this) : this.event_bus._processEventImmediately(this)
861
+
862
+ if (!raise_if_any) {
863
+ return completion_promise
864
+ }
865
+
866
+ // Always delegate to _processEventImmediately — it walks up the parent event tree
867
+ // to determine whether we're inside a handler (works cross-bus). If no
868
+ // ancestor handler is in-flight, it falls back to eventCompleted().
869
+ return completion_promise.then((completed_event) => {
870
+ const first_error = completed_event._firstProcessingError()
871
+ if (first_error !== undefined) {
872
+ if (first_error instanceof Error) {
873
+ throw first_error
874
+ }
875
+ throw new Error(String(first_error))
876
+ }
877
+ return completed_event
878
+ })
879
+ }
880
+
881
+ // returns the first non-undefined handler result value, cancelling remaining handlers
882
+ // when any handler completes. Works with all event_handler_concurrency modes:
883
+ // parallel: races all handlers, returns first non-undefined, aborts the rest
884
+ // serial: runs handlers sequentially, returns first non-undefined, skips remaining
885
+ first(): Promise<EventResultType<this> | undefined> {
886
+ if (!this.event_bus) {
887
+ return Promise.reject(new Error('event has no bus attached'))
888
+ }
889
+ const original = this._event_original ?? this
890
+ original.event_handler_completion = 'first'
891
+ return this.done({ raise_if_any: false }).then((completed_event) => {
892
+ const first_error = completed_event._firstProcessingError({ ignore_first_mode_control_errors: true })
893
+ if (first_error !== undefined) {
894
+ if (first_error instanceof Error) {
895
+ throw first_error
896
+ }
897
+ throw new Error(String(first_error))
898
+ }
899
+ const orig = completed_event._event_original ?? completed_event
900
+ return Array.from(orig.event_results.values())
901
+ .filter(
902
+ (result) =>
903
+ result.status === 'completed' && result.result !== undefined && result.result !== null && !(result.result instanceof BaseEvent)
904
+ )
905
+ .sort((a, b) => compareIsoDatetime(a.completed_at, b.completed_at))
906
+ .map((result) => result.result as EventResultType<this>)
907
+ .at(0)
908
+ })
909
+ }
910
+
911
+ // returns handler result values in event_results insertion order.
912
+ // equivalent to await event.done(); Array.from(event.event_results.values()).map((entry) => entry.result)
913
+ eventResultsList(
914
+ include: EventResultsListInclude<this>,
915
+ options?: EventResultsListOptions<this>
916
+ ): Promise<Array<EventResultType<this> | undefined>>
917
+ eventResultsList(options?: EventResultsListOptions<this>): Promise<Array<EventResultType<this> | undefined>>
918
+ async eventResultsList(
919
+ include_or_options?: EventResultsListInclude<this> | EventResultsListOptions<this>,
920
+ maybe_options?: EventResultsListOptions<this>
921
+ ): Promise<Array<EventResultType<this> | undefined>> {
922
+ const default_include: EventResultsListInclude<this> = (_result, event_result) =>
923
+ event_result.status === 'completed' &&
924
+ event_result.result !== undefined &&
925
+ event_result.result !== null &&
926
+ !(event_result.result instanceof Error) &&
927
+ !(event_result.result instanceof BaseEvent) &&
928
+ event_result.error === undefined
929
+
930
+ let options: EventResultsListOptions<this>
931
+ let include: EventResultsListInclude<this>
932
+ if (typeof include_or_options === 'function') {
933
+ options = maybe_options ?? {}
934
+ include = include_or_options
935
+ } else {
936
+ options = include_or_options ?? {}
937
+ include = options.include ?? default_include
938
+ }
939
+ const raise_if_any = options.raise_if_any ?? true
940
+ const raise_if_none = options.raise_if_none ?? true
941
+
942
+ const original = this._event_original ?? this
943
+ const resolved_timeout_seconds = options.timeout ?? original.event_timeout ?? this.event_bus?.event_timeout ?? null
944
+ let completed_event: this
945
+
946
+ if (resolved_timeout_seconds === null) {
947
+ completed_event = await this.done({ raise_if_any: false })
948
+ } else {
949
+ completed_event = await _runWithTimeout(
950
+ resolved_timeout_seconds,
951
+ () => new Error(`Timed out waiting for ${original.event_type} results after ${resolved_timeout_seconds}s`),
952
+ () => this.done({ raise_if_any: false })
953
+ )
954
+ }
955
+
956
+ const all_results: EventResult<this>[] = Array.from(completed_event.event_results.values())
957
+ const error_results = all_results.filter((event_result) => event_result.error !== undefined || event_result.result instanceof Error)
958
+
959
+ if (raise_if_any && error_results.length > 0) {
960
+ if (error_results.length === 1) {
961
+ const first_error = error_results[0]
962
+ if (first_error.error instanceof Error) {
963
+ throw first_error.error
964
+ }
965
+ if (first_error.result instanceof Error) {
966
+ throw first_error.result
967
+ }
968
+ throw new Error(String(first_error.error ?? first_error.result))
969
+ }
970
+
971
+ const errors = error_results.map((event_result) => {
972
+ if (event_result.error instanceof Error) {
973
+ return event_result.error
974
+ }
975
+ if (event_result.result instanceof Error) {
976
+ return event_result.result
977
+ }
978
+ return new Error(String(event_result.error ?? event_result.result))
979
+ })
980
+ throw new AggregateError(
981
+ errors,
982
+ `Event ${completed_event.event_type}#${completed_event.event_id.slice(-4)} had ${errors.length} handler error(s)`
983
+ )
984
+ }
985
+
986
+ const included_results = all_results.filter((event_result) => include(event_result.result, event_result))
987
+ if (raise_if_none && included_results.length === 0) {
988
+ throw new Error(
989
+ `Expected at least one handler to return a non-null result, but none did: ${completed_event.event_type}#${completed_event.event_id.slice(-4)}`
990
+ )
991
+ }
992
+
993
+ return included_results.map((event_result) => event_result.result)
994
+ }
995
+
996
+ // awaitable that waits for the event to be processed in normal queue order by the _runloop
997
+ eventCompleted(): Promise<this> {
998
+ const original = this._event_original ?? this
999
+ original._markBlocksParentCompletionIfAwaitedFromEmittingHandler()
1000
+ if (this.event_status === 'completed') {
1001
+ return Promise.resolve(this)
1002
+ }
1003
+ this._notifyDoneListeners()
1004
+ return this._event_completed_signal!.promise
1005
+ }
1006
+
1007
+ _markBlocksParentCompletionIfAwaitedFromEmittingHandler(): void {
1008
+ const original = this._event_original ?? this
1009
+ if (original.event_blocks_parent_completion || !original.event_bus) {
1010
+ return
1011
+ }
1012
+ const active_result = original.event_bus.locks._getActiveHandlerResultForCurrentAsyncContext()
1013
+ if (!active_result || active_result.status !== 'started') {
1014
+ return
1015
+ }
1016
+ const active_parent = active_result.event._event_original ?? active_result.event
1017
+ const is_child_of_active_handler =
1018
+ original.event_parent_id === active_parent.event_id &&
1019
+ original.event_emitted_by_handler_id === active_result.handler_id &&
1020
+ active_result.event_children.some((child) => (child._event_original ?? child).event_id === original.event_id)
1021
+ if (is_child_of_active_handler) {
1022
+ original.event_blocks_parent_completion = true
1023
+ }
1024
+ }
1025
+
1026
+ _markPending(): this {
1027
+ const original = this._event_original ?? this
1028
+ original.event_status = 'pending'
1029
+ original.event_started_at = null
1030
+ original.event_completed_at = null
1031
+ original.event_results.clear()
1032
+ original.event_pending_bus_count = 0
1033
+ original._setDispatchContext(undefined)
1034
+ original._event_completed_signal = null
1035
+ original._lock_for_event_handler = null
1036
+ original.event_bus = undefined
1037
+ return this
1038
+ }
1039
+
1040
+ eventReset(): this {
1041
+ const original = this._event_original ?? this
1042
+ const ctor = original.constructor as typeof BaseEvent
1043
+ const fresh_event = ctor.fromJSON(original.toJSON()) as this
1044
+ fresh_event.event_id = uuidv7()
1045
+ return fresh_event._markPending()
1046
+ }
1047
+
1048
+ _markStarted(started_at: string | null = null, notify_hook: boolean = true): void {
1049
+ const original = this._event_original ?? this
1050
+ if (original.event_status !== 'pending') {
1051
+ return
1052
+ }
1053
+ original.event_status = 'started'
1054
+ original.event_started_at = started_at === null ? monotonicDatetime() : monotonicDatetime(started_at)
1055
+ if (notify_hook && original.event_bus) {
1056
+ const bus_for_hook = original.event_bus
1057
+ const event_for_bus = bus_for_hook._getEventProxyScopedToThisBus(original)
1058
+ void bus_for_hook.onEventChange(event_for_bus, 'started')
1059
+ }
1060
+ }
1061
+
1062
+ _markCompleted(force: boolean = true, notify_parents: boolean = true): void {
1063
+ const original = this._event_original ?? this
1064
+ if (original.event_status === 'completed') {
1065
+ return
1066
+ }
1067
+ if (!force) {
1068
+ if (original.event_pending_bus_count > 0) {
1069
+ return
1070
+ }
1071
+ if (!original._areAllChildrenComplete()) {
1072
+ return
1073
+ }
1074
+ }
1075
+ original.event_status = 'completed'
1076
+ original.event_completed_at = monotonicDatetime()
1077
+ if (original.event_bus) {
1078
+ const bus_for_hook = original.event_bus
1079
+ const event_for_bus = bus_for_hook._getEventProxyScopedToThisBus(original)
1080
+ void bus_for_hook.onEventChange(event_for_bus, 'completed')
1081
+ }
1082
+ original._setDispatchContext(null)
1083
+ original._notifyDoneListeners()
1084
+ original._event_completed_signal!.resolve(original)
1085
+ original._event_completed_signal = null
1086
+ original.dropFromZeroHistoryBuses()
1087
+ if (notify_parents && original.event_bus) {
1088
+ original._notifyEventParentsOfCompletion()
1089
+ }
1090
+ }
1091
+
1092
+ private dropFromZeroHistoryBuses(): void {
1093
+ if (!this.event_bus) {
1094
+ return
1095
+ }
1096
+ const original = this._event_original ?? this
1097
+ for (const bus of this.event_bus.all_instances) {
1098
+ if (bus.event_history.max_history_size !== 0) {
1099
+ continue
1100
+ }
1101
+ bus.removeEventFromHistory(original.event_id)
1102
+ }
1103
+ }
1104
+
1105
+ get event_errors(): unknown[] {
1106
+ return (
1107
+ Array.from(this.event_results.values())
1108
+ // filter for events that have completed + have non-undefined error values
1109
+ .filter((event_result) => event_result.error !== undefined && event_result.completed_at !== null)
1110
+ // sort by completion time
1111
+ .sort((event_result_a, event_result_b) => compareIsoDatetime(event_result_a.completed_at, event_result_b.completed_at))
1112
+ // assemble array of flat error values
1113
+ .map((event_result) => event_result.error)
1114
+ )
1115
+ }
1116
+
1117
+ private _isFirstModeControlError(error: unknown): boolean {
1118
+ if (!(error instanceof EventHandlerCancelledError || error instanceof EventHandlerAbortedError)) {
1119
+ return false
1120
+ }
1121
+ if (error.message.includes('first() resolved')) {
1122
+ return true
1123
+ }
1124
+ return error.cause instanceof Error && error.cause.message.includes('first() resolved')
1125
+ }
1126
+
1127
+ _firstProcessingError(options: { ignore_first_mode_control_errors?: boolean } = {}): unknown | undefined {
1128
+ const ignore_first_mode_control_errors = options.ignore_first_mode_control_errors ?? false
1129
+ return Array.from(this.event_results.values())
1130
+ .filter((event_result) => event_result.error !== undefined && event_result.completed_at !== null)
1131
+ .filter((event_result) => (ignore_first_mode_control_errors ? !this._isFirstModeControlError(event_result.error) : true))
1132
+ .sort((event_result_a, event_result_b) => compareIsoDatetime(event_result_a.completed_at, event_result_b.completed_at))
1133
+ .map((event_result) => event_result.error)
1134
+ .at(0)
1135
+ }
1136
+
1137
+ // Returns the first non-undefined completed handler result, sorted by completion time.
1138
+ // Useful after first() or done() to get the winning result value.
1139
+ get event_result(): EventResultType<this> | undefined {
1140
+ return Array.from(this.event_results.values())
1141
+ .filter((event_result) => event_result.completed_at !== null && event_result.result !== undefined)
1142
+ .sort((event_result_a, event_result_b) => compareIsoDatetime(event_result_a.completed_at, event_result_b.completed_at))
1143
+ .map((event_result) => event_result.result as EventResultType<this>)
1144
+ .at(0)
1145
+ }
1146
+
1147
+ _areAllChildrenComplete(visited: Set<string> = new Set()): boolean {
1148
+ const original = this._event_original ?? this
1149
+ if (visited.has(original.event_id)) {
1150
+ return true
1151
+ }
1152
+ visited.add(original.event_id)
1153
+
1154
+ for (const child of original.event_children) {
1155
+ const original_child = child._event_original ?? child
1156
+ if (!original_child.event_blocks_parent_completion) {
1157
+ continue
1158
+ }
1159
+ if (original_child.event_status !== 'completed') {
1160
+ return false
1161
+ }
1162
+ if (!original_child._areAllChildrenComplete(visited)) {
1163
+ return false
1164
+ }
1165
+ }
1166
+ return true
1167
+ }
1168
+
1169
+ private _notifyDoneListeners(): void {
1170
+ if (this._event_completed_signal) {
1171
+ return
1172
+ }
1173
+ this._event_completed_signal = withResolvers<this>()
1174
+ }
1175
+
1176
+ // Break internal reference chains so a completed event can be GC'd when
1177
+ // Evicted from event_history. Called by EventHistory.trimEventHistory().
1178
+ _gc(): void {
1179
+ this._event_completed_signal = null
1180
+ this._setDispatchContext(null)
1181
+ this.event_bus = undefined
1182
+ this._lock_for_event_handler = null
1183
+ for (const result of this.event_results.values()) {
1184
+ result.event_children = []
1185
+ }
1186
+ this.event_results.clear()
1187
+ }
1188
+ }
1189
+
1190
+ const hydrateEventResults = <TEvent extends BaseEvent>(event: TEvent, raw_event_results: unknown): Map<string, EventResult<TEvent>> => {
1191
+ const event_results = new Map<string, EventResult<TEvent>>()
1192
+ if (!Array.isArray(raw_event_results)) {
1193
+ return event_results
1194
+ }
1195
+ for (const item of raw_event_results) {
1196
+ const result = EventResult.fromJSON(event, item)
1197
+ const map_key = typeof result.handler_id === 'string' && result.handler_id.length > 0 ? result.handler_id : result.id
1198
+ event_results.set(map_key, result)
1199
+ }
1200
+ return event_results
1201
+ }